sinja 1.0.0.pre2 → 1.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c550d3cff0b520955dbd6add3f45fe426d0ba03
4
- data.tar.gz: 2c0273185088e63db97d8a1de1e6c8612f9999c8
3
+ metadata.gz: 4491b24eba74ba902735fc26ff679b074bc5a76c
4
+ data.tar.gz: 54f55fd1b953b7dd31b5bb911386bb6f1bf83419
5
5
  SHA512:
6
- metadata.gz: beaec25e26829f91f373e886449a64d10629f4905c1fb809576522aa866cd7843bd61c263fb219e566fe17fe2f31c59d0565c5c4230d10e9acd23bd4b09dc781
7
- data.tar.gz: d7d470ba61bc03da60129b9e8566b5bd98fdcf7e6223f4e59e72c112bca2fdbfc8d026e209aeb4a651493281a6e38e0f247733ddd906dbefd4c2bce66ee44177
6
+ metadata.gz: 362031551256e0e28912b13447825269388a54ff698624027ae44e557fc37489686c11427fe43c84b5983d23f8317772c58f5d0185c8373df1e4f43698abf90a
7
+ data.tar.gz: f282eef01caa1c271f7555f1121f3d2d7fba5e65962a20c10e8e688e380ec67930be8b95002eaad734de1ed9b573421cadaa95b77af6bcea9ef64dd3975e8bf7
data/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # Sinja (Sinatra::JSONAPI)
2
2
 
