{"id":26733448,"url":"https://github.com/amcaplan/simple_jsonapi_client","last_synced_at":"2025-04-14T12:41:31.841Z","repository":{"id":24259104,"uuid":"100206466","full_name":"amcaplan/simple_jsonapi_client","owner":"amcaplan","description":"An opinionated framework for building JSONAPI clients","archived":false,"fork":false,"pushed_at":"2023-01-19T04:50:49.000Z","size":122,"stargazers_count":10,"open_issues_count":10,"forks_count":9,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-23T19:49:16.196Z","etag":null,"topics":[],"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/amcaplan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-08-13T21:45:22.000Z","updated_at":"2025-02-06T04:47:22.000Z","dependencies_parsed_at":"2023-02-10T20:31:20.199Z","dependency_job_id":null,"html_url":"https://github.com/amcaplan/simple_jsonapi_client","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amcaplan%2Fsimple_jsonapi_client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amcaplan%2Fsimple_jsonapi_client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amcaplan%2Fsimple_jsonapi_client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amcaplan%2Fsimple_jsonapi_client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/amcaplan","download_url":"https://codeload.github.com/amcaplan/simple_jsonapi_client/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248883080,"owners_count":21177147,"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":[],"created_at":"2025-03-28T01:49:26.544Z","updated_at":"2025-04-14T12:41:31.820Z","avatar_url":"https://github.com/amcaplan.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.org/amcaplan/simple_jsonapi_client.svg?branch=master)](https://travis-ci.org/amcaplan/simple_jsonapi_client)\n[![Gem Version](https://badge.fury.io/rb/simple_jsonapi_client.svg)](https://badge.fury.io/rb/simple_jsonapi_client)\n\n# What is `SimpleJSONAPIClient`?\n\n`SimpleJSONAPIClient` is a framework for building Ruby clients for JSONAPI-compliant services.  \n\n# How do I use `SimpleJSONAPIClient`?\n\n## Setup\n\nFirst create models inheriting from `SimpleJSONAPIClient::Base`, and specifying a few details.\n\n* `COLLECTION_URL` - the path to fetch the resource collection\n* `INDIVIDUAL_URL` - the path to fetch an individual resource\n* `TYPE` - the JSONAPI resource type to use when creating a new resource\n* `attributes` - the names of attributes which can be found on the resource\n* relationships - `has_one` and `has_many` define relationships, and take these arguments:\n  * relationship name (e.g., the `:goats` in `has_many :goats`)\n  * `class_name` to use when instantiating related objects\n\nThey should look like this:\n\n```ruby\nclass Post \u003c SimpleJSONAPIClient::Base\n  COLLECTION_URL = '/posts'\n  INDIVIDUAL_URL = '/posts/%{id}'\n  TYPE = 'posts'\n\n  attributes :title, :text\n  meta :copyright\n\n  has_one :author, class_name: 'Author'\n  has_many :comments, class_name: 'Comment'\nend\n\nclass Author \u003c SimpleJSONAPIClient::Base\n  COLLECTION_URL = '/authors'\n  INDIVIDUAL_URL = '/authors/%{id}'\n  TYPE = 'authors'\n\n  attributes :name\n\n  has_many :posts, class_name: 'Post'\n  has_many :comments, class_name: 'Comment'\nend\n\nclass Comment \u003c SimpleJSONAPIClient::Base\n  COLLECTION_URL = '/comments'\n  INDIVIDUAL_URL = '/comments/%{id}'\n  TYPE = 'comments'\n\n  attributes :text\n\n  has_one :post, class_name: 'Post'\n  has_one :author, class_name: 'Author'\nend\n```\n\nIf you have behavior you'd like to share across models, you may want to first create an abstract class inheriting from `SimpleJSONAPIClient::Base` and then have all your models inherit from that.\n\nNext, create a [`Faraday`](https://github.com/lostisland/faraday) connection to handle the domain, authorization strategy, and anything else you need (making sure to include [JSON parsing middleware](https://github.com/lostisland/faraday_middleware/wiki/Parsing-responses)):\n\n```ruby\ndef connection(token)\n  default_headers = {\n    'Accept' =\u003e 'application/vnd.api+json',\n    'Content-Type' =\u003e 'application/vnd.api+json',\n    'Authorization' =\u003e \"token=#{token}\"\n  }\n\n  @connection ||= Faraday.new(url: 'https://example.com', headers: default_headers) do |connection|\n    connection.request :json\n    connection.response :json, :content_type =\u003e /\\bjson$/ # use middleware to parse JSON when response Content-Type is json\n    connection.adapter :net_http\n  end\nend\n```\n\nNow you can start making requests!\n\n## Fetching\n\n### Laziness and `SimpleJSONAPIClient`\n\n```ruby\nPost.fetch_all(connection: connection)\n=\u003e #\u003cEnumerator: #\u003cEnumerator::Generator:0x00562894acd420\u003e:each\u003e\n```\n\nWhat's going on?  `SimpleJSONAPIClient` tries to be as lazy as possible while still being convenient.  So if you actually want to fetch everything, you'll be able to call `Array` methods and it will fetch the resource, paginating through all the results.  If it's an endpoint with thousands of pages, you can use `Enumerator` methods like `#each` and it'll paginate through the results, fetching the next page when it runs out of objects.\n\nLet's call `#to_a` to see a bit more detail.\n\n```ruby\nposts = Post.fetch_all(connection: connection).to_a\n=\u003e [#\u003cPost id=1 title=\"A Very Proper Post Title\" text=\"I am absolutely incensed about something.\" author=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments\u003e\u003e,\n #\u003cPost id=2 title=\"The System is Down\" text=\"The Cheat\" author=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/2/author\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/2/comments\u003e\u003e]\n```\n\nAttributes are loaded immediately, but relationships are lazily instantiated.  So if we dig a little bit further:\n\n```ruby\nposts.first.author\n=\u003e #\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author\u003e\n```\n\nNope, still lazy!  However, once we start fetching details about the author, `SimpleJSONAPIClient` knows a request has to be made, and fills in the details:\n\n```ruby\nposts.first.author.id\n=\u003e \"3\"\n\nposts.first.author\n=\u003e #\u003cAuthor id=3 name=\"Filbert\" posts=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Post url=http://jsonapi_app:3000/authors/3/posts\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/authors/3/comments\u003e\u003e\n```\n\nWe can read more easily by calling `#as_json`:\n\n```ruby\nposts.first.author.as_json\n=\u003e {\n  :data =\u003e {\n    :type =\u003e \"authors\",\n    :attributes =\u003e { :name =\u003e \"Filbert\" },\n    :relationships =\u003e {\n      :posts =\u003e {\n        :data =\u003e [{ :type =\u003e \"posts\", :id =\u003e \"1\" }]\n      },\n      :comments =\u003e { :data =\u003e [] }\n    }\n  }\n}\n```\n\n### More About Fetching Capabilities\n\nYou can also explicitly fetch a single item:\n\n```ruby\npost = Post.fetch(connection: connection, url_opts: { id: 1 })\n=\u003e #\u003cPost id=1 title=\"A Very Proper Post Title\" text=\"I am absolutely incensed about something.\" author=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments\u003e\u003e\n```\n\n`url_opts`, in all cases where you see them, are passed to the template Strings for `INDIVIDUAL_URL` and `COLLECTION_URL` in the model.\n\nYou've already seen that `id` and `relationships` are available; `attributes` and `meta` information also become methods on the object:\n\n```ruby\npost = Post.fetch(connection: connection, url_opts: { id: 1 })\npost.title\n=\u003e \"A Very Proper Post Title\"\npost.text\n=\u003e \"I am absolutely incensed about something.\"\npost.copyright\n=\u003e \"Copyright 2017\"\n```\n\nYou can also use JSONAPI includes to reduce the number of requests that are necessary:\n\n```ruby\npost = JSONAPIAppClient::Post.fetch(connection: connection, url_opts: { id: 1 }, includes: ['author', 'comments.author'])\npost.author # will not make another web request\npost.comments.first.author # will not make another web request\n```\n\n`SimpleJSONAPIClient` will check the included records for related records you access through the returned model.\n\nAnd finally, you can use JSONAPI-style filtering and pagination as well:\n\n```ruby\n# Given 3 authors...\nJSONAPIAppClient::Author.fetch_all(connection: connection).to_a\n=\u003e [#\u003cJSONAPIAppClient::Author id=1 name=\"Filbert\" posts=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/1/posts\u003e comments=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/1/comments\u003e\u003e,\n #\u003cJSONAPIAppClient::Author id=2 name=\"Dilbert\" posts=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/2/posts\u003e comments=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/2/comments\u003e\u003e,\n #\u003cJSONAPIAppClient::Author id=3 name=\"Wilbert\" posts=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/3/posts\u003e comments=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/3/comments\u003e\u003e]\n\n# Filtering by name\nJSONAPIAppClient::Author.fetch_all(connection: connection, filter_opts: { name: 'Filbert' }).to_a\n=\u003e [#\u003cJSONAPIAppClient::Author id=1 name=\"Filbert\" posts=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/1/posts\u003e comments=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/1/comments\u003e\u003e]\n\n# Just grabbing the last page\nJSONAPIAppClient::Author.fetch_all(connection: connection, page_opts: { size: 1, number: 3 }).to_a\n=\u003e [#\u003cJSONAPIAppClient::Author id=3 name=\"Wilbert\" posts=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/3/posts\u003e comments=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/3/comments\u003e\u003e]\n\n# You can adjust the pagination strategy and SimpleJSONAPIClient will follow it, but return the same results\nJSONAPIAppClient::Author.fetch_all(connection: connection, page_opts: { size: 1, number: 1 }).to_a\n=\u003e [#\u003cJSONAPIAppClient::Author id=1 name=\"Filbert\" posts=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/1/posts\u003e comments=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/1/comments\u003e\u003e,\n #\u003cJSONAPIAppClient::Author id=2 name=\"Dilbert\" posts=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/2/posts\u003e comments=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/2/comments\u003e\u003e,\n #\u003cJSONAPIAppClient::Author id=3 name=\"Wilbert\" posts=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/3/posts\u003e comments=#\u003cSimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/3/comments\u003e\u003e]\n```\n\n## Creating\n\nCreating records is available from the model class:\n\n```ruby\npost = Post.fetch(url_opts: { id: 1 }, connection: connection)\n=\u003e #\u003cPost id=1 title=\"A Very Proper Post Title\" text=\"I am absolutely incensed about something.\" author=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments\u003e\u003e\nauthor = Author.fetch(url_opts: { id: 1}, connection: connection)\n=\u003e #\u003cAuthor id=1 name=\"Filbert\" posts=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Post url=http://jsonapi_app:3000/authors/1/posts\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/authors/1/comments\u003e\u003e\n\nComment.create(connection: connection, text: 'I adore your article!', post: post, author: author)\n=\u003e #\u003cComment id=19 text=\"I adore your article!\" post=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Client::Post url=http://jsonapi_app:3000/comments/19/post\u003e author=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/comments/19/author\u003e\u003e\n```\n\nThe created record is returned; if creation fails, a `SimpleJSONAPIClient::Errors::ApiError` is raised.\n\n## Updating\n\nIf you want to update a record, you can do it from the model itself:\n\n```ruby\npost = Post.fetch(url_opts: { id: 1 }, connection: connection)\n=\u003e #\u003cPost id=1 title=\"A Very Proper Post Title\" text=\"I am absolutely incensed about something.\" author=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments\u003e\u003e\n[2] pry(main)\u003e post.update(attributes: { text: 'foo' })\n=\u003e #\u003cPost id=1 title=\"A Very Proper Post Title\" text=\"foo\" author=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments\u003e\u003e\n```\n\nIf you have the ID of the record handy, you update straight from the model class without fetching the record first:\n\n```ruby\nPost.update(id: 1, url_opts: { id: 1 }, connection: connection, text: 'foo')\n=\u003e #\u003cPost id=1 title=\"A Very Proper Post Title\" text=\"foo\" author=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments\u003e\u003e\n```\n\n## Deleting\n\nYou can delete a record from the model itself:\n\n```ruby\npost = Post.fetch(url_opts: { id: 1 }, connection: connection)\n=\u003e #\u003cPost id=1 title=\"A Very Proper Post Title\" text=\"I am absolutely incensed about something.\" author=#\u003cSimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author\u003e comments=#\u003cSimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments\u003e\u003e\n\npost.delete\n=\u003e true\nPost.fetch(url_opts: { id: 1 }, connection: connection)\n=\u003e nil\n```\n\nor from the class, if you have the ID:\n\n```ruby\nPost.delete(url_opts: { id: 1 }, connection: connection)\n=\u003e true\nPost.fetch(url_opts: { id: 1 }, connection: connection)\n=\u003e nil\n```\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'simple_jsonapi_client'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install simple_jsonapi_client\n\n## Development\n\nYou must have [Docker](https://docker.com) and [Docker Compose](https://docs.docker.com/compose/) installed to run the tests and use the built-in development utilities.\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment, and `bin/rails` to interact with the Rails app in `spec/jsonapi_app` that is provided for local development and testing.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/amcaplan/simple_jsonapi_client. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the SimpleJsonapiClient project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/amcaplan/simple_jsonapi_client/blob/master/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famcaplan%2Fsimple_jsonapi_client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Famcaplan%2Fsimple_jsonapi_client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famcaplan%2Fsimple_jsonapi_client/lists"}