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 +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
|
-
[![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
|
-
[
|
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
|