Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/jgraichen/acfs
API client for services
https://github.com/jgraichen/acfs
activemodel rails rest rest-client ruby services
Last synced: 1 day ago
JSON representation
API client for services
- Host: GitHub
- URL: https://github.com/jgraichen/acfs
- Owner: jgraichen
- License: mit
- Created: 2013-03-18T19:49:18.000Z (almost 12 years ago)
- Default Branch: main
- Last Pushed: 2025-01-17T09:19:04.000Z (13 days ago)
- Last Synced: 2025-01-21T17:07:20.414Z (9 days ago)
- Topics: activemodel, rails, rest, rest-client, ruby, services
- Language: Ruby
- Homepage:
- Size: 777 KB
- Stars: 13
- Watchers: 8
- Forks: 8
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# Acfs - *API client for services*
[![Gem Version](https://img.shields.io/gem/v/acfs?logo=ruby)](https://rubygems.org/gems/acfs)
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/jgraichen/acfs/test.yml?logo=github)](https://github.com/jgraichen/acfs/actions)
[![Coverage Status](http://img.shields.io/coveralls/jgraichen/acfs/master.svg)](https://coveralls.io/r/jgraichen/acfs)
[![RubyDoc Documentation](http://img.shields.io/badge/rubydoc-here-blue.svg)](http://rubydoc.info/github/jgraichen/acfs/master/frames)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:
```ruby
gem 'acfs', '~> 2.0'
```And then execute:
```console
bundle
```Or install it yourself as:
```console
gem install acfs
```## Usage
First you need to define your service(s):
```ruby
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`.
```ruby
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 :integerend
```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:
```ruby
@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? #=> falseAcfs.run # Will request `http://users.myapp.org/users`
@users #=> [, ...]
```If you need multiple resources or dependent resources first define a "plan" how they can be loaded:
```ruby
@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
endAcfs.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.
```ruby
@user = User.find_by age: 24Acfs.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:
```ruby
@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`:
```ruby
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, :integerend
```The following code explains the routing for singleton resource requests:
```ruby
my_single = Single.new
mysingle.save # sends POST request to /singlemy_single = Single.find
Acfs.run # sends GET request to /singlemy_single.age = 28
my_single.save # sends PUT request to /singlemy_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:
```ruby
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:
```ruby
class Computer < Acfs::Resource
...
endclass Pc < Computer end
class Mac < Computer end
```With the following response on `GET /computers` the collection will contain the appropriate subclass resources:
```json
[
{ "id": 5, "type": "Computer"},
{ "id": 6, "type": "Mac"},
{ "id": 8, "type": "Pc"}
]
``````ruby
@computers = Computer.allAcfs.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:
```ruby
# spec_helper.rb# This will enable stabs before each spec and clear internal state
# after each spec.
require 'acfs/rspec'
``````ruby
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: '[email protected]', password: 's3cr3t' }, return: { id: 'longhash', user: 1 }
Acfs::Stub.resource MyUser, :update, with: lambda { |op| op.data.include? :my_var }, raise: 400
endit 'should find user number one' do
user = MyUser.find 1
Acfs.runexpect(user.id).to eq 1
expect(user.name).to eq 'John Smith'
expect(user.age).to eq 32expect(@stub).to be_called
expect(@stub).to_not be_called 5.times
endit 'should not find user number two' do
MyUser.find 3expect { Acfs.run }.to raise_error(Acfs::ResourceNotFound)
endit 'should allow stub resource creation' do
session = Session.create! ident: '[email protected]', 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:
```ruby
before do
Acfs::Stub.allow_requests = true
endit '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][1] and exposes the 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 `acfs.run`
* `acfs.run`: Run all queued operations.Read the [official guide][2] on how to subscribe to these events.
[1]: http://guides.rubyonrails.org/active_support_instrumentation.html
[2]: http://guides.rubyonrails.org/active_support_instrumentation.html#subscribing-to-an-event## Contributing
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## License
[MIT License](LICENSE)