{"id":16956986,"url":"https://github.com/jgraichen/acfs","last_synced_at":"2025-09-10T22:33:08.903Z","repository":{"id":7512383,"uuid":"8862653","full_name":"jgraichen/acfs","owner":"jgraichen","description":"API client for services","archived":false,"fork":false,"pushed_at":"2025-02-14T15:53:28.000Z","size":813,"stargazers_count":13,"open_issues_count":1,"forks_count":8,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-03-31T07:08:57.583Z","etag":null,"topics":["activemodel","rails","rest","rest-client","ruby","services"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jgraichen.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2013-03-18T19:49:18.000Z","updated_at":"2025-02-14T15:53:33.000Z","dependencies_parsed_at":"2024-12-24T07:11:13.449Z","dependency_job_id":"2aa05b46-78b6-48f3-9aaf-f95c39b8ec66","html_url":"https://github.com/jgraichen/acfs","commit_stats":{"total_commits":623,"total_committers":16,"mean_commits":38.9375,"dds":0.2937399678972713,"last_synced_commit":"6618b501ccbe16577de221361fccf68c11bc0e67"},"previous_names":[],"tags_count":89,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jgraichen%2Facfs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jgraichen%2Facfs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jgraichen%2Facfs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jgraichen%2Facfs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jgraichen","download_url":"https://codeload.github.com/jgraichen/acfs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247622983,"owners_count":20968575,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activemodel","rails","rest","rest-client","ruby","services"],"created_at":"2024-10-13T22:16:34.228Z","updated_at":"2025-04-07T09:19:49.864Z","avatar_url":"https://github.com/jgraichen.png","language":"Ruby","readme":"# Acfs - *API client for services*\n\n[![Gem Version](https://img.shields.io/gem/v/acfs?logo=ruby)](https://rubygems.org/gems/acfs)\n[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/jgraichen/acfs/test.yml?logo=github)](https://github.com/jgraichen/acfs/actions)\n[![Coverage Status](http://img.shields.io/coveralls/jgraichen/acfs/master.svg)](https://coveralls.io/r/jgraichen/acfs)\n[![RubyDoc Documentation](http://img.shields.io/badge/rubydoc-here-blue.svg)](http://rubydoc.info/github/jgraichen/acfs/master/frames)\n\nAcfs is a library to develop API client libraries for single services within a larger service oriented application.\n\nAcfs 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.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'acfs', '~\u003e 2.1'\n```\n\nAnd then execute:\n\n```console\nbundle\n```\n\nOr install it yourself as:\n\n```console\ngem install acfs\n```\n\n## Usage\n\nFirst you need to define your service(s):\n\n```ruby\nclass UserService \u003c Acfs::Service\n  self.base_url = 'https://users.myapp.org'\n\n  # You can configure middlewares you want to use for the service here.\n  # Each service has it own middleware stack.\n  #\n  use Acfs::Middleware::JsonDecoder\n  use Acfs::Middleware::MessagePackDecoder\nend\n```\n\nThis specifies where the `UserService` is located. You can now create some models representing resources served by the `UserService`.\n\n```ruby\nclass User \u003c Acfs::Resource\n  service UserService # Associate `User` model with `UserService`.\n\n  # Define model attributes and types\n  # Types are needed to parse and generate request and response payload.\n\n  attribute :id, :uuid # Types can be classes or symbols.\n                       # Symbols will be used to load a class from `Acfs::Model::Attributes` namespace.\n                       # Eg. `:uuid` will load class `Acfs::Model::Attributes::Uuid`.\n\n  attribute :name, :string, default: 'Anonymous'\n  attribute :age, ::Acfs::Model::Attributes::Integer # Or use :integer\n\nend\n```\n\nThe service and model classes can be shipped as a gem or git submodule to be included by the frontend application(s).\n\nYou can use the model there:\n\n```ruby\n@user = User.find 14\n\n@user.loaded? #=\u003e false\n\nAcfs.run # This will run all queued request as parallel as possible.\n         # For @user the following URL will be requested:\n         # `http://users.myapp.org/users/14`\n\n@model.name # =\u003e \"...\"\n\n@users = User.all\n@users.loaded? #=\u003e false\n\nAcfs.run # Will request `http://users.myapp.org/users`\n\n@users #=\u003e [\u003cUser\u003e, ...]\n```\n\nIf you need multiple resources or dependent resources first define a \"plan\" how they can be loaded:\n\n```ruby\n@user = User.find(5) do |user|\n  # Block will be executed right after user with id 5 is loaded\n\n  # You can load additional resources also from other services\n  # Eg. fetch comments from `CommentSerivce`. The line below will\n  # load comments from `http://comments.myapp.org/comments?user=5`\n  @comments = Comment.where user: user.id\n\n  # You can load multiple resources in parallel if you have multiple\n  # ids.\n  @friends  = User.find 1, 4, 10 do |friends|\n    # This block will be executed when all friends are loaded.\n    # [ ... ]\n  end\nend\n\nAcfs.run # This call will fire all request as parallel as possible.\n         # The sequence above would look similar to:\n         #\n         # Start                Fin\n         #   |===================|       `Acfs.run`\n         #   |====|                      /users/5\n         #   |    |==============|       /comments?user=5\n         #   |    |======|               /users/1\n         #   |    |=======|              /users/4\n         #   |    |======|               /users/10\n\n# Now we can access all resources:\n\n@user.name       # =\u003e \"John\n@comments.size   # =\u003e 25\n@friends[0].name # =\u003e \"Miracolix\"\n```\n\nUse `.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.\n\n```ruby\n@user = User.find_by age: 24\n\nAcfs.run # Will request `http://users.myapp.org/users?age=24`\n\n@user # Contains the first user object returned by the index action\n```\n\nIf 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.\n\nAcfs has basic update support using `PUT` requests:\n\n```ruby\n@user = User.find 5\n@user.name = \"Bob\"\n\n@user.changed? # =\u003e true\n@user.persisted? # =\u003e false\n\n@user.save # Or .save!\n           # Will PUT new resource to service synchronously.\n\n@user.changed? # =\u003e false\n@user.persisted? # =\u003e true\n```\n\n## Singleton resources\n\nSingletons can be used in Acfs by creating a new resource which inherits from `SingletonResource`:\n\n```ruby\nclass Single \u003c Acfs::SingletonResource\n  service UserService # Associate `Single` model with `UserService`.\n\n  # Define model attributes and types as with regular resources\n\n  attribute :name, :string, default: 'Anonymous'\n  attribute :age, :integer\n\nend\n```\n\nThe following code explains the routing for singleton resource requests:\n\n```ruby\nmy_single = Single.new\nmysingle.save # sends POST request to /single\n\nmy_single = Single.find\nAcfs.run # sends GET request to /single\n\nmy_single.age = 28\nmy_single.save # sends PUT request to /single\n\nmy_single.delete # sends DELETE request to /single\n```\n\nYou also can pass parameters to the find call. They will be sent as query parameters to the index action:\n\n```ruby\nmy_single = Single.find name: 'Max'\nAcfs.run # sends GET request with param to /single?name=Max\n```\n\n## Resource Inheritance\n\nAcfs 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:\n\n```ruby\nclass Computer \u003c Acfs::Resource\n  ...\nend\n\nclass Pc \u003c Computer end\nclass Mac \u003c Computer end\n```\n\nWith the following response on `GET /computers` the collection will contain the appropriate subclass resources:\n\n```json\n[\n    { \"id\": 5, \"type\": \"Computer\"},\n    { \"id\": 6, \"type\": \"Mac\"},\n    { \"id\": 8, \"type\": \"Pc\"}\n]\n```\n\n```ruby\n@computers = Computer.all\n\nAcfs.run\n\n@computer[0].class # =\u003e Computer\n@computer[1].class # =\u003e Mac\n@computer[2].class # =\u003e Pc\n```\n\n## Stubbing\n\nYou can stub resources in applications using an Acfs service client:\n\n```ruby\n# spec_helper.rb\n\n# This will enable stabs before each spec and clear internal state\n# after each spec.\nrequire 'acfs/rspec'\n```\n\n```ruby\nbefore do\n  @stub = Acfs::Stub.resource MyUser, :read, with: { id: 1 }, return: { id: 1, name: 'John Smith', age: 32 }\n  Acfs::Stub.resource MyUser, :read, with: { id: 2 }, raise: :not_found\n  Acfs::Stub.resource Session, :create, with: { ident: 'john@exmaple.org', password: 's3cr3t' }, return: { id: 'longhash', user: 1 }\n  Acfs::Stub.resource MyUser, :update, with: lambda { |op| op.data.include? :my_var }, raise: 400\nend\n\nit 'should find user number one' do\n  user = MyUser.find 1\n  Acfs.run\n\n  expect(user.id).to eq 1\n  expect(user.name).to eq 'John Smith'\n  expect(user.age).to eq 32\n\n  expect(@stub).to be_called\n  expect(@stub).to_not be_called 5.times\nend\n\nit 'should not find user number two' do\n  MyUser.find 3\n\n  expect { Acfs.run }.to raise_error(Acfs::ResourceNotFound)\nend\n\nit 'should allow stub resource creation' do\n  session = Session.create! ident: 'john@exmaple.org', password: 's3cr3t'\n\n  expect(session.id).to eq 'longhash'\n  expect(session.user).to eq 1\nend\n```\n\nBy default, Acfs raises an error when a non stubbed resource should be requested. You can switch of the behavior:\n\n```ruby\nbefore do\n  Acfs::Stub.allow_requests = true\nend\n\nit 'should find user number one' do\n  user = MyUser.find 1\n  Acfs.run             # Would have raised Acfs::RealRequestNotAllowedError\n                       # Will run real request to user service instead.\nend\n```\n\n## Instrumentation\n\nAcfs supports [instrumentation via active support][1] and exposes the following events:\n\n* `acfs.operation.complete(operation, response)`: Acfs operation completed\n* `acfs.runner.sync_run(operation)`: Run operation right now skipping queue.\n* `acfs.runner.enqueue(operation)`: Enqueue operation to be run later.\n* `acfs.before_run`: directly before `acfs.run`\n* `acfs.run`: Run all queued operations.\n\nRead the [official guide][2] on how to subscribe to these events.\n\n[1]: http://guides.rubyonrails.org/active_support_instrumentation.html\n[2]: http://guides.rubyonrails.org/active_support_instrumentation.html#subscribing-to-an-event\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Add specs for your feature\n4. Implement your feature\n5. Commit your changes (`git commit -am 'Add some feature'`)\n6. Push to the branch (`git push origin my-new-feature`)\n7. Create new Pull Request\n\n## License\n\n[MIT License](LICENSE)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjgraichen%2Facfs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjgraichen%2Facfs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjgraichen%2Facfs/lists"}