Acfs - API client for services

Gem Version Build Status Coverage Status Code Climate Dependency Status RubyDoc Documentation

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 on a per service level and automatic request queuing and parallel processing. See Usage for more.


Add this line to your application's Gemfile:

gem 'acfs', '~> 0.21.0'

Note: Acfs is under development. I'll try to avoid changes to the public API but internal APIs may change quite often.

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 = ''

  # 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

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


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 # This will run all queued request as parallel as possible.
         # For @user the following URL will be requested:
         # `` # => "..."

@users = User.all
@users.loaded? #=> false # Will request ``

@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 ``
  @comments = Comment.where user:

  # 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 # This call will fire all request as parallel as possible.
         # The sequence above would look similar to:
         # Start                Fin
         #   |===================|       ``
         #   |====|                      /users/5
         #   |    |==============|       /comments?user=5
         #   |    |======|               /users/1
         #   |    |=======|              /users/4
         #   |    |======|               /users/10

# Now we can access all resources:       # => "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 params will be sent as GET parameters and can be used for filtering in the service's controller. ```ruby @user = User.find_by age: 24 # Will request

@user # Contains the first user object returned by the index action `` If no object can be found,.find_bywill returnnil. The optional callback will then be called withnilas parameter. Use.find_by!to raise anAcfs::ResourceNotFoundexception 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 = "Bob"

@user.changed? # => true
@user.persisted? # => false # 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


The following code explains the routing for singleton resource requests:

my_single = # sends POST request to /single

my_single = Single.find # sends GET request to /single

my_single.age = 28 # sends PUT request to /single

my_single.delete # sends DELETE request to /single

You also can pass parameters to the find call, these will sent as GET params to the index action:

my_single = Single.find name: 'Max' # 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

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

@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: '', password: 's3cr3t' }, return: { id: 'longhash', user: 1 }
  Acfs::Stub.resource MyUser, :update, with: lambda { |op| :my_var }, raise: 400

it 'should find user number one' do
  user = MyUser.find 1

  expect( be == 1
  expect( be == 'John Smith'
  expect(user.age).to be == 32

  expect(@stub).to be_called
  expect(@stub).to_not be_called 5.times

it 'should not find user number two' do
  MyUser.find 3

  expect { }.to raise_error(Acfs::ResourceNotFound)

it 'should allow stub resource creation' do
  session = Session.create! ident: '', password: 's3cr3t'

  expect( be == 'longhash'
  expect(session.user).to be == 1

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

it 'should find user number one' do
  user = MyUser.find 1             # Would have raised Acfs::RealRequestNotAllowedError
                       # Will run real request to user service instead.


Acfs supports instrumentation via active support.

Acfs expose to following events

  • acfs.operation.complete(operation, response): Acfs operation completed
  • acfs.runner.sync_run(operation): Run operation right now skipping queue.
  • acfs.runner.enqueue(operation): Enqueue operation to be run later.
  • acfs.before_run: directly before
  • Run all queued operations.

Read official guide to see to to subscribe.


  • Update
    • Better new? detection eg. storing ETag from request resources.
    • Use PATCH for with only changed attributes and If-Unmodifed-Since and If-Match header fields if resource was surly loaded from service and not created with an id (e.g id: 5, name: "john").
    • Conflict detection (ETag / If-Unmodified-Since)
  • High level features
    • Support for custom mime types on client and server side. (application/vnd.myservice.user.v2+msgpack)
    • Server side components
      • Reusing model definitions for generating responses?
      • Rails responders providing REST operations with integrated ETag, Modified Headers, conflict detection, ...
    • Pagination? Filtering? (If service API provides such features.)
  • Documentation


  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Add specs for your feature
  4. Implement your feature
  5. Commit your changes (git commit -am 'Add some feature')
  6. Push to the branch (git push origin my-new-feature)
  7. Create new Pull Request



MIT License

Copyright (c) 2013 Jan Graichen. MIT license, see LICENSE for more details.