3
- [![Build Status](https://travis-ci.org/mwpastore/sinja.svg?branch=master)](https://travis-ci.org/mwpastore/sinja)
4
3
  [![Gem Version](https://badge.fury.io/rb/sinja.svg)](https://badge.fury.io/rb/sinja)
4
+ [![Build Status](https://travis-ci.org/mwpastore/sinja.svg?branch=master)](https://travis-ci.org/mwpastore/sinja)
5
5
 
6
6
  Sinja is a [Sinatra][1] [extension][10] for quickly building [RESTful][11],
7
- [JSON:API][2]-[compliant][7] web services, leveraging the excellent
7
+ [{json:api}][2]-[compliant][7] web services, leveraging the excellent
8
8
  [JSONAPI::Serializers][3] gem and [Sinatra::Namespace][21] extension. It
9
9
  enhances Sinatra's DSL to enable resource-, relationship-, and role-centric
10
10
  definition of applications, and it configures Sinatra with the proper settings,
11
- MIME-types, filters, conditions, and error-handling to implement JSON:API.
11
+ MIME-types, filters, conditions, and error-handling to implement {json:api}.
12
12
  Sinja aims to be lightweight (to the extent that Sinatra is), ORM-agnostic (to
13
13
  the extent that JSONAPI::Serializers is), and opinionated (to the extent that
14
- the JSON:API specification is).
14
+ the {json:api} specification is).
15
15
 
16
16
  <!-- START doctoc generated TOC please keep comment here to allow auto update -->
17
17
  <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
@@ -19,16 +19,18 @@ the JSON:API specification is).
19
19
 
20
20
  - [Synopsis](#synopsis)
21
21
  - [Installation](#installation)
22
- - [Features](#features)
23
- - [Extensibility](#extensibility)
24
- - [Public APIs](#public-apis)
22
+ - [Features & Design](#features--design)
23
+ - [Ol' Blue Eyes is Back](#ol-blue-eyes-is-back)
24
+ - [Public APIs](#public-apis)
25
+ - [Commonly Used](#commonly-used)
26
+ - [Less-Commonly Used](#less-commonly-used)
25
27
  - [Performance](#performance)
26
- - [Comparison with JSONAPI::Resources (JR)](#comparison-with-jsonapiresources-jr)
27
- - [Usage](#usage)
28
+ - [Comparison with JSONAPI::Resources](#comparison-with-jsonapiresources)
29
+ - [Basic Usage](#basic-usage)
28
30
  - [Configuration](#configuration)
29
31
  - [Sinatra](#sinatra)
30
32
  - [Sinja](#sinja)
31
- - [Resource Locator](#resource-locator)
33
+ - [Resource Locators](#resource-locators)
32
34
  - [Action Helpers](#action-helpers)
33
35
  - [`resource`](#resource)
34
36
  - [`index {..}` => Array](#index---array)
@@ -46,11 +48,18 @@ the JSON:API specification is).
46
48
  - [`clear {..}` => TrueClass?](#clear---trueclass)
47
49
  - [`merge {|rios| ..}` => TrueClass?](#merge-rios---trueclass)
48
50
  - [`subtract {|rios| ..}` => TrueClass?](#subtract-rios---trueclass)
49
- - [Action Helper Hooks &amp; Utilities](#action-helper-hooks-amp-utilities)
51
+ - [Advanced Usage](#advanced-usage)
52
+ - [Action Helper Hooks & Utilities](#action-helper-hooks--utilities)
50
53
  - [Authorization](#authorization)
51
54
  - [`default_roles` configurables](#default_roles-configurables)
52
55
  - [`:roles` Action Helper option](#roles-action-helper-option)
53
56
  - [`role` helper](#role-helper)
57
+ - [Query Parameters](#query-parameters)
58
+ - [Working with Collections](#working-with-collections)
59
+ - [Filtering](#filtering)
60
+ - [Sorting](#sorting)
61
+ - [Paging](#paging)
62
+ - [Finalizing](#finalizing)
54
63
  - [Conflicts](#conflicts)
55
64
  - [Validations](#validations)
56
65
  - [Missing Records](#missing-records)
@@ -63,8 +72,10 @@ the JSON:API specification is).
63
72
  - [Many-to-Many](#many-to-many)
64
73
  - [Coalesced Find Requests](#coalesced-find-requests)
65
74
  - [Patchless Clients](#patchless-clients)
75
+ - [Application Concerns](#application-concerns)
66
76
  - [Sinja or Sinatra::JSONAPI](#sinja-or-sinatrajsonapi)
67
77
  - [Code Organization](#code-organization)
78
+ - [Testing](#testing)
68
79
  - [Development](#development)
69
80
  - [Contributing](#contributing)
70
81
  - [License](#license)
@@ -96,14 +107,14 @@ freeze_jsonapi
96
107
 
97
108
  Assuming the presence of a `Post` model and serializer, running the above
98
109
  "classic"-style Sinatra application would enable the following endpoints (with
99
- all other JSON:API endpoints returning 404 or 405):
110
+ all other {json:api} endpoints returning 404 or 405):
100
111
 
101
112
  * `GET /posts/<id>`
102
113
  * `GET /posts`
103
114
  * `POST /posts`
104
115
 
105
116
  The resource locator and other action helpers, documented below, enable other
106
- endpoints.
117
+ endpoints. Please see the [demo-app](/demo-app) for more complete examples.
107
118
 
108
119
  Of course, "modular"-style Sinatra aplications require you to register the
109
120
  extension:
@@ -143,37 +154,40 @@ Or install it yourself as:
143
154
  $ gem install sinja
144
155
  ```
145
156
 
146
- ## Features
157
+ ## Features & Design
147
158
 
148
159
  * ORM-agnostic
149
- * Role-based authorization
160
+ * Simple role-based authorization
150
161
  * To-one and to-many relationships and related resources
151
162
  * Side-loaded relationships on resource creation and update
152
163
  * Error-handling
153
164
  * Conflicts (constraint violations)
154
165
  * Missing records
155
166
  * Validation failures
156
- * Plus all the features of JSONAPI::Serializers!
167
+ * Filtering, sorting, and paging collections
168
+ * Plus all the features of [JSONAPI::Serializers][3]!
157
169
 
158
170
  Its main competitors in the Ruby space are [ActiveModelSerializers][12] (AMS)
159
- with the JsonApi adapter and [JSONAPI::Resources][8] (JR), both of which are
160
- designed to work with [Rails][16] and [ActiveRecord][17]/[ActiveModel][18]
161
- (although they may work with [Sequel][13] via [sequel-rails][14] and Sequel's
162
- [`:active_model` plugin][15]). Otherwise, you might use something like Sinatra,
163
- [Roda][20], or [Grape][19] with JSONAPI::Serializers, your own routes, and a
164
- ton of boilerplate. The goal of this extension is to provide most or all of the
165
- boilerplate for a Sintara application and automate the drawing of routes based
166
- on the resource definitions.
167
-
168
- ### Extensibility
169
-
170
- The "power" of implementing this functionality as a Sinatra extension is that
171
- all of Sinatra's usual features are available within your resource definitions.
172
- The action helpers blocks get compiled into Sinatra helpers, and the
173
- `resource`, `has_one`, and `has_many` keywords build [Sinatra::Namespace][21]
174
- blocks. You can manage caching directives, set headers, and even `halt` (or
175
- `not_found`, although such cases are usually handled transparently by returning
176
- `nil` values or empty collections from action helpers) as desired.
171
+ with the JsonApi adapter, [JSONAPI::Resources][8] (JR), and
172
+ [jsonapi-utils][26], all of which are designed to work with [Rails][16] and
173
+ [ActiveRecord][17]/[ActiveModel][18] (although they may work with [Sequel][13]
174
+ via [sequel-rails][14] and Sequel's [`:active_model` plugin][15]). Otherwise,
175
+ you might use something like Sinatra, [Roda][20], or [Grape][19] with
176
+ JSONAPI::Serializers (or another (de)serialization library), your own routes,
177
+ and a ton of boilerplate. The goal of this extension is to provide most or all
178
+ of the boilerplate for a Sintara application and automate the drawing of routes
179
+ based on the resource definitions.
180
+
181
+ ### Ol' Blue Eyes is Back
182
+
183
+ The "power" so to speak of implementing this functionality as a Sinatra
184
+ extension is that all of Sinatra's usual features are available within your
185
+ resource definitions. The action helpers blocks get compiled into Sinatra
186
+ helpers, and the `resource`, `has_one`, and `has_many` keywords build
187
+ [Sinatra::Namespace][21] blocks. You can manage caching directives, set
188
+ headers, and even `halt` (or `not_found`, although such cases are usually
189
+ handled transparently by returning `nil` values or empty collections from
190
+ action helpers) as appropriate.
177
191
 
178
192
  ```ruby
179
193
  class App < Sinatra::Base
@@ -232,7 +246,7 @@ class App < Sinatra::Base
232
246
 
233
247
  show do |id|
234
248
  book = find(id)
235
- not_found "Book #{id} not found!" unless book
249
+ next unless book
236
250
  headers 'X-ISBN'=>book.isbn
237
251
  last_modified book.updated_at
238
252
  next book, include: %w[author]
@@ -266,13 +280,30 @@ class App < Sinatra::Base
266
280
  end
267
281
  ```
268
282
 
269
- #### Public APIs
283
+ ### Public APIs
284
+
285
+ Sinja makes a few APIs public to help you work around edge cases in your
286
+ application.
287
+
288
+ #### Commonly Used
270
289
 
271
290
  **can?**
272
291
  : Takes the symbol of an action helper and returns true if the current user has
273
292
  access to call that action helper for the current resource using the `role`
274
293
  helper and role definitions detailed under "Authorization" below.
275
294
 
295
+ **role?**
296
+ : Takes a list of role(s) and returns true if it has members in common with the
297
+ current user's role(s).
298
+
299
+ **sideloaded?**
300
+ : Returns true if the request was invoked from another action helper.
301
+
302
+ #### Less-Commonly Used
303
+
304
+ These are helpful if you want to add some custom routes to your Sinja
305
+ application.
306
+
276
307
  **data**
277
308
  : Returns the `data` key of the deserialized request payload (with symbolized
278
309
  names).
@@ -284,10 +315,6 @@ end
284
315
  **dedasherize_names**
285
316
  : Takes a hash and returns the hash with its keys dedasherized (deeply).
286
317
 
287
- **role?**
288
- : Takes a list of role(s) and returns true if it has members in common with the
289
- current user's role(s).
290
-
291
318
  **serialize_model**
292
319
  : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
293
320
  a serialized model.
@@ -306,9 +333,6 @@ end
306
333
  and returns a serialized collection if non-empty, or the root metadata if
307
334
  present, or a HTTP status 204.
308
335
 
309
- **sideloaded?**
310
- : Returns true if the request was invoked from another action helper.
311
-
312
336
  ### Performance
313
337
 
314
338
  Although there is some heavy metaprogramming happening at boot time, the end
@@ -318,60 +342,52 @@ written them verbosely. The main caveat is that there are quite a few block
318
342
  closures, which don't perform as well as normal methods in Ruby. Feedback
319
343
  welcome.
320
344
 
321
- ### Comparison with JSONAPI::Resources (JR)
322
-
323
- | Feature | JR | Sinja |
324
- | :-------------- | :--------------------------- | :------------------------------------------------ |
325
- | Serializer | Built-in | [JSONAPI::Serializers][3] |
326
- | Framework | Rails | Sinatra, but easy to mount within others |
327
- | Routing | ActionDispatch::Routing | Mustermann |
328
- | Caching | ActiveSupport::Cache | BYO |
329
- | ORM | ActiveRecord/ActiveModel | BYO |
330
- | Authorization | [Pundit][9] | Role-based (`roles` keyword and `role` helper) |
331
- | Immutability | `immutable` method | Omit mutator action helpers |
332
- | Fetchability | `fetchable_fields` method | Omit attributes in Serializer |
333
- | Creatability | `creatable_fields` method | Handle in `create` action helper or Model\* |
334
- | Updatability | `updatable_fields` method | Handle in `update` action helper or Model\* |
335
- | Sortability | `sortable_fields` method | Handle `params[:sort]` in `index` action helper |
336
- | Default sorting | `default_sort` method | Set default for `params[:sort]` |
337
- | Context | `context` method | Rack middleware (e.g. `env['context']`) |
338
- | Attributes | Define in Model and Resource | Define in Model\* and Serializer |
339
- | Formatting | `format` attribute keyword | Define attribute as a method in Serialier |
340
- | Relationships | Define in Model and Resource | Define in Model, Resource, and Serializer |
341
- | Filters | `filter(s)` keywords | Handle `params[:filter]` in `index` action helper |
342
- | Default filters | `default` filter keyword | Set default for `params[:filter]` |
345
+ ### Comparison with JSONAPI::Resources
346
+
347
+ | Feature | JR | Sinja |
348
+ | :-------------- | :------------------------------- | :------------------------------------------------ |
349
+ | Serializer | Built-in | [JSONAPI::Serializers][3] |
350
+ | Framework | Rails | Sinatra, but easy to mount within others |
351
+ | Routing | ActionDispatch::Routing | Mustermann |
352
+ | Caching | ActiveSupport::Cache | BYO |
353
+ | ORM | ActiveRecord/ActiveModel | BYO |
354
+ | Authorization | [Pundit][9] | Role-based |
355
+ | Immutability | `immutable` method | Omit mutator action helpers (e.g. `update`) |
356
+ | Fetchability | `fetchable_fields` method | Omit attributes in Serializer |
357
+ | Creatability | `creatable_fields` method | Handle in `create` action helper or Model\* |
358
+ | Updatability | `updatable_fields` method | Handle in `update` action helper or Model\* |
359
+ | Sortability | `sortable_fields` method | `sort` helper and `:sort_by` option |
360
+ | Default sorting | `default_sort` method | Set default for `params[:sort]` |
361
+ | Context | `context` method | Rack middleware (e.g. `env['context']`) |
362
+ | Attributes | Define in Model and Resource | Define in Model\* and Serializer |
363
+ | Formatting | `:format` attribute keyword | Define attribute as a method in Serialier |
364
+ | Relationships | Define in Model and Resource | Define in Model, Resource, and Serializer |
365
+ | Filters | `filter(s)` keywords | `filter` helper and `:filter_by` option |
366
+ | Default filters | `:default` filter keyword | Set default for `params[:filter]` |
367
+ | Pagination | JSONAPI::Paginator | `page` helper and `page_using` configurable |
368
+ | Meta | `meta` method | Serializer `:meta` option |
369
+ | Primary keys | `resource_key_type` configurable | Serializer `id` method |
343
370
 
344
371
  \* - Depending on your ORM.
345
372
 
346
- This list is incomplete. TODO:
347
-
348
- * Primary keys
349
- * Pagination
350
- * Custom links
351
- * Meta
352
- * Side-loading (on request and response)
353
- * Namespaces
354
- * Configuration
355
- * Validation
356
-
357
- ## Usage
373
+ ## Basic Usage
358
374
 
359
375
  You'll need a database schema and models (using the engine and ORM of your
360
376
  choice) and [serializers][3] to get started. Create a new Sinatra application
361
- (classic or modular) to hold all your JSON:API endpoints and (if modular)
377
+ (classic or modular) to hold all your {json:api} endpoints and (if modular)
362
378
  register this extension. Instead of defining routes with `get`, `post`, etc. as
363
379
  you normally would, define `resource` blocks with action helpers and `has_one`
364
380
  and `has_many` relationship blocks (with their own action helpers). Sinja will
365
381
  draw and enable the appropriate routes based on the defined resources,
366
382
  relationships, and action helpers. Other routes will return the appropriate
367
- HTTP status codes: 403, 404, or 405.
383
+ HTTP statuses: 403, 404, or 405.
368
384
 
369
385
  ### Configuration
370
386
 
371
387
  #### Sinatra
372
388
 
373
389
  Registering this extension has a number of application-wide implications,
374
- detailed below. If you have any non-JSON:API routes, you may want to keep them
390
+ detailed below. If you have any non-{json:api} routes, you may want to keep them
375
391
  in a separate application and incorporate them as middleware or mount them
376
392
  elsewhere (e.g. with [Rack::URLMap][4]), or host them as a completely separate
377
393
  web service. It may not be feasible to have custom routes that don't conform to
@@ -381,14 +397,14 @@ these settings.
381
397
  * Disables [Rack::Protection][6] (can be reenabled with `enable :protection` or
382
398
  by manually `use`-ing the Rack::Protection middleware)
383
399
  * Disables static file routes (can be reenabled with `enable :static`)
384
- * Disables "classy" error pages (in favor of "classy" JSON:API error documents)
385
- * Adds an `:api_json` MIME-type (`Sinja::MIME_TYPE`)
400
+ * Disables "classy" error pages (in favor of "classy" {json:api} error documents)
401
+ * Adds an `:api_json` MIME-type (`application/vnd.api+json`)
386
402
  * Enforces strict checking of the `Accept` and `Content-Type` request headers
387
403
  * Sets the `Content-Type` response header to `:api_json` (can be overriden with
388
404
  the `content_type` helper)
389
- * Normalizes query parameters to reflect the features supported by JSON:API
390
- (this may be strictly enforced in future versions of Sinja)
391
- * Formats all errors to the proper JSON:API structure
405
+ * Normalizes and strictly enforces query parameters to reflect the features
406
+ supported by {json:api}
407
+ * Formats all errors to the proper {json:api} structure
392
408
  * Serializes all response bodies (including errors) to JSON
393
409
  * Modifies `halt` and `not_found` to raise exceptions instead of just setting
394
410
  the status code and body of the response
@@ -414,6 +430,13 @@ configure_jsonapi do |c|
414
430
  #c.default_has_one_roles = {}
415
431
  #c.default_has_many_roles = {}
416
432
 
433
+ # You can't set this directly; see "Query Parameters" below
434
+ #c.query_params = {
435
+ # :include=>[], :fields=>{}, :filter=>{}, :page=>{}, :sort=>[]
436
+ #}
437
+
438
+ #c.page_using = {} # see "Paging" below
439
+
417
440
  # Set the error logger used by Sinja
418
441
  #c.error_logger = ->(error_hash) { logger.error('sinja') { error_hash } }
419
442
 
@@ -428,10 +451,10 @@ end
428
451
 
429
452
  The above structures are mutable (e.g. you can do `c.conflict_exceptions <<
430
453
  FooError` and `c.serializer_opts[:meta] = { foo: 'bar' }`) until you call
431
- `freeze_jsonapi` to freeze the configuration store. You should always freeze
432
- the store after Sinja is configured and all your resources are defined.
454
+ `freeze_jsonapi` to freeze the configuration store. **You should always freeze
455
+ the store after Sinja is configured and all your resources are defined.**
433
456
 
434
- ### Resource Locator
457
+ ### Resource Locators
435
458
 
436
459
  Much of Sinja's advanced functionality (e.g. updating and destroying resources,
437
460
  relationship routes) is dependent upon its ability to locate the corresponding
@@ -490,13 +513,10 @@ below. Implicitly return the expected values as described below (as an array if
490
513
  necessary) or use the `next` keyword (instead of `return` or `break`) to exit
491
514
  the action helper. Return values marked with a question mark below may be
492
515
  omitted entirely. Any helper may additionally return an options hash to pass
493
- along to JSONAPI::Serializer.serialize (will be merged into the global
494
- `serializer_opts` described above).
495
-
496
- The `:include` (see "Side-Unloading Related Resources" below) and `:fields`
497
- query parameters are automatically passed through to JSONAPI::Serializers. The
498
- `:sort`, `:page`, and `:filter` query parameters must be handled manually (with
499
- one exception, discussed under "Coalesced Find Requests" below).
516
+ along to JSONAPI::Serializer.serialize (which will be merged into the global
517
+ `serializer_opts` described above). The `:include` (see "Side-Unloading Related
518
+ Resources" below) and `:fields` (for sparse fieldsets) query parameters are
519
+ automatically passed through to JSONAPI::Serializers.
500
520
 
501
521
  All arguments to action helpers are "tainted" and should be treated as
502
522
  potentially dangerous: IDs, attribute hashes, and (arrays of) [resource
@@ -521,6 +541,11 @@ Return an array of zero or more objects to serialize on the response.
521
541
  Take an ID and return the corresponding object (or `nil` if not found) to
522
542
  serialize on the response.
523
543
 
544
+ ##### `show_many {|ids| ..}` => Array
545
+
546
+ Take an array of IDs and return an equally-lengthed array of objects to
547
+ serialize on the response. See "Coalesced Find Requests" below.
548
+
524
549
  ##### `create {|attr, id| ..}` => id, Object?
525
550
 
526
551
  With client-generated IDs: Take a hash of (dedasherized) attributes and a
@@ -615,7 +640,9 @@ unless already missing) the relationships on `resource`. To serialize the
615
640
  updated linkage on the response, refresh or reload `resource` (if necessary)
616
641
  and return a truthy value.
617
642
 
618
- ### Action Helper Hooks &amp; Utilities
643
+ ## Advanced Usage
644
+
645
+ ### Action Helper Hooks & Utilities
619
646
 
620
647
  You may remove a previously-registered action helper with `remove_<action>`:
621
648
 
@@ -809,11 +836,159 @@ resource :foos do
809
836
  end
810
837
  ```
811
838
 
839
+ Please see the [demo-app](/demo-app) for more complete examples.
840
+
841
+ ### Query Parameters
842
+
843
+ The {json:api} specification states that any unhandled query parameters should
844
+ cause the request to abort with HTTP status 400. To enforce this requirement,
845
+ Sinja maintains a global "whitelist" of acceptable query parameters as well as
846
+ a per-route whitelist, and interrogates your application to see which features
847
+ it supports; for example, a route may allow a `filter` query parameter, but you
848
+ may not have defined a `filter` helper.
849
+
850
+ To allow a custom query parameter through, add it to the `query_params`
851
+ configurable with a `nil` value:
852
+
853
+ ```ruby
854
+ configure_jsonapi do |c|
855
+ c.query_params[:foo] = nil
856
+ end
857
+ ```
858
+
859
+ To let a custom route accept standard query parameters, add a `:qparams` route
860
+ condition:
861
+
862
+ ```ruby
863
+ get '/top10', qparams: [:include, :sort] do
864
+ # ..
865
+ end
866
+ ```
867
+
868
+ ### Working with Collections
869
+
870
+ #### Filtering
871
+
872
+ Allow clients to filter the collections returned by the `index` and `fetch`
873
+ action helpers by defining a `filter` helper in the appropriate scope that
874
+ takes a collection and a hash of `filter` query parameters (with its top-level
875
+ keys dedasherized and symbolized) and returns the filtered collection. You may
876
+ also set a `:filter_by` option on the action helper to an array of symbols
877
+ representing the "filter-able" fields for that resource.
878
+
879
+ For example, to implement simple equality filters using Sequel:
880
+
881
+ ```ruby
882
+ helpers do
883
+ def filter(collection, fields={})
884
+ collection.where(fields)
885
+ end
886
+ end
887
+
888
+ resource :posts do
889
+ index(filter_by: [:title, :type]) do
890
+ Foo # return a Sequel::Dataset (instead of an array of Sequel::Model instances)
891
+ end
892
+
893
+ has_many :comments do
894
+ fetch(filter_by: :status) do
895
+ resource.comments_dataset # return a Sequel::Dataset
896
+ end
897
+ end
898
+ end
899
+ ```
900
+
901
+ #### Sorting
902
+
903
+ Allow clients to sort the collections returned by the `index` and `fetch`
904
+ action helpers by defining a `sort` helper in the appropriate scope that takes
905
+ a collection and a hash of `sort` query parameters (with its top-level keys
906
+ dedasherized and symbolized) and returns the sorted collection. The hash values
907
+ are either `:asc` (to sort ascending) or `:desc` (to sort descending). You may
908
+ also set a `:sort_by` option on the action helper to an array of symbols
909
+ representing the "sort-able" fields for that resource.
910
+
911
+ For example, to implement sorting using Sequel:
912
+
913
+ ```ruby
914
+ helpers do
915
+ def sort(collection, fields={})
916
+ collection.order(*fields.map { |k, v| Sequel.send(v, k) })
917
+ end
918
+ end
919
+
920
+ resource :posts do
921
+ index(sort_by: :created_at) do
922
+ Foo # return a Sequel::Dataset (instead of an array of Sequel::Model instances)
923
+ end
924
+ end
925
+ ```
926
+
927
+ #### Paging
928
+
929
+ Allow clients to page the collections returned by the `index` and `fetch`
930
+ action helpers by defining a `page` helper in the appropriate scope that takes
931
+ a collection and a hash of `page` query parameters (with its top-level keys
932
+ dedasherized and symbolized) and returns the paged collection along with a
933
+ special nested hash used to build the paging links.
934
+
935
+ The top-level keys of the hash returned by this method must be members of the
936
+ set: {`:self`, `:first`, `:prev`, `:next`, `:last`}. The values of the hash are
937
+ hashes themselves containing the query parameters used to construct the
938
+ corresponding link. For example, the hash:
939
+
940
+ ```ruby
941
+ {
942
+ prev: {
943
+ number: 3,
944
+ size: 10
945
+ },
946
+ next: {
947
+ number: 5,
948
+ size: 10
949
+ }
950
+ }
951
+ ```
952
+
953
+ Could be used to build the following top-level links in the response document:
954
+
955
+ ```json
956
+ "links": {
957
+ "prev": "/posts?page[number]=3&page[size]=10",
958
+ "next": "/posts?page[number]=5&page[size]=10"
959
+ }
960
+ ```
961
+
962
+ You must also set the `page_using` configurable to a hash of symbols
963
+ representing the paging fields used in your application (for example, `:number`
964
+ and `:size` for the above example) along with their default values (or `nil`).
965
+ Please see the [Sequel helpers](/lib/sinja/helpers/sequel.rb) in this
966
+ repository for a detailed, working example.
967
+
968
+ #### Finalizing
969
+
970
+ If you need to perform any additional actions on a collection after it is
971
+ filtered, sorted, and/or paged, but before it is serialized, define a
972
+ `finalize` helper that takes a collection and returns the finalized collection.
973
+ For example, to convert Sequel datasets to arrays of models before
974
+ serialization:
975
+
976
+ ```ruby
977
+ helpers do
978
+ def finalize(collection)
979
+ collection.all
980
+ end
981
+ end
982
+ ```
983
+
984
+ (Note that in addition to finalizing Sequel datasets with `#all`, you should
985
+ also enable the `:tactical_eager_loading` plugin for the best compatibility
986
+ with JSONAPI::Serializers.)
987
+
812
988
  ### Conflicts
813
989
 
814
990
  If your database driver raises exceptions on constraint violations, you should
815
- specify which exception class(es) should be handled and return HTTP status code
816
- 409.
991
+ specify which exception class(es) should be handled and return HTTP status 409.
817
992
 
818
993
  For example, using [Sequel][13]:
819
994
 
@@ -826,7 +1001,7 @@ end
826
1001
  ### Validations
827
1002
 
828
1003
  If your ORM raises exceptions on validation errors, you should specify which
829
- exception class(es) should be handled and return HTTP status code 422, along
1004
+ exception class(es) should be handled and return HTTP status 422, along
830
1005
  with a formatter proc that transforms the exception object into an array of
831
1006
  two-element arrays containing the name or symbol of the attribute that failed
832
1007
  validation and the detailed errror message for that attribute.
@@ -843,9 +1018,9 @@ end
843
1018
  ### Missing Records
844
1019
 
845
1020
  If your database driver raises exceptions on missing records, you should
846
- specify which exception class(es) should be handled and return HTTP status code
847
- 404. This is particularly useful for relationship action helpers, which don't
848
- have access to a dedicated subresource locator.
1021
+ specify which exception class(es) should be handled and return HTTP status 404.
1022
+ This is particularly useful for relationship action helpers, which don't have
1023
+ access to a dedicated subresource locator.
849
1024
 
850
1025
  For example, using [Sequel][13]:
851
1026
 
@@ -904,7 +1079,7 @@ descendents will _not_ be automatically excluded.
904
1079
  Sinja works hard to DRY up your business logic. As mentioned above, when a
905
1080
  request comes in to create or update a resource and that request includes
906
1081
  relationships, Sinja will try to farm out the work to your defined relationship
907
- routes. Let's look at this example request from the JSON:API specification:
1082
+ routes. Let's look at this example request from the {json:api} specification:
908
1083
 
909
1084
  ```
910
1085
  POST /photos HTTP/1.1
@@ -946,7 +1121,7 @@ either `graft` or `create`.
946
1121
 
947
1122
  `create` and `update` are the only two action helpers that trigger sideloading;
948
1123
  `graft`, `merge`, and `clear` are the only action helpers invoked by
949
- sideloading. You must indicate which combinations are valid using the
1124
+ sideloading. You must indicate which combinations are valid using the
950
1125
  `:sideload_on` action helper option. (Note that if you want to sideload `merge`
951
1126
  on `update`, you must define a `clear` action helper as well.) For example:
952
1127
 
@@ -1074,16 +1249,21 @@ constraints on the join table.
1074
1249
 
1075
1250
  ### Coalesced Find Requests
1076
1251
 
1077
- If your JSON:API client coalesces find requests, the `show` action helper will
1252
+ If your {json:api} client coalesces find requests, the `show` action helper will
1078
1253
  be invoked once for each ID in the `:id` filter, and the resulting collection
1079
1254
  will be serialized on the response. Both query parameter syntaxes for arrays
1080
1255
  are supported: `?filter[id]=1,2` and `?filter[id][]=1&filter[id][]=2`. If any
1081
1256
  ID is not found (i.e. `show` returns `nil`), the route will halt with HTTP
1082
- status code 404.
1257
+ status 404.
1258
+
1259
+ Optionally, to reduce round trips to the database, you may define a "special"
1260
+ `show_many` action helper that takes an array of IDs to show. It does not take
1261
+ `:roles` or any other options and will only be invoked if the current user has
1262
+ access to `show`. This feature is still experimental.
1083
1263
 
1084
1264
  ### Patchless Clients
1085
1265
 
1086
- JSON:API [recommends][23] supporting patchless clients by using the
1266
+ {json:api} [recommends][23] supporting patchless clients by using the
1087
1267
  `X-HTTP-Method-Override` request header to coerce a `POST` into a `PATCH`. To
1088
1268
  support this in Sinja, add the Sinja::MethodOverride middleware (which is a
1089
1269
  stripped-down version of [Rack::MethodOverride][24]) into your application (or
@@ -1102,6 +1282,8 @@ class MyApp < Sinatra::Base
1102
1282
  end
1103
1283
  ```
1104
1284
 
1285
+ ## Application Concerns
1286
+
1105
1287
  ### Sinja or Sinatra::JSONAPI
1106
1288
 
1107
1289
  Everything is dual-namespaced under both Sinatra::JSONAPI and Sinja, and Sinja
@@ -1179,6 +1361,26 @@ class App < Sinatra::Base
1179
1361
  end
1180
1362
  ```
1181
1363
 
1364
+ ### Testing
1365
+
1366
+ The short answer to "How do I test my Sinja application?" is "Like you would
1367
+ any other Sinatra application." Unfortunately, the testing story isn't quite
1368
+ *there* yet for Sinja. I think leveraging something like [Munson][27] or
1369
+ [json_api_client][28] is probably the best bet for integration testing, but
1370
+ unfortunately both projects are rife with broken and/or missing critical
1371
+ features. And until we can solve the general code organization problem (most
1372
+ likely with patches to Sinatra), it will remain difficult to isolate action
1373
+ helpers and other artifacts for unit testing.
1374
+
1375
+ Sinja's own test suite is based on [Rack::Test][29] (plus some ugly kludges).
1376
+ I wouldn't recommend it but it might be a good place to start looking for
1377
+ ideas. It leverages the [demo-app](/demo-app) with Sequel and an in-memory
1378
+ database to perform integration testing of Sinja's various features under
1379
+ MRI/YARV and JRuby. The goal is to free you from worrying about whether your
1380
+ applications will behave according to the {json:api} spec (as long as you
1381
+ follow the usage documented in this README) and focus on testing your business
1382
+ logic.
1383
+
1182
1384
  ## Development
1183
1385
 
1184
1386
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
@@ -1226,3 +1428,7 @@ License](http://opensource.org/licenses/MIT).
1226
1428
  [23]: http://jsonapi.org/recommendations/#patchless-clients
1227
1429
  [24]: http://www.rubydoc.info/github/rack/rack/Rack/MethodOverride
1228
1430
  [25]: http://www.sinatrarb.com/mustermann/
1431
+ [26]: https://github.com/tiagopog/jsonapi-utils
1432
+ [27]: https://github.com/coryodaniel/munson
1433
+ [28]: https://github.com/chingor13/json_api_client
1434
+ [29]: https://github.com/brynary/rack-test