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.
Add this line to your application's Gemfile:
gem 'acfs', '~> 1.7'
And then execute:
> bundle
Or install it yourself as:
> gem install acfs
First you need to define your service(s):
class UserService < Acfs::Service
self.base_url = 'http://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 # => "Miraculix"
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
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
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
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
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.run
acfs.run
: Run all queued operations.
Read the official guide on how to subscribe to these events.
- 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
MIT License
Copyright (c) 2013-2022 Jan Graichen. MIT license, see LICENSE for more details.