sinja 1.0.0.pre2 → 1.1.0.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +313 -107
- data/demo-app/README.md +58 -0
- data/demo-app/app.rb +8 -4
- data/demo-app/boot.rb +3 -1
- data/demo-app/classes/author.rb +18 -15
- data/demo-app/classes/comment.rb +11 -9
- data/demo-app/classes/post.rb +21 -16
- data/demo-app/classes/tag.rb +12 -8
- data/demo-app/database.rb +4 -6
- data/lib/sinja/config.rb +85 -76
- data/lib/sinja/helpers/relationships.rb +5 -3
- data/lib/sinja/helpers/sequel.rb +43 -0
- data/lib/sinja/helpers/serializers.rb +39 -20
- data/lib/sinja/relationship_routes/has_many.rb +9 -5
- data/lib/sinja/relationship_routes/has_one.rb +4 -4
- data/lib/sinja/resource.rb +19 -21
- data/lib/sinja/resource_routes.rb +33 -20
- data/lib/sinja/version.rb +1 -1
- data/lib/sinja.rb +137 -48
- data/sinja.gemspec +1 -1
- metadata +4 -4
- data/demo-app/test.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4491b24eba74ba902735fc26ff679b074bc5a76c
|
4
|
+
data.tar.gz: 54f55fd1b953b7dd31b5bb911386bb6f1bf83419
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
[](https://travis-ci.org/mwpastore/sinja)
|
4
3
|
[](https://badge.fury.io/rb/sinja)
|
4
|
+
[](https://travis-ci.org/mwpastore/sinja)
|
5
5
|
|
6
6
|
Sinja is a [Sinatra][1] [extension][10] for quickly building [RESTful][11],
|
7
|
-
[
|
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
|
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
|
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
|
-
- [
|
24
|
-
|
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
|
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
|
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
|
-
|
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
|
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
|
-
*
|
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
|
-
*
|
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
|
160
|
-
designed to work with [Rails][16] and
|
161
|
-
(although they may work with [Sequel][13]
|
162
|
-
[`:active_model` plugin][15]). Otherwise,
|
163
|
-
[Roda][20], or [Grape][19] with
|
164
|
-
|
165
|
-
boilerplate
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
`
|
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
|
-
|
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
|
-
|
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
|
322
|
-
|
323
|
-
| Feature | JR
|
324
|
-
| :-------------- |
|
325
|
-
| Serializer | Built-in
|
326
|
-
| Framework | Rails
|
327
|
-
| Routing | ActionDispatch::Routing
|
328
|
-
| Caching | ActiveSupport::Cache
|
329
|
-
| ORM | ActiveRecord/ActiveModel
|
330
|
-
| Authorization | [Pundit][9]
|
331
|
-
| Immutability | `immutable` method
|
332
|
-
| Fetchability | `fetchable_fields` method
|
333
|
-
| Creatability | `creatable_fields` method
|
334
|
-
| Updatability | `updatable_fields` method
|
335
|
-
| Sortability | `sortable_fields` method
|
336
|
-
| Default sorting | `default_sort` method
|
337
|
-
| Context | `context` method
|
338
|
-
| Attributes | Define in Model and Resource
|
339
|
-
| Formatting |
|
340
|
-
| Relationships | Define in Model and Resource
|
341
|
-
| Filters | `filter(s)` keywords
|
342
|
-
| Default filters |
|
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
|
-
|
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
|
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
|
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-
|
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"
|
385
|
-
* Adds an `:api_json` 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
|
390
|
-
|
391
|
-
* Formats all errors to the proper
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
847
|
-
|
848
|
-
|
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
|
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.
|
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
|
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
|
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
|
-
|
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
|