Acfs
API client for services
Install / Use
/learn @jgraichen/AcfsREADME
Acfs - API client for services
Acfs is a library to develop API client libraries for single services within a larger service oriented application.
Acfs covers model and service abstraction, convenient query and filter methods, full middleware stack for pre-processing requests and responses, as well as automatic request queuing and parallel processing.
Installation
Add this line to your application's Gemfile:
gem 'acfs', '~> 2.1'
And then execute:
bundle
Or install it yourself as:
gem install acfs
Usage
First you need to define your service(s):
class UserService < Acfs::Service
self.base_url = 'https://users.myapp.org'
# You can configure middlewares you want to use for the service here.
# Each service has it own middleware stack.
#
use Acfs::Middleware::JsonDecoder
use Acfs::Middleware::MessagePackDecoder
end
This specifies where the UserService is located. You can now create some models representing resources served by the UserService.
class User < Acfs::Resource
service UserService # Associate `User` model with `UserService`.
# Define model attributes and types
# Types are needed to parse and generate request and response payload.
attribute :id, :uuid # Types can be classes or symbols.
# Symbols will be used to load a class from `Acfs::Model::Attributes` namespace.
# Eg. `:uuid` will load class `Acfs::Model::Attributes::Uuid`.
attribute :name, :string, default: 'Anonymous'
attribute :age, ::Acfs::Model::Attributes::Integer # Or use :integer
end
The service and model classes can be shipped as a gem or git submodule to be included by the frontend application(s).
You can use the model there:
@user = User.find 14
@user.loaded? #=> false
Acfs.run # This will run all queued request as parallel as possible.
# For @user the following URL will be requested:
# `http://users.myapp.org/users/14`
@model.name # => "..."
@users = User.all
@users.loaded? #=> false
Acfs.run # Will request `http://users.myapp.org/users`
@users #=> [<User>, ...]
If you need multiple resources or dependent resources first define a "plan" how they can be loaded:
@user = User.find(5) do |user|
# Block will be executed right after user with id 5 is loaded
# You can load additional resources also from other services
# Eg. fetch comments from `CommentSerivce`. The line below will
# load comments from `http://comments.myapp.org/comments?user=5`
@comments = Comment.where user: user.id
# You can load multiple resources in parallel if you have multiple
# ids.
@friends = User.find 1, 4, 10 do |friends|
# This block will be executed when all friends are loaded.
# [ ... ]
end
end
Acfs.run # This call will fire all request as parallel as possible.
# The sequence above would look similar to:
#
# Start Fin
# |===================| `Acfs.run`
# |====| /users/5
# | |==============| /comments?user=5
# | |======| /users/1
# | |=======| /users/4
# | |======| /users/10
# Now we can access all resources:
@user.name # => "John
@comments.size # => 25
@friends[0].name # => "Miracolix"
Use .find_by to get first element only. .find_by will call the index-Action and return the first resource. Optionally passed parameters will be sent as GET parameters and can be used for filtering in the service's controller.
@user = User.find_by age: 24
Acfs.run # Will request `http://users.myapp.org/users?age=24`
@user # Contains the first user object returned by the index action
If no object can be found, .find_by will return nil. The optional callback will then be called with nil as parameter. Use .find_by! to raise an Acfs::ResourceNotFound exception if no object can be found. .find_by! will only invoke the optional callback if an object was successfully loaded.
Acfs has basic update support using PUT requests:
@user = User.find 5
@user.name = "Bob"
@user.changed? # => true
@user.persisted? # => false
@user.save # Or .save!
# Will PUT new resource to service synchronously.
@user.changed? # => false
@user.persisted? # => true
Singleton resources
Singletons can be used in Acfs by creating a new resource which inherits from SingletonResource:
class Single < Acfs::SingletonResource
service UserService # Associate `Single` model with `UserService`.
# Define model attributes and types as with regular resources
attribute :name, :string, default: 'Anonymous'
attribute :age, :integer
end
The following code explains the routing for singleton resource requests:
my_single = Single.new
mysingle.save # sends POST request to /single
my_single = Single.find
Acfs.run # sends GET request to /single
my_single.age = 28
my_single.save # sends PUT request to /single
my_single.delete # sends DELETE request to /single
You also can pass parameters to the find call. They will be sent as query parameters to the index action:
my_single = Single.find name: 'Max'
Acfs.run # sends GET request with param to /single?name=Max
Resource Inheritance
Acfs provides a resource inheritance similar to ActiveRecord Single Table Inheritance. If a type attribute exists and is a valid subclass of your resource they will be converted to you subclassed resources:
class Computer < Acfs::Resource
...
end
class Pc < Computer end
class Mac < Computer end
With the following response on GET /computers the collection will contain the appropriate subclass resources:
[
{ "id": 5, "type": "Computer"},
{ "id": 6, "type": "Mac"},
{ "id": 8, "type": "Pc"}
]
@computers = Computer.all
Acfs.run
@computer[0].class # => Computer
@computer[1].class # => Mac
@computer[2].class # => Pc
Stubbing
You can stub resources in applications using an Acfs service client:
# spec_helper.rb
# This will enable stabs before each spec and clear internal state
# after each spec.
require 'acfs/rspec'
before do
@stub = Acfs::Stub.resource MyUser, :read, with: { id: 1 }, return: { id: 1, name: 'John Smith', age: 32 }
Acfs::Stub.resource MyUser, :read, with: { id: 2 }, raise: :not_found
Acfs::Stub.resource Session, :create, with: { ident: 'john@exmaple.org', password: 's3cr3t' }, return: { id: 'longhash', user: 1 }
Acfs::Stub.resource MyUser, :update, with: lambda { |op| op.data.include? :my_var }, raise: 400
end
it 'should find user number one' do
user = MyUser.find 1
Acfs.run
expect(user.id).to eq 1
expect(user.name).to eq 'John Smith'
expect(user.age).to eq 32
expect(@stub).to be_called
expect(@stub).to_not be_called 5.times
end
it 'should not find user number two' do
MyUser.find 3
expect { Acfs.run }.to raise_error(Acfs::ResourceNotFound)
end
it 'should allow stub resource creation' do
session = Session.create! ident: 'john@exmaple.org', password: 's3cr3t'
expect(session.id).to eq 'longhash'
expect(session.user).to eq 1
end
By default, Acfs raises an error when a non stubbed resource should be requested. You can switch of the behavior:
before do
Acfs::Stub.allow_requests = true
end
it 'should find user number one' do
user = MyUser.find 1
Acfs.run # Would have raised Acfs::RealRequestNotAllowedError
# Will run real request to user service instead.
end
Instrumentation
Acfs supports instrumentation via active support and exposes the following events:
acfs.operation.complete(operation, response): Acfs operation completedacfs.runner.sync_run(operation): Run operation right now skipping queue.acfs.runner.enqueue(operation): Enqueue operation to be run later.acfs.before_run: directly beforeacfs.runacfs.run: Run all queued operations.
Read the official guide on how to subscribe to these events.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Add specs for your feature
- Implement your feature
- Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
License
Related Skills
bluebubbles
353.3kUse when you need to send or manage iMessages via BlueBubbles (recommended iMessage integration). Calls go through the generic message tool with channel="bluebubbles".
gh-issues
353.3kFetch GitHub issues, spawn sub-agents to implement fixes and open PRs, then monitor and address PR review comments. Usage: /gh-issues [owner/repo] [--label bug] [--limit 5] [--milestone v1.0] [--assignee @me] [--fork user/repo] [--watch] [--interval 5] [--reviews-only] [--cron] [--dry-run] [--model glm-5] [--notify-channel -1002381931352]
healthcheck
353.3kHost security hardening and risk-tolerance configuration for OpenClaw deployments
node-connect
353.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
