{"id":13493349,"url":"https://github.com/mwpastore/sinja","last_synced_at":"2025-03-24T03:30:58.301Z","repository":{"id":46260908,"uuid":"70150770","full_name":"mwpastore/sinja","owner":"mwpastore","description":"RESTful, {json:api}-compliant web services in Sinatra","archived":false,"fork":false,"pushed_at":"2021-10-02T11:54:34.000Z","size":327,"stargazers_count":87,"open_issues_count":9,"forks_count":8,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-03-19T02:38:57.150Z","etag":null,"topics":["ember-data","json-api","ruby-framework","ruby-gem","sinatra","web-framework"],"latest_commit_sha":null,"homepage":"http://sinja-rb.org","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/mwpastore.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-10-06T12:13:44.000Z","updated_at":"2024-09-10T08:22:10.000Z","dependencies_parsed_at":"2022-09-11T03:10:47.824Z","dependency_job_id":null,"html_url":"https://github.com/mwpastore/sinja","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mwpastore%2Fsinja","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mwpastore%2Fsinja/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mwpastore%2Fsinja/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mwpastore%2Fsinja/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mwpastore","download_url":"https://codeload.github.com/mwpastore/sinja/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245204461,"owners_count":20577352,"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":["ember-data","json-api","ruby-framework","ruby-gem","sinatra","web-framework"],"created_at":"2024-07-31T19:01:14.389Z","updated_at":"2025-03-24T03:30:57.949Z","avatar_url":"https://github.com/mwpastore.png","language":"Ruby","readme":"# Sinja (Sinatra::JSONAPI)\n\n\u003c!--\n  Title: Sinja\n  Description: RESTful, {json:api}-compliant web services in Sinatra\n  Author: Mike Pastore\n  Keywords: JSON, API, JSONAPI, JSON:API, {json:api}, Ruby, Sinatra, JSONAPI::Serializers, jsonapi-serializers\n  --\u003e\n\n[![Gem Version](https://badge.fury.io/rb/sinja.svg)](https://badge.fury.io/rb/sinja)\n[![Dependency Status](https://gemnasium.com/badges/github.com/mwpastore/sinja.svg)](https://gemnasium.com/github.com/mwpastore/sinja)\n[![Build Status](https://travis-ci.org/mwpastore/sinja.svg?branch=master)](https://travis-ci.org/mwpastore/sinja)\n[![{json:api} version](https://img.shields.io/badge/%7Bjson%3Aapi%7D%20version-1.0-lightgrey.svg)][7]\n[![Chat in #sinja-rb on Gitter](https://badges.gitter.im/sinja-rb/Lobby.svg)](https://gitter.im/sinja-rb/Lobby)\n\nSinja is a [Sinatra 2.0][1] [extension][10] for quickly building [RESTful][11],\n[{json:api}][2]-compliant web services, leveraging the excellent\n[JSONAPI::Serializers][3] gem for payload serialization. It enhances Sinatra's\nDSL to enable resource-, relationship-, and role-centric API development, and\nit configures Sinatra with the proper settings, MIME-types, filters,\nconditions, and error-handling.\n\nThere are [many][31] parsing (deserializing), rendering (serializing), and\nother \"JSON API\" libraries available for Ruby, but relatively few that attempt\nto correctly implement the entire {json:api} server specification, including\nrouting, request header and query parameter checking, and relationship\nside-loading.  Sinja lets you focus on the business logic of your applications\nwithout worrying about the specification, and without pulling in a heavy\nframework like [Rails][16]. It's lightweight, ORM-agnostic, and\n[Ember.js][32]-friendly!\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n\n- [Synopsis](#synopsis)\n- [Installation](#installation)\n- [Ol' Blue Eyes is Back](#ol-blue-eyes-is-back)\n- [Basic Usage](#basic-usage)\n  - [Configuration](#configuration)\n    - [Sinatra](#sinatra)\n    - [Sinja](#sinja)\n  - [Resources](#resources)\n  - [Resource Locators](#resource-locators)\n  - [Action Helpers](#action-helpers)\n    - [`resource`](#resource)\n    - [`has_one`](#has_one)\n    - [`has_many`](#has_many)\n- [Advanced Usage](#advanced-usage)\n  - [Action Helper Hooks \u0026 Utilities](#action-helper-hooks--utilities)\n  - [Authorization](#authorization)\n    - [`default_roles` configurables](#default_roles-configurables)\n    - [`:roles` Action Helper option](#roles-action-helper-option)\n    - [`role` helper](#role-helper)\n  - [Query Parameters](#query-parameters)\n  - [Working with Collections](#working-with-collections)\n    - [Filtering](#filtering)\n    - [Sorting](#sorting)\n    - [Paging](#paging)\n    - [Finalizing](#finalizing)\n  - [Conflicts](#conflicts)\n  - [Validations](#validations)\n  - [Missing Records](#missing-records)\n  - [Transactions](#transactions)\n  - [Side-Unloading Related Resources](#side-unloading-related-resources)\n  - [Side-Loading Relationships](#side-loading-relationships)\n    - [Deferring Relationships](#deferring-relationships)\n    - [Avoiding Null Foreign Keys](#avoiding-null-foreign-keys)\n  - [Coalesced Find Requests](#coalesced-find-requests)\n  - [Patchless Clients](#patchless-clients)\n- [Extensions](#extensions)\n  - [Sequel](#sequel)\n- [Application Concerns](#application-concerns)\n  - [Performance](#performance)\n  - [Public APIs](#public-apis)\n    - [Commonly Used](#commonly-used)\n    - [Less-Commonly Used](#less-commonly-used)\n  - [Sinja or Sinatra::JSONAPI](#sinja-or-sinatrajsonapi)\n  - [Code Organization](#code-organization)\n  - [Testing](#testing)\n- [Comparison with JSONAPI::Resources](#comparison-with-jsonapiresources)\n- [Development](#development)\n- [Contributing](#contributing)\n- [License](#license)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Synopsis\n\n```ruby\nrequire 'sinatra/jsonapi'\n\nresource :posts do\n  show do |id|\n    Post[id.to_i]\n  end\n\n  index do\n    Post.all\n  end\n\n  create do |attr|\n    post = Post.create(attr)\n    next post.id, post\n  end\nend\n\nfreeze_jsonapi\n```\n\nAssuming the presence of a `Post` model and serializer, running the above\n\"classic\"-style Sinatra application would enable the following endpoints (with\nall other {json:api} endpoints returning 404 or 405):\n\n* `GET /posts/\u003cid\u003e`\n* `GET /posts`\n* `POST /posts`\n\nThe resource locator and other action helpers, documented below, enable other\nendpoints.\n\nOf course, \"modular\"-style Sinatra aplications (subclassing Sinatra::Base)\nrequire you to register the extension:\n\n```ruby\nrequire 'sinatra/base'\nrequire 'sinatra/jsonapi'\n\nclass App \u003c Sinatra::Base\n  register Sinatra::JSONAPI\n\n  resource :posts do\n    # ..\n  end\n\n  freeze_jsonapi\nend\n```\n\nPlease see the [demo-app](/demo-app) for a more complete example.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'sinja'\n```\n\nAnd then execute:\n\n```sh\n$ bundle\n```\n\nOr install it yourself as:\n\n```sh\n$ gem install sinja\n```\n\nSinja is not compatible with Sinatra 1.x due to its limitations with nested\nregexp-style namespaces and routes.\n\n## Ol' Blue Eyes is Back\n\nThe \"power\" so to speak of implementing this functionality as a Sinatra\nextension is that all of Sinatra's usual features are available within your\nresource definitions. Action helper blocks get compiled into Sinatra helpers,\nand the `resource`, `has_one`, and `has_many` keywords build\n[Sinatra::Namespace][21] blocks. You can manage caching directives, set\nheaders, and even `halt` (or `not_found`, although such cases are usually\nhandled transparently by returning `nil` values or empty collections from\naction helpers) as appropriate.\n\n```ruby\nclass App \u003c Sinatra::Base\n  register Sinatra::JSONAPI\n\n  # \u003c- This is a Sinatra::Base class definition. (Duh.)\n\n  resource :books do\n    # \u003c- This is a Sinatra::Namespace block.\n\n    show do |id|\n      # \u003c- This is a \"special\" Sinatra helper, scoped to the resource namespace.\n    end\n\n    has_one :author do\n      # \u003c- This is a Sinatra::Namespace block, nested under the resource namespace.\n\n      pluck do\n        # \u003c- This is a \"special\" Sinatra helper, scoped to the nested namespace.\n      end\n    end\n  end\n\n  freeze_jsonapi\nend\n```\n\nThis lets you easily pepper in all the syntactic sugar you might expect to see\nin a typical Sinatra application:\n\n```ruby\nclass App \u003c Sinatra::Base\n  register Sinatra::JSONAPI\n\n  configure :development do\n    enable :logging\n  end\n\n  helpers do\n    def foo; true end\n  end\n\n  before do\n    cache_control :public, max_age: 3_600\n  end\n\n  # define a custom /status route\n  get('/status', provides: :json) { 'OK' }\n\n  resource :books do\n    helpers do\n      def find(id)\n        Book[id.to_i]\n      end\n    end\n\n    show do\n      headers 'X-ISBN'=\u003eresource.isbn\n      last_modified resource.updated_at\n      next resource, include: ['author']\n    end\n\n    has_one :author do\n      helpers do\n        def bar; false end\n      end\n\n      before do\n        cache_control :private\n        halt 403 unless foo || bar\n      end\n\n      pluck do\n        etag resource.author.hash, :weak\n        resource.author\n      end\n    end\n\n    # define a custom /books/top10 route\n    get '/top10' do\n      halt 403 unless can?(:index) # restrict access to those with index rights\n\n      serialize_models Book.where{}.reverse_order(:recent_sales).limit(10).all\n    end\n  end\n\n  freeze_jsonapi\nend\n```\n\n## Basic Usage\n\nYou'll need a database schema and models (using the engine and ORM of your\nchoice) and [serializers][3] to get started. Create a new Sinatra application\n(classic or modular) to hold all your {json:api} controllers and (if\nsubclassing Sinatra::Base) register this extension. Instead of defining routes\nwith `get`, `post`, etc. as you normally would, define `resource` blocks with\naction helpers and `has_one` and `has_many` relationship blocks (with their own\naction helpers). Sinja will draw and enable the appropriate routes based on the\ndefined resources, relationships, and action helpers. Other routes will return\nthe appropriate HTTP statuses: 403, 404, or 405.\n\n### Configuration\n\n#### Sinatra\n\nRegistering this extension has a number of application-wide implications,\ndetailed below. If you have any non-{json:api} routes, you may want to keep them\nin a separate application and incorporate them as middleware or mount them\nelsewhere (e.g. with [Rack::URLMap][4]), or host them as a completely separate\nweb service. It may not be feasible to have custom routes that don't conform to\nthese settings.\n\n* Registers [Sinatra::Namespace][21] and [Mustermann][25]\n* Disables [Rack::Protection][6] (can be reenabled with `enable :protection` or\n  by manually `use`-ing the Rack::Protection middleware)\n* Disables static file routes (can be reenabled with `enable :static`; be sure\n  to reenable Rack::Protection::PathTraversal as well)\n* Disables \"classy\" error pages (in favor of \"classy\" {json:api} error documents)\n* Adds an `:api_json` MIME-type (`application/vnd.api+json`)\n* Enforces strict checking of the `Accept` and `Content-Type` request headers\n* Sets the `Content-Type` response header to `:api_json` (can be overriden with\n  the `content_type` helper)\n* Normalizes and strictly enforces query parameters to reflect the features\n  supported by {json:api}\n* Formats all errors to the proper {json:api} structure\n* Serializes all response bodies (including errors) to JSON\n* Modifies `halt` and `not_found` to raise exceptions instead of just setting\n  the status code and body of the response\n\n#### Sinja\n\nSinja provides its own configuration store that can be accessed through the\n`configure_jsonapi` block. The following configurables are available (with\ntheir defaults shown):\n\n```ruby\nconfigure_jsonapi do |c|\n  #c.conflict_exceptions = [] # see \"Conflicts\" below\n\n  #c.not_found_exceptions = [] # see \"Missing Records\" below\n\n  # see \"Validations\" below\n  #c.validation_exceptions = []\n  #c.validation_formatter = -\u003e{ [] }\n\n  # see \"Authorization\" below\n  #c.default_roles = {}\n  #c.default_has_one_roles = {}\n  #c.default_has_many_roles = {}\n\n  # You can't set this directly; see \"Query Parameters\" below\n  #c.query_params = {\n  #  :include=\u003eArray, :fields=\u003eHash, :filter=\u003eHash, :page=\u003eHash, :sort=\u003eArray\n  #}\n\n  #c.page_using = {} # see \"Paging\" below\n\n  # Set the error logger used by Sinja (set to `nil' to disable)\n  #c.error_logger = -\u003e(error_hash) { logger.error('sinja') { error_hash } }\n\n  # A hash of options to pass to JSONAPI::Serializer.serialize\n  #c.serializer_opts = {}\n\n  # JSON methods to use when serializing response bodies and errors\n  #c.json_generator = development? ? :pretty_generate : :generate\n  #c.json_error_generator = development? ? :pretty_generate : :generate\nend\n```\n\nThe above structures are mutable (e.g. you can do `c.conflict_exceptions \u003c\u003c\nFooError` and `c.serializer_opts[:meta] = { foo: 'bar' }`) until you call\n`freeze_jsonapi` to freeze the configuration store. **You should always freeze\nthe store after Sinja is configured and all your resources are defined.**\n\n### Resources\n\nResources declared with the `resource` keyword (and relationships declared with\nthe `has_many` and `has_one` keywords) are dasherized and pluralized to match\nthe \"type\" property of JSONAPI::Serializers. For example, `resource :foo_bar`\nwould instruct Sinja to draw the appropriate routes under `/foo-bars`. Your\nserializer type(s) should always match your resource (and relationship) names;\nsee the relevant [documentation][33] for more information.\n\nThe primary key portion of the route is extracted using a regular expression,\n`\\d+` by default. To use a different pattern, pass the `:pkre` resource route\noption:\n\n```ruby\nresource :foo_bar, pkre: /\\d+-\\d+/ do\n  helpers do\n    def find(id)\n      # Look up a FooBar with a composite primary key of two integers.\n      FooBar[id.split('-', 2).map!(\u0026:to_i)]\n    end\n  end\n\n  # ..\nend\n```\n\nThis helps Sinja (and Sinatra) disambiguate between standard {json:api} routes\nused to fetch resources (e.g. `GET /foo-bars/1`) and similarly-structured\ncustom routes (e.g. `GET /foo-bars/recent`).\n\n### Resource Locators\n\nMuch of Sinja's advanced functionality (e.g. updating and destroying resources,\nrelationship routes) is dependent upon its ability to locate the corresponding\nresource for a request. To enable these features, define an ordinary helper\nmethod named `find` in your resource definition that takes a single ID argument\nand returns the corresponding object. Once defined, a `resource` object will be\nmade available in any action helpers that operate on a single (parent)\nresource.\n\n```ruby\nresource :posts do\n  helpers do\n    def find(id)\n      Post[id.to_i]\n    end\n  end\n\n  show do\n    next resource, include: 'comments'\n  end\nend\n```\n\n* What's the difference between `find` and `show`?\n\n  You can think of it as the difference between a Model and a View: `find`\n  retrieves the record, `show` presents it.\n\n* Why separate the two? Why not use `show` as the resource locator?\n\n  For a variety of reasons, but primarily because the access rights for viewing\n  a resource are not always the same as those for updating and/or destroying a\n  resource, and vice-versa. For example, a user may be able to delete a\n  resource or subtract a relationship link without being able to see the\n  resource or its relationship linkage.\n\n* How do I control access to the resource locator?\n\n  You don't. Instead, control access to the action helpers that use it: `show`,\n  `update`, `destroy`, and all of the relationship action helpers such as\n  `pluck` and `fetch`.\n\n* What happens if I define an action helper that requires a resource locator,\n  but don't define a resource locator?\n\n  Sinja will act as if you had not defined the action helper.\n\nAs a bit of syntactic sugar, if you define a `find` helper and subsequently\ncall `show` without a block, Sinja will generate a `show` action helper that\nsimply returns `resource`.\n\n### Action Helpers\n\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n  \u003cth\u003e\u003ca href=\"#resource\"\u003e\u003ccode\u003eresource\u003c/code\u003e\u003c/a\u003e\u003c/th\u003e\n  \u003cth\u003e\u003ca href=\"#has_one\"\u003e\u003ccode\u003ehas_one\u003c/code\u003e\u003c/a\u003e\u003c/th\u003e\n  \u003cth\u003e\u003ca href=\"#has_many\"\u003e\u003ccode\u003ehas_many\u003c/code\u003e\u003c/a\u003e\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr valign=\"top\"\u003e\n  \u003ctd\u003e\u003cul\u003e\n    \u003cli\u003e\u003ca href=\"#index---array\"\u003e\u003ccode\u003eindex\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ccode\u003eshow\u003c/code\u003e \u003ca href=\"#show---object\"\u003ew/ resource locator\u003c/a\u003e or \u003ca href=\"#show-id---object\"\u003ew/o\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#show_many-ids---array\"\u003e\u003ccode\u003eshow_many\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ccode\u003ecreate\u003c/code\u003e \u003ca href=\"#create-attr-id---id-object\"\u003ew/ client-generated IDs\u003c/a\u003e or \u003ca href=\"#create-attr---id-object\"\u003ew/o\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#update-attr---object\"\u003e\u003ccode\u003eupdate\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#destroy-\"\u003e\u003ccode\u003edestroy\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003cul\u003e\n    \u003cli\u003e\u003ca href=\"#pluck---object\"\u003e\u003ccode\u003epluck\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#prune---trueclass\"\u003e\u003ccode\u003eprune\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#graft-rio---trueclass\"\u003e\u003ccode\u003egraft\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003cul\u003e\n    \u003cli\u003e\u003ca href=\"#fetch---array\"\u003e\u003ccode\u003efetch\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#clear---trueclass\"\u003e\u003ccode\u003eclear\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#replace-rios---trueclass\"\u003e\u003ccode\u003ereplace\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#merge-rios---trueclass\"\u003e\u003ccode\u003emerge\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#subtract-rios---trueclass\"\u003e\u003ccode\u003esubtract\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\nAction helpers should be defined within the appropriate block contexts\n(`resource`, `has_one`, or `has_many`) using the given keywords and arguments\nbelow. Implicitly return the expected values as described below (as an array if\nnecessary) or use the `next` keyword (instead of `return` or `break`) to exit\nthe action helper. Return values with a question mark below may be omitted\nentirely. Any helper may additionally return an options hash to pass along to\nJSONAPI::Serializer.serialize (which will be merged into the global\n`serializer_opts` described above). The `:include` (see \"Side-Unloading Related\nResources\" below) and `:fields` (for sparse fieldsets) query parameters are\nautomatically passed through to JSONAPI::Serializers.\n\nAll arguments to action helpers are \"tainted\" and should be treated as\npotentially dangerous: IDs, attribute hashes, and (arrays of) [resource\nidentifier object][22] hashes.\n\nFinally, some routes will automatically invoke the resource locator on your\nbehalf and make the selected resource available to the corresponding action\nhelper(s) as `resource`. For example, the `PATCH /\u003cname\u003e/:id` route looks up\nthe resource with that ID using the `find` resource locator and makes it\navailable to the `update` action helper as `resource`.\n\n#### `resource`\n\n##### `index {..}` =\u003e Array\n\nReturn an array of zero or more objects to serialize on the response.\n\n##### `show {|id| ..}` =\u003e Object\n\nWithout a resource locator: Take an ID and return the corresponding object (or\n`nil` if not found) to serialize on the response. (Note that only one or the\nother `show` action helpers is allowed in any given resource block.)\n\n##### `show {..}` =\u003e Object\n\nWith a resource locator: Return the `resource` object to serialize on the\nresponse. (Note that only one or the other `show` action helpers is allowed in\nany given resource block.)\n\n##### `show_many {|ids| ..}` =\u003e Array\n\nTake an array of IDs and return an equally-lengthed array of objects to\nserialize on the response. See \"Coalesced Find Requests\" below.\n\n##### `create {|attr| ..}` =\u003e id, Object\n\nWithout client-generated IDs: Take a hash of (dedasherized) attributes, create\na new resource, and return the server-generated ID and the created resource.\n(Note that only one or the other `create` action helpers is allowed in any\ngiven resource block.)\n\n##### `create {|attr, id| ..}` =\u003e id, Object?\n\nWith client-generated IDs: Take a hash of (dedasherized) attributes and a\nclient-generated ID, create a new resource, and return the ID and optionally\nthe created resource. (Note that only one or the other `create` action helpers\nis allowed in any given resource block.)\n\n##### `update {|attr| ..}` =\u003e Object?\n\nTake a hash of (dedasherized) attributes, update `resource`, and optionally\nreturn the updated resource. **Requires a resource locator.**\n\n##### `destroy {..}`\n\nDelete or destroy `resource`. **Requires a resource locator.**\n\n#### `has_one`\n\n**Requires a resource locator.**\n\n##### `pluck {..}` =\u003e Object\n\nReturn the related object vis-\u0026agrave;-vis `resource` to serialize on the\nresponse.\n\n##### `prune {..}` =\u003e TrueClass?\n\nRemove the relationship from `resource`. To serialize the updated linkage on\nthe response, refresh or reload `resource` (if necessary) and return a truthy\nvalue.\n\nFor example, using [Sequel][13]:\n\n```ruby\nhas_one :qux do\n  prune do\n    resource.qux = nil\n    resource.save_changes # will return truthy if the relationship was present\n  end\nend\n```\n\n##### `graft {|rio| ..}` =\u003e TrueClass?\n\nTake a [resource identifier object][22] hash and update the relationship on\n`resource`. To serialize the updated linkage on the response, refresh or reload\n`resource` (if necessary) and return a truthy value.\n\n#### `has_many`\n\n**Requires a resource locator.**\n\n##### `fetch {..}` =\u003e Array\n\nReturn an array of related objects vis-\u0026agrave;-vis `resource` to serialize on\nthe response.\n\n##### `clear {..}` =\u003e TrueClass?\n\nRemove all relationships from `resource`. To serialize the updated linkage on\nthe response, refresh or reload `resource` (if necessary) and return a truthy\nvalue.\n\nFor example, using [Sequel][13]:\n\n```ruby\nhas_many :bars do\n  clear do\n    resource.remove_all_bars # will return truthy if relationships were present\n  end\nend\n```\n\n##### `replace {|rios| ..}` =\u003e TrueClass?\n\nTake an array of [resource identifier object][22] hashes and update\n(add/remove) the relationships on `resource`. To serialize the updated linkage\non the response, refresh or reload `resource` (if necessary) and return a\ntruthy value.\n\nIn principle, `replace` should delete all members of the existing collection\nand insert all members of a new collection, but in practice\u0026mdash;for\nperformance reasons, especially with large collections and/or complex\nconstraints\u0026mdash;it may be prudent to simply apply a delta.\n\n##### `merge {|rios| ..}` =\u003e TrueClass?\n\nTake an array of [resource identifier object][22] hashes and update (add unless\nalready present) the relationships on `resource`. To serialize the updated\nlinkage on the response, refresh or reload `resource` (if necessary) and return\na truthy value.\n\n##### `subtract {|rios| ..}` =\u003e TrueClass?\n\nTake an array of [resource identifier object][22] hashes and update (remove\nunless already missing) the relationships on `resource`. To serialize the\nupdated linkage on the response, refresh or reload `resource` (if necessary)\nand return a truthy value.\n\n## Advanced Usage\n\n### Action Helper Hooks \u0026 Utilities\n\nYou may remove a previously-registered action helper with `remove_\u003caction\u003e`:\n\n```ruby\nresource :foos do\n  index do\n    # ..\n  end\n\n  remove_index\nend\n```\n\nYou may invoke an action helper keyword without a block to modify the options\n(i.e. roles and sideloading) of a previously-registered action helper while\npreseving the existing behavior:\n\n```ruby\nresource :bars do\n  show do |id|\n    # ..\n  end\n\n  show(roles: :admin) # restrict the above action helper to the `admin' role\nend\n```\n\nYou may define an ordinary helper method named `before_\u003caction\u003e` (in the\nresource or relationship scope or any parent scopes) that takes the same\narguments as the corresponding block:\n\n```ruby\nhelpers do\n  def before_create(attr)\n    halt 400 unless valid_key?(attr.delete(:special_key))\n  end\nend\n\nresource :quxes do\n  create do |attr|\n    attr.key?(:special_key) # =\u003e false\n  end\nend\n```\n\nAny changes made to attribute hashes or (arrays of) resource identifier object\nhashes in a `before` hook will be persisted to the action helper.\n\n### Authorization\n\nSinja provides a simple role-based authorization scheme to restrict access to\nroutes based on the action helpers they invoke. For example, you might say all\nlogged-in users have access to `index`, `show`, `pluck`, and `fetch` (the\nread-only action helpers), but only administrators have access to `create`,\n`update`, etc. (the read-write action helpers). You can have as many roles as\nyou'd like, e.g. a super-administrator role to restrict access to `destroy`.\nUsers can be in one or more roles, and action helpers can be restricted to one\nor more roles for maximum flexibility.\n\nThe scheme is 100% opt-in. If you prefer to use [Pundit][34] or some other gem\nto handle authorization, go nuts!\n\nThere are three main components to Sinja's built-in scheme:\n\n#### `default_roles` configurables\n\nYou set the default roles for the entire Sinja application in the top-level\nconfiguration. Action helpers without any default roles are unrestricted by\ndefault.\n\n```ruby\nconfigure_jsonapi do |c|\n  # Resource roles\n  c.default_roles = {\n    index: :user,\n    show: :user,\n    create: :admin,\n    update: :admin,\n    destroy: :super\n  }\n\n  # To-one relationship roles\n  c.default_has_one_roles = {\n    pluck: :user,\n    prune: :admin,\n    graft: :admin\n  }\n\n  # To-many relationship roles\n  c.default_has_many_roles = {\n    fetch: :user,\n    clear: :admin,\n    replace: :admin,\n    merge: :admin,\n    subtract: :admin\n  }\nend\n```\n\n#### `:roles` Action Helper option\n\nTo override the default roles for any given action helper, specify a `:roles`\noption when defining it. To remove all restrictions from an action helper, set\n`:roles` to an empty array. For example, to manage access to `show` at\ndifferent levels of granularity (with the above default roles):\n\n```ruby\nresource :foos do\n  show do\n    # any logged-in user (with the `user' role) can access /foos/:id\n  end\nend\n\nresource :bars do\n  show(roles: :admin) do\n    # only logged-in users with the `admin' role can access /bars/:id\n  end\nend\n\nresource :quxes do\n  show(roles: []) do\n    # anyone (bypassing the `role' helper) can access /quxes/:id\n  end\nend\n```\n\n#### `role` helper\n\nFinally, define a `role` helper in your application that returns the user's\nrole(s) (if any). You can handle login failures in your middleware, elsewhere\nin the application (i.e. a `before` filter), or within the helper, either by\nraising an error or by letting Sinja raise an error on restricted action\nhelpers when `role` returns `nil` (the default behavior).\n\n```ruby\nhelpers do\n  def role\n    env['my_auth_middleware'].login!\n    session[:roles]\n  rescue MyAuthenticationFailure=\u003ee\n    nil\n  end\nend\n```\n\nIf you need more fine-grained control, for example if your action helper logic\nvaries by the user's role, you can use a switch statement on `role` along with\nthe `Sinja::Roles` utility class:\n\n```ruby\nindex(roles: [:user, :admin, :super]) do\n  case role\n  when Sinja::Roles[:user]\n    # logic specific to the `user' role\n  when Sinja::Roles[:admin, :super]\n    # logic specific to administrative roles\n  end\nend\n```\n\nOr use the `role?` helper:\n\n```ruby\nshow do |id|\n  exclude = []\n  exclude \u003c\u003c 'secrets' unless role?(:admin)\n\n  next resource, exclude: exclude\nend\n```\n\nYou can append resource- or even relationship-specific roles by defining a\nnested helper and calling `super` (keeping in mind that `resource` may be\n`nil`).\n\n```ruby\nhelpers do\n  def role\n    [:user] if logged_in_user\n  end\nend\n\nresource :foos do\n  helpers do\n    def role\n      super.tap do |a|\n        a \u003c\u003c :owner if resource\u0026.owner == logged_in_user\n      end\n    end\n  end\n\n  create(roles: :user) {|attr| .. }\n  update(roles: :owner) {|attr| .. }\nend\n```\n\nPlease see the [demo-app](/demo-app) for a more complete example.\n\nFinally, because the `role` helper is invoked several times and may return\ndifferent results throughout the request lifecycle, Sinja does not memoize\n(cache the return value keyed by function signature) it. If you have an\nexpensive component of your role helper that is not context-dependent, it may\nbe worth memoizing yourself:\n\n```ruby\nhelpers do\n  def role\n    @roles ||= expensive_role_lookup.freeze\n\n    @roles.dup.tap do |a|\n      a \u003c\u003c :foo if bar\n    end\n  end\nend\n```\n\n### Query Parameters\n\nThe {json:api} specification states that any unhandled query parameters should\ncause the request to abort with HTTP status 400. To enforce this requirement,\nSinja maintains a global \"whitelist\" of acceptable query parameters as well as\na per-route whitelist, and interrogates your application to see which features\nit supports; for example, a route may generally allow a `filter` query\nparameter, but you may not have defined a `filter` helper.\n\nTo let a custom query parameter through to the standard action helpers, add it\nto the `query_params` configurable with a `nil` value:\n\n```ruby\nconfigure_jsonapi do |c|\n  c.query_params[:foo] = nil\nend\n```\n\nTo let a custom route accept standard query parameters, add a `:qparams` route\ncondition:\n\n```ruby\nget '/top10', qparams: [:include, :sort] do\n  # ..\nend\n```\n\n### Working with Collections\n\n#### Filtering\n\nAllow clients to filter the collections returned by the `index` and `fetch`\naction helpers by defining a `filter` helper in the appropriate scope that\ntakes a collection and a hash of `filter` query parameters (with its top-level\nkeys dedasherized and symbolized) and returns the filtered collection. You may\nalso set a `:filter_by` option on the action helper to an array of symbols\nrepresenting the \"filter-able\" fields for that resource.\n\nFor example, to implement simple equality filters using Sequel:\n\n```ruby\nhelpers do\n  def filter(collection, fields={})\n    collection.where(fields)\n  end\nend\n\nresource :posts do\n  index(filter_by: [:title, :type]) do\n    Foo # return a Sequel::Dataset (instead of an array of Sequel::Model instances)\n  end\nend\n```\n\nThe easiest way to set a default filter is to tweak the post-processed query\nparameter(s) in a `before_\u003caction\u003e` hook:\n\n```ruby\nresource :posts do\n  helpers do\n    def before_index\n      params[:filter][:type] = 'article' if params[:filter].empty?\n    end\n  end\n\n  index do\n    # ..\n  end\nend\n```\n\n#### Sorting\n\nAllow clients to sort the collections returned by the `index` and `fetch`\naction helpers by defining a `sort` helper in the appropriate scope that takes\na collection and a hash of `sort` query parameters (with its top-level keys\ndedasherized and symbolized) and returns the sorted collection. The hash values\nare either `:asc` (to sort ascending) or `:desc` (to sort descending). You may\nalso set a `:sort_by` option on the action helper to an array of symbols\nrepresenting the \"sort-able\" fields for that resource.\n\nFor example, to implement sorting using Sequel:\n\n```ruby\nhelpers do\n  def sort(collection, fields={})\n    collection.order(*fields.map {|k, v| Sequel.send(v, k) })\n  end\nend\n\nresource :posts do\n  index(sort_by: :created_at) do\n    Foo # return a Sequel::Dataset (instead of an array of Sequel::Model instances)\n  end\nend\n```\n\nThe easiest way to set a default sort order is to tweak the post-processed\nquery parameter(s) in a `before_\u003caction\u003e` hook:\n\n```ruby\nresource :posts do\n  helpers do\n    def before_index\n      params[:sort][:title] = :asc if params[:sort].empty?\n    end\n  end\n\n  index do\n    # ..\n  end\nend\n```\n\n#### Paging\n\nAllow clients to page the collections returned by the `index` and `fetch`\naction helpers by defining a `page` helper in the appropriate scope that takes\na collection and a hash of `page` query parameters (with its top-level keys\ndedasherized and symbolized) and returns the paged collection along with a\nspecial nested hash used as root metadata and to build the paging links.\n\nThe top-level keys of the hash returned by this method must be members of the\nset: {`:self`, `:first`, `:prev`, `:next`, `:last`}. The values of the hash are\nhashes themselves containing the query parameters used to construct the\ncorresponding link. For example, the hash:\n\n```ruby\n{\n  prev: {\n    number: 3,\n    size: 10\n  },\n  next: {\n    number: 5,\n    size: 10\n  }\n}\n```\n\nCould be used to build the following top-level links in the response document:\n\n```json\n\"links\": {\n  \"prev\": \"/posts?page[number]=3\u0026page[size]=10\",\n  \"next\": \"/posts?page[number]=5\u0026page[size]=10\"\n}\n```\n\nYou must also set the `page_using` configurable to a hash of symbols\nrepresenting the paging fields used in your application (for example, `:number`\nand `:size` for the above example) along with their default values (or `nil`).\nPlease see the [Sequel extension][30] for a detailed, working example.\n\nThe easiest way to page a collection by default is to tweak the post-processed\nquery parameter(s) in a `before_\u003caction\u003e` hook:\n\n```ruby\nresource :posts do\n  helpers do\n    def before_index\n      params[:page][:number] = 1 if params[:page].empty?\n    end\n  end\n\n  index do\n    # ..\n  end\nend\n```\n\n#### Finalizing\n\nIf you need to perform any additional actions on a collection after it is\nfiltered, sorted, and/or paged, but before it is serialized, define a\n`finalize` helper that takes a collection and returns the finalized collection.\nFor example, to convert Sequel datasets to arrays of models before\nserialization:\n\n```ruby\nhelpers do\n  def finalize(collection)\n    collection.all\n  end\nend\n```\n\n### Conflicts\n\nIf your database driver raises exceptions on constraint violations, you should\nspecify which exception class(es) should be handled and return HTTP status 409.\n\nFor example, using [Sequel][13]:\n\n```ruby\nconfigure_jsonapi do |c|\n  c.conflict_exceptions \u003c\u003c Sequel::ConstraintViolation\nend\n```\n\n### Validations\n\nIf your ORM raises exceptions on validation errors, you should specify which\nexception class(es) should be handled and return HTTP status 422, along\nwith a formatter proc that transforms the exception object into an array of\ntwo-element arrays containing the name or symbol of the attribute that failed\nvalidation and the detailed errror message for that attribute.\n\nFor example, using [Sequel][13]:\n\n```ruby\nconfigure_jsonapi do |c|\n  c.validation_exceptions \u003c\u003c Sequel::ValidationFailed\n  c.validation_formatter = -\u003e(e) { e.errors.keys.zip(e.errors.full_messages) }\nend\n```\n\n### Missing Records\n\nIf your database driver raises exceptions on missing records, you should\nspecify which exception class(es) should be handled and return HTTP status 404.\nThis is particularly useful for relationship action helpers, which don't have\naccess to a dedicated subresource locator.\n\nFor example, using [Sequel][13]:\n\n```ruby\nconfigure_jsonapi do |c|\n  c.not_found_exceptions \u003c\u003c Sequel::NoMatchingRow\nend\n```\n\n### Transactions\n\nIf your database driver support transactions, you should define a yielding\n`transaction` helper in your application for Sinja to use when working with\nsideloaded data in the request. For example, if relationship data is provided\nin the request payload when creating resources, Sinja will automatically farm\nout to other routes to build those relationships after the resource is created.\nIf any step in that process fails, ideally the parent resource and any\nrelationships would be rolled back before returning an error message to the\nrequester.\n\nFor example, using [Sequel][13] with the database handle stored in the constant\n`DB`:\n\n```ruby\nhelpers do\n  def transaction\n    DB.transaction { yield }\n  end\nend\n```\n\n### Side-Unloading Related Resources\n\nYou may pass an `:include` serializer option (which can be either a\ncomma-delimited string or array of strings) when returning resources from\naction helpers. This instructs JSONAPI::Serializers to include a default set of\nrelated resources along with the primary resource. If the client specifies an\n`include` query parameter, Sinja will automatically pass it to\nJSONAPI::Serializer.serialize, replacing any default value. You may also pass a\nSinja-specific `:exclude` option to prevent certain related resources from\nbeing included in the response. If you exclude a resource, its descendents will\nbe automatically excluded as well. Feedback welcome.\n\nSinja will attempt to automatically exclude related resources based on the\ncurrent user's role(s) and any available `pluck` and `fetch` action helper\nroles. For example, if resource Foo has many Bars and the current user does not\nhave access to Foo.Bars#fetch, the user will not be able to include Bars. It\nwill traverse the roles configuration, so if the current user has access to\nFoo.Bars#fetch but not Bars.Qux#pluck, the user will be able to include Bars\nbut not Bars.Qux. This feature is experimental. Note that in contrast to the\n`:exclude` option, if a related resource is excluded by this mechanism, its\ndescendents will _not_ be automatically excluded.\n\n### Side-Loading Relationships\n\nSinja works hard to DRY up your business logic. As mentioned above, when a\nrequest comes in to create or update a resource and that request includes\nrelationships, Sinja will try to farm out the work to your defined relationship\nroutes. Let's look at this example request from the {json:api} specification:\n\n```\nPOST /photos HTTP/1.1\nContent-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n```\n\n```json\n{\n  \"data\": {\n    \"type\": \"photos\",\n    \"attributes\": {\n      \"title\": \"Ember Hamster\",\n      \"src\": \"http://example.com/images/productivity.png\"\n    },\n    \"relationships\": {\n      \"photographer\": {\n        \"data\": { \"type\": \"people\", \"id\": \"9\" }\n      }\n    }\n  }\n}\n```\n\nAssuming a `:photos` resource with a `has_one :photographer` relationship in\nthe application, and `graft` is configured to sideload on `create` (more on\nthis in a moment), Sinja will invoke the following action helpers in turn:\n\n1. `create` on the Photos resource (with `data.attributes`)\n1. `graft` on the Photographer relationship (with\n    `data.relationships.photographer.data`)\n\nIf any step of the process fails\u0026mdash;for example, if the `graft` action\nhelper is not defined in the Photographer relationship, or if it does not\npermit sideloading from `create`, or if it raises an error\u0026mdash;the entire\nrequest will fail and any database changes will be rolled back (given a\n`transaction` helper). Note that the user's role must grant them access to call\neither `graft` or `create`.\n\n`create` and `update` are the resource action helpers that trigger sideloading;\n`graft` and `prune` are the to-one action helpers invoked by sideloading; and\n`replace`, `merge`, and `clear` are the to-many action helpers invoked by\nsideloading. You must indicate which combinations are valid using the\n`:sideload_on` action helper option. For example:\n\n```ruby\nresource :photos do\n  helpers do\n    def find(id) ..; end\n  end\n\n  create {|attr| .. }\n\n  has_one :photographer do\n    # Allow `create' to sideload Photographer\n    graft(sideload_on: :create) {|rio| .. }\n  end\n\n  has_many :tags do\n    # Allow `create' to sideload Tags\n    merge(sideload_on: :create) {|rios| .. }\n  end\nend\n```\n\nThe following matrix outlines which combinations of action helpers and\n`:sideload_on` options enable which behaviors:\n\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n  \u003cth rowspan=\"2\"\u003eDesired behavior\u003c/th\u003e\n  \u003cth colspan=\"2\"\u003eFor to-one relationship(s)\u003c/th\u003e\n  \u003cth colspan=\"2\"\u003eFor to-many relationship(s)\u003c/th\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n  \u003cth\u003eDefine Action Helper\u003c/th\u003e\n  \u003cth\u003eWith \u003ccode\u003e:sideload_on\u003c/code\u003e\u003c/th\u003e\n  \u003cth\u003eDefine Action Helper\u003c/th\u003e\n  \u003cth\u003eWith \u003ccode\u003e:sideload_on\u003c/code\u003e\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n  \u003ctd\u003eSet relationship(s) when creating resource\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003egraft\u003c/code\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003e:create\u003c/code\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003emerge\u003c/code\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003e:create\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n  \u003ctd\u003eSet relationship(s) when updating resource\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003egraft\u003c/code\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003e:update\u003c/code\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003ereplace\u003c/code\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003e:update\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n  \u003ctd\u003eDelete relationship(s) when updating resource\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003eprune\u003c/code\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003e:update\u003c/code\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003eclear\u003c/code\u003e\u003c/td\u003e\n  \u003ctd\u003e\u003ccode\u003e:update\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\n#### Deferring Relationships\n\nIf you're side-loading multiple relationships, you may need one applied before\nanother (e.g. set the author of a post before setting its tags). You can use\nthe built-in `defer` helper to affect the order of operations:\n\n```ruby\nhas_one :author do\n  graft(sideload_on: :create) do |rio|\n    resource.author = Author.with_pk!(rio[:id].to_i)\n    resource.save_changes\n  end\nend\n\nhas_many :tags do\n  merge(sideload_on: :create) do |rios|\n    defer unless resource.author # come back to this if the author isn't set yet\n\n    tags = resource.author.preferred_tags\n    # ..\n  end\nend\n```\n\n#### Avoiding Null Foreign Keys\n\nNow, let's say our DBA is forward-thinking and wants to make the foreign key\nconstraint between the `photographer_id` column on the Photos table and the\nPeople table non-nullable. Unfortunately, that will break Sinja, because the\nPhoto will be inserted first, with a null Photographer. (Deferrable constraints\nwould be a perfect solution to this problem, but `NOT NULL` constraints are not\ndeferrable in Postgres, and constraints in general are not deferrable in\nMySQL.)\n\nInstead, we'll need to enforce our non-nullable relationships at the\napplication level. To accomplish this, define an ordinary helper named\n`validate!` (in the resource scope or any parent scopes). This method, if\npresent, is invoked from within the transaction after the entire request has\nbeen processed, and so can abort the transaction (following your ORM's\nsemantics). For example:\n\n```ruby\nresource :photos do\n  helpers do\n    def validate!\n      fail 'Invalid Photographer for Photo' if resource.photographer.nil?\n    end\n  end\nend\n```\n\nIf your ORM supports validation\u0026mdash;and \"deferred validation\"\u0026mdash;you can\neasily handle all such situations (as well as other types of validations) at\nthe top-level of your application. (Make sure to define your validation\nexceptions and formatter as described above.) For example, using [Sequel][13]:\n\n```ruby\nclass Photo \u003c Sequel::Model\n  many_to_one :photographer\n\n  # http://sequel.jeremyevans.net/rdoc/files/doc/validations_rdoc.html\n  def validate\n    super\n    errors.add(:photographer, 'cannot be null') if photographer.nil?\n  end\nend\n\nhelpers do\n  def validate!\n    raise Sequel::ValidationFailed, resource.errors unless resource.valid?\n  end\nend\n\nresource :photos do\n  create do |attr|\n    photo = Photo.new\n    photo.set(attr)\n    photo.save(validate: false) # defer validation\n    next photo.id, photo\n  end\n\n  has_one :photographer do\n    graft(sideload_on: :create) do |rio|\n      resource.photographer = People.with_pk!(rio[:id].to_i)\n      resource.save_changes(validate: !sideloaded?) # defer validation if sideloaded\n    end\n  end\nend\n```\n\nNote that the `validate!` hook is _only_ invoked from within transactions\ninvolving the `create` and `update` action helpers (and any action helpers\ninvoked via the sideloading mechanism), so this deferred validation pattern is\nonly appropriate in those cases. You must use immedate validation in all other\ncases. The `sideloaded?` helper is provided to help disambiguate edge cases.\n\n\u003e TODO: The following three sections are a little confusing. Rewrite them.\n\n##### Many-to-One\n\nExample: Photo belongs to (has one) Photographer; Photo.Photographer cannot be\nnull.\n\n* Don't define `prune` relationship action helper\n* Define `graft` relationship action helper to enable reassigning the Photographer\n* Define `destroy` resource action helper to enable removing the Photo\n* Use `validate!` helper to check for nulls\n\n##### One-to-Many\n\nExample: Photographer has many Photos; Photo.Photographer cannot be null.\n\n* Don't define `clear` relationship action helper\n* Don't define `subtract` relationship action helper\n* Delegate removing Photos and reassigning Photographers to Photo resource\n\n##### Many-to-Many\n\nExample: Photo has many Tags.\n\nNothing to worry about here! Feel free to use `NOT NULL` foreign key\nconstraints on the join table.\n\n### Coalesced Find Requests\n\nIf your {json:api} client coalesces find requests, the resource locator (or\n`show` action helper) will be invoked once for each ID in the `:id` filter, and\nthe resulting collection will be serialized on the response. Both query\nparameter syntaxes for arrays are supported: `?filter[id]=1,2` and\n`?filter[id][]=1\u0026filter[id][]=2`. If any ID is not found (i.e. `show` returns\n`nil`), the route will halt with HTTP status 404.\n\nOptionally, to reduce round trips to the database, you may define a \"special\"\n`show_many` action helper that takes an array of IDs to show. It does not take\n`:roles` or any other options and will only be invoked if the current user has\naccess to `show`. This feature is experimental.\n\nCollections assembled during coalesced find requests will not be filtered,\nsorted, or paged. The easiest way to limit the number of records that can be\nqueried is to define a `show_many` action helper and validate the length of the\npassed array in the `before_show_many` hook. For example, using [Sequel][13]:\n\n```ruby\nresource :foos do\n  helpers do\n    def before_show_many(ids)\n      halt 413, 'You want the impossible.' if ids.length \u003e 50\n    end\n  end\n\n  show_many do |ids|\n    Foo.where_all(id: ids.map!(\u0026:to_i))\n  end\nend\n```\n\n### Patchless Clients\n\n{json:api} [recommends][23] supporting patchless clients by using the\n`X-HTTP-Method-Override` request header to coerce a `POST` into a `PATCH`. To\nsupport this in Sinja, add the Sinja::MethodOverride middleware (which is a\nstripped-down version of [Rack::MethodOverride][24]) into your application (or\nRackup configuration):\n\n```ruby\nrequire 'sinja'\nrequire 'sinja/method_override'\n\nclass MyApp \u003c Sinatra::Base\n  use Sinja::MethodOverride\n\n  register Sinja\n\n  # ..\nend\n```\n\n## Extensions\n\nSinja extensions provide additional helpers, DSL, and ORM-specific boilerplate\nas separate gems. Community contributions welcome!\n\n### Sequel\n\nPlease see [Sinja::Sequel][30] for more information.\n\n## Application Concerns\n\n### Performance\n\nAlthough there is some heavy metaprogramming happening at boot time, the end\nresult is simply a collection of Sinatra namespaces, routes, filters,\nconditions, helpers, etc., and Sinja applications should perform as if you had\nwritten them verbosely. The main caveat is that there are quite a few block\nclosures, which don't perform as well as normal methods in Ruby. Feedback\nwelcome.\n\n### Public APIs\n\nSinja makes a few APIs public to help you work around edge cases in your\napplication.\n\n#### Commonly Used\n\n**can?**\n: Takes the symbol of an action helper and returns true if the current user has\n  access to call that action helper for the current resource using the `role`\n  helper and role definitions detailed under \"Authorization\" below.\n\n**role?**\n: Takes a list of role(s) and returns true if it has members in common with the\n  current user's role(s).\n\n**sideloaded?**\n: Returns true if the request was invoked from another action helper.\n\n#### Less-Commonly Used\n\nThese are helpful if you want to add some custom routes to your Sinja\napplication.\n\n**data**\n: Returns the `data` key of the deserialized request payload (with symbolized\n  names).\n\n**dedasherize**\n: Takes a string or symbol and returns the string or symbol with any and all\n  dashes transliterated to underscores, and camelCase converted to snake_case.\n\n**dedasherize_names**\n: Takes a hash and returns the hash with its keys dedasherized (deeply).\n\n**serialize_model**\n: Takes a model (and optional hash of JSONAPI::Serializers options) and returns\n  a serialized model.\n\n**serialize_model?**\n: Takes a model (and optional hash of JSONAPI::Serializers options) and returns\n  a serialized model if non-`nil`, or the root metadata if present, or a HTTP\n  status 204.\n\n**serialize_models**\n: Takes an array of models (and optional hash of JSONAPI::Serializers options)\n  and returns a serialized collection.\n\n**serialize_models?**\n: Takes an array of models (and optional hash of JSONAPI::Serializers options)\n  and returns a serialized collection if non-empty, or the root metadata if\n  present, or a HTTP status 204.\n\n### Sinja or Sinatra::JSONAPI\n\nEverything is dual-namespaced under both Sinatra::JSONAPI and Sinja, and Sinja\nrequires Sinatra::Base, so this:\n\n```ruby\nrequire 'sinatra/base'\nrequire 'sinatra/jsonapi'\n\nclass App \u003c Sinatra::Base\n  register Sinatra::JSONAPI\n\n  configure_jsonapi do |c|\n    # ..\n  end\n\n  # ..\n\n  freeze_jsonapi\nend\n```\n\nCan also be written like this (\"modular\"-style applications only):\n\n```ruby\nrequire 'sinja'\n\nclass App \u003c Sinatra::Base\n  register Sinja\n\n  sinja.configure do |c|\n    # ..\n  end\n\n  # ..\n\n  sinja.freeze\nend\n```\n\n### Code Organization\n\nSinja applications might grow overly large with a block for each resource. I am\nstill working on a better way to handle this (as well as a way to provide\nstandalone resource controllers for e.g. cloud functions), but for the time\nbeing you can store each resource block as its own Proc, and pass it to the\n`resource` keyword as a block. The migration to some future solution should be\nrelatively painless. For example:\n\n```ruby\n# controllers/foo_controller.rb\nFooController = proc do\n  show do |id|\n    Foo[id.to_i]\n  end\n\n  index do\n    Foo.all\n  end\n\n  # ..\nend\n\n# app.rb\nrequire 'sinatra/base'\nrequire 'sinatra/jsonapi'\n\nrequire_relative 'controllers/foo_controller'\n\nclass App \u003c Sinatra::Base\n  register Sinatra::JSONAPI\n\n  resource :foos, \u0026FooController\n\n  freeze_jsonapi\nend\n```\n\n### Testing\n\nThe short answer to \"How do I test my Sinja application?\" is \"Like you would\nany other Sinatra application.\" Unfortunately, the testing story isn't quite\n*there* yet for Sinja. I think leveraging something like [Munson][27] or\n[json_api_client][28] is probably the best bet for integration testing, but\nunfortunately both projects are rife with broken and/or missing critical\nfeatures. And until we can solve the general code organization problem (most\nlikely with patches to Sinatra), it will remain difficult to isolate action\nhelpers and other artifacts for unit testing.\n\nSinja's own test suite is based on [Rack::Test][29] (plus some ugly kludges).\nI wouldn't recommend it but it might be a good place to start looking for\nideas. It leverages the [demo-app](/demo-app) with Sequel and an in-memory\ndatabase to perform integration testing of Sinja's various features under\nMRI/YARV and JRuby. The goal is to free you from worrying about whether your\napplications will behave according to the {json:api} spec (as long as you\nfollow the usage documented in this README) and focus on testing your business\nlogic.\n\n## Comparison with JSONAPI::Resources\n\n| Feature         | JR                               | Sinja                                             |\n| :-------------- | :------------------------------- | :------------------------------------------------ |\n| Serializer      | Built-in                         | [JSONAPI::Serializers][3]                         |\n| Framework       | Rails                            | Sinatra 2.0, but easy to mount within others      |\n| Routing         | ActionDispatch::Routing          | Mustermann                                        |\n| Caching         | ActiveSupport::Cache             | BYO                                               |\n| ORM             | ActiveRecord/ActiveModel         | BYO                                               |\n| Authorization   | [Pundit][9]                      | Role-based                                        |\n| Immutability    | `immutable` method               | Omit mutator action helpers (e.g. `update`)       |\n| Fetchability    | `fetchable_fields` method        | Omit attributes in Serializer                     |\n| Creatability    | `creatable_fields` method        | Handle in `create` action helper or Model\\*       |\n| Updatability    | `updatable_fields` method        | Handle in `update` action helper or Model\\*       |\n| Sortability     | `sortable_fields` method         | `sort` helper and `:sort_by` option               |\n| Default sorting | `default_sort` method            | Set default for `params[:sort]`                   |\n| Context         | `context` method                 | Rack middleware (e.g. `env['context']`)           |\n| Attributes      | Define in Model and Resource     | Define in Model\\* and Serializer                  |\n| Formatting      | `:format` attribute keyword      | Define attribute as a method in Serialier         |\n| Relationships   | Define in Model and Resource     | Define in Model, Resource, and Serializer         |\n| Filters         | `filter(s)` keywords             | `filter` helper and `:filter_by` option           |\n| Default filters | `:default` filter keyword        | Set default for `params[:filter]`                 |\n| Pagination      | JSONAPI::Paginator               | `page` helper and `page_using` configurable       |\n| Meta            | `meta` method                    | Serializer `:meta` option               |\n| Primary keys    | `resource_key_type` configurable | Serializer `id` method               |\n\n\\* \u0026ndash; Depending on your ORM.\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run\n`rake spec` to run the tests. You can also run `bin/console` for an interactive\nprompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To\nrelease a new version, update the version number in `version.rb`, and then run\n`bundle exec rake release`, which will create a git tag for the version, push\ngit commits and tags, and push the `.gem` file to\n[rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at\nhttps://github.com/mwpastore/sinja.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT\nLicense](http://opensource.org/licenses/MIT).\n\n[1]: http://www.sinatrarb.com\n[2]: http://jsonapi.org\n[3]: https://github.com/fotinakis/jsonapi-serializers\n[4]: http://www.rubydoc.info/github/rack/rack/master/Rack/URLMap\n[5]: http://rodauth.jeremyevans.net\n[6]: https://github.com/sinatra/sinatra/tree/master/rack-protection\n[7]: http://jsonapi.org/format/1.0/\n[8]: https://github.com/cerebris/jsonapi-resources\n[9]: https://github.com/cerebris/jsonapi-resources#authorization\n[10]: http://www.sinatrarb.com/extensions-wild.html\n[11]: https://en.wikipedia.org/wiki/Representational_state_transfer\n[12]: https://github.com/rails-api/active_model_serializers\n[13]: http://sequel.jeremyevans.net\n[14]: http://talentbox.github.io/sequel-rails/\n[15]: http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/ActiveModel.html\n[16]: http://rubyonrails.org\n[17]: https://github.com/rails/rails/tree/master/activerecord\n[18]: https://github.com/rails/rails/tree/master/activemodel\n[19]: http://www.ruby-grape.org\n[20]: http://roda.jeremyevans.net\n[21]: http://www.sinatrarb.com/contrib/namespace.html\n[22]: http://jsonapi.org/format/#document-resource-identifier-objects\n[23]: http://jsonapi.org/recommendations/#patchless-clients\n[24]: http://www.rubydoc.info/github/rack/rack/Rack/MethodOverride\n[25]: http://www.sinatrarb.com/mustermann/\n[26]: https://jsonapi-suite.github.io/jsonapi_suite/\n[27]: https://github.com/coryodaniel/munson\n[28]: https://github.com/chingor13/json_api_client\n[29]: https://github.com/brynary/rack-test\n[30]: https://github.com/mwpastore/sinja-sequel\n[31]: http://jsonapi.org/implementations/#server-libraries-ruby\n[32]: http://emberjs.com\n[33]: https://github.com/fotinakis/jsonapi-serializers#more-customizations\n[34]: https://github.com/elabs/pundit\n","funding_links":[],"categories":["Writing APIs"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmwpastore%2Fsinja","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmwpastore%2Fsinja","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmwpastore%2Fsinja/lists"}