sinja 0.2.0.beta2 → 1.0.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: 888cd268dead23fb968c04f789e45ba33530f859
4
- data.tar.gz: ec062d1583793fa03f88338eec33f2f3883f754d
3
+ metadata.gz: 1ca76b5776f261ca35dbf3d7728b8f6c5e17b720
4
+ data.tar.gz: eb1238aab2e77a50558b6445a0cd5055e3fe9f39
5
5
  SHA512:
6
- metadata.gz: e78db5f95b48107ac6fcd3442c99fbd9662d8ea63cdea41d89bd5507474afeb84e45384f2ad1a6c4eecb802182e2d3fde40a71060361c7a621f5393255011520
7
- data.tar.gz: c40082498c163ad4dfe297b405109546fe1de879716ca2c2fab754e4c17f649aa807354ad6c8fbacd8dc1339d85c1e45ed7582d8d7f77bd5f1c9e341b369a8fc
6
+ metadata.gz: 9134e5396394e7b0f92305e88df7777e4abbd39e0f1b0da839e5382629dd59d97bdd41c72cc45bd2455578bc332d2381b6aa49548653333b21f7965fe4db5e28
7
+ data.tar.gz: 6907c31c9ecbe601b2eb1f93accedf39288d2cf20be57df6e96a295a1baddc8dbc4f662d1a50e367eb0806718d69e8daf8d77fe54e971ea3a7c1e6ccc89ff71f
data/.gitignore CHANGED
@@ -1,9 +1,10 @@
1
1
  /.bundle/
2
2
  /.yardoc
3
- /Gemfile.lock
3
+ Gemfile.lock
4
4
  /_yardoc/
5
5
  /coverage/
6
6
  /doc/
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /test*.rb
@@ -1,5 +1,15 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.1
5
- before_install: gem install bundler -v 1.13.2
4
+ - 2.3.3
5
+ - ruby-head
6
+ - jruby-9.1.6.0
7
+ - jruby-head
8
+ env:
9
+ - sinatra=1.4.7 rails=4.2.7.1
10
+ - sinatra=2.0.0.beta2 rails=5.0.0.1
11
+ jdk:
12
+ - oraclejdk8
13
+ before_install:
14
+ - gem uninstall bundler
15
+ - gem install bundler -v 1.11.2
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+ gem 'munson', '~> 0.3',
5
+ :git=>'https://github.com/mwpastore/munson', :branch=>'develop'
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Build Status](https://travis-ci.org/mwpastore/sinja.svg?branch=master)](https://travis-ci.org/mwpastore/sinja)
4
4
  [![Gem Version](https://badge.fury.io/rb/sinja.svg)](https://badge.fury.io/rb/sinja)
5
5
 
6
- Sinja is a [Sinatra 2.0][1] [extension][10] for quickly building [RESTful][11],
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
@@ -13,10 +13,6 @@ 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
14
  the JSON:API specification is).
15
15
 
16
- **CAVEAT EMPTOR: This gem is still very new and under active development. The
17
- API is mostly stable, but there still may be significant breaking changes. It
18
- has not yet been thoroughly tested or vetted in a production environment.**
19
-
20
16
  <!-- START doctoc generated TOC please keep comment here to allow auto update -->
21
17
  <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
22
18
 
@@ -32,6 +28,7 @@ has not yet been thoroughly tested or vetted in a production environment.**
32
28
  - [Configuration](#configuration)
33
29
  - [Sinatra](#sinatra)
34
30
  - [Sinja](#sinja)
31
+ - [Resource Locator](#resource-locator)
35
32
  - [Action Helpers](#action-helpers)
36
33
  - [`resource`](#resource)
37
34
  - [`index {..}` => Array](#index---array)
@@ -49,14 +46,24 @@ has not yet been thoroughly tested or vetted in a production environment.**
49
46
  - [`clear {..}` => TrueClass?](#clear---trueclass)
50
47
  - [`merge {|rios| ..}` => TrueClass?](#merge-rios---trueclass)
51
48
  - [`subtract {|rios| ..}` => TrueClass?](#subtract-rios---trueclass)
49
+ - [Action Helper Hooks &amp; Utilities](#action-helper-hooks-amp-utilities)
52
50
  - [Authorization](#authorization)
53
- - [`default_roles` configurable](#default_roles-configurable)
51
+ - [`default_roles` configurables](#default_roles-configurables)
54
52
  - [`:roles` Action Helper option](#roles-action-helper-option)
55
53
  - [`role` helper](#role-helper)
56
54
  - [Conflicts](#conflicts)
55
+ - [Validations](#validations)
56
+ - [Missing Records](#missing-records)
57
57
  - [Transactions](#transactions)
58
+ - [Side-Unloading Related Resources](#side-unloading-related-resources)
59
+ - [Side-Loading Relationships](#side-loading-relationships)
60
+ - [Avoiding Null Foreign Keys](#avoiding-null-foreign-keys)
61
+ - [Many-to-One](#many-to-one)
62
+ - [One-to-Many](#one-to-many)
63
+ - [Many-to-Many](#many-to-many)
58
64
  - [Coalesced Find Requests](#coalesced-find-requests)
59
- - [Module Namespaces](#module-namespaces)
65
+ - [Patchless Clients](#patchless-clients)
66
+ - [Sinja or Sinatra::JSONAPI](#sinja-or-sinatrajsonapi)
60
67
  - [Code Organization](#code-organization)
61
68
  - [Development](#development)
62
69
  - [Contributing](#contributing)
@@ -71,20 +78,17 @@ require 'sinatra'
71
78
  require 'sinatra/jsonapi'
72
79
 
73
80
  resource :posts do
74
- index do
75
- Post.all
76
- end
77
-
78
81
  show do |id|
79
82
  Post[id.to_i]
80
83
  end
81
84
 
85
+ index do
86
+ Post.all
87
+ end
88
+
82
89
  create do |attr|
83
90
  Post.create(attr)
84
91
  end
85
-
86
- has_one :author
87
- has_many :comments
88
92
  end
89
93
 
90
94
  freeze_jsonapi
@@ -94,14 +98,13 @@ Assuming the presence of a `Post` model and serializer, running the above
94
98
  "classic"-style Sinatra application would enable the following endpoints (with
95
99
  all other JSON:API endpoints returning 404 or 405):
96
100
 
97
- * `GET /posts`
98
101
  * `GET /posts/<id>`
99
- * `GET /posts/<id>/author`
100
- * `GET /posts/<id>/comments`
101
- * `GET /posts/<id>/relationships/author`
102
- * `GET /posts/<id>/relationships/comments`
102
+ * `GET /posts`
103
103
  * `POST /posts`
104
104
 
105
+ The resource locator and other action helpers, documented below, enable other
106
+ endpoints.
107
+
105
108
  Of course, "modular"-style Sinatra aplications require you to register the
106
109
  extension:
107
110
 
@@ -144,9 +147,12 @@ $ gem install sinja
144
147
 
145
148
  * ORM-agnostic
146
149
  * Role-based authorization
147
- * To-one and to-many relationships
148
- * Side-loaded relationships on resource creation
149
- * Conflict (constraint violation) handling
150
+ * To-one and to-many relationships and related resources
151
+ * Side-loaded relationships on resource creation and update
152
+ * Error-handling
153
+ * Conflicts (constraint violations)
154
+ * Missing records
155
+ * Validation failures
150
156
  * Plus all the features of JSONAPI::Serializers!
151
157
 
152
158
  Its main competitors in the Ruby space are [ActiveModelSerializers][12] (AMS)
@@ -164,9 +170,10 @@ on the resource definitions.
164
170
  The "power" of implementing this functionality as a Sinatra extension is that
165
171
  all of Sinatra's usual features are available within your resource definitions.
166
172
  The action helpers blocks get compiled into Sinatra helpers, and the
167
- `resource`, `has_one`, and `has_many` keywords simply build
168
- [Sinatra::Namespace][21] blocks. You can manage caching directives, set
169
- headers, and even `halt` (or `not_found`) out of action helpers as desired.
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.
170
177
 
171
178
  ```ruby
172
179
  class App < Sinatra::Base
@@ -178,14 +185,14 @@ class App < Sinatra::Base
178
185
  # <- This is a Sinatra::Namespace block.
179
186
 
180
187
  show do |id|
181
- # <- This is a Sinatra helper, scoped to the resource namespace.
188
+ # <- This is a "special" Sinatra helper, scoped to the resource namespace.
182
189
  end
183
190
 
184
191
  has_one :author do
185
192
  # <- This is a Sinatra::Namespace block, nested under the resource namespace.
186
193
 
187
194
  pluck do
188
- # <- This is a Sinatra helper, scoped to the nested namespace.
195
+ # <- This is a "special" Sinatra helper, scoped to the nested namespace.
189
196
  end
190
197
  end
191
198
  end
@@ -217,8 +224,14 @@ class App < Sinatra::Base
217
224
  get('/status', provides: :json) { 'OK' }
218
225
 
219
226
  resource :books do
227
+ helpers do
228
+ def find(id)
229
+ Book[id.to_i]
230
+ end
231
+ end
232
+
220
233
  show do |id|
221
- book = Book[id.to_i]
234
+ book = find(id)
222
235
  not_found "Book #{id} not found!" unless book
223
236
  headers 'X-ISBN'=>book.isbn
224
237
  last_modified book.updated_at
@@ -243,6 +256,8 @@ class App < Sinatra::Base
243
256
 
244
257
  # define a custom /books/top10 route
245
258
  get '/top10' do
259
+ halt 403 unless can?(:index) # restrict access to those with index rights
260
+
246
261
  serialize_models Book.where{}.reverse_order(:recent_sales).limit(10).all
247
262
  end
248
263
  end
@@ -253,6 +268,11 @@ end
253
268
 
254
269
  #### Public APIs
255
270
 
271
+ **can?**
272
+ : Takes the symbol of an action helper and returns true if the current user has
273
+ access to call that action helper for the current resource using the `role`
274
+ helper and role definitions detailed under "Authorization" below.
275
+
256
276
  **data**
257
277
  : Returns the `data` key of the deserialized request payload (with symbolized
258
278
  names).
@@ -282,6 +302,9 @@ end
282
302
  **dedasherize_names**
283
303
  : Takes a hash and returns the hash with its keys dedasherized (deeply).
284
304
 
305
+ **sideloaded?**
306
+ : Returns true if the request was invoked from another action helper.
307
+
285
308
  ### Performance
286
309
 
287
310
  Although there is some heavy metaprogramming happening at boot time, the end
@@ -333,11 +356,11 @@ You'll need a database schema and models (using the engine and ORM of your
333
356
  choice) and [serializers][3] to get started. Create a new Sinatra application
334
357
  (classic or modular) to hold all your JSON:API endpoints and (if modular)
335
358
  register this extension. Instead of defining routes with `get`, `post`, etc. as
336
- you normally would, simply define `resource` blocks with action helpers and
337
- `has_one` and `has_many` relationship blocks (with their own action helpers).
338
- Sinja will draw and enable the appropriate routes based on the defined
339
- resources, relationships, and action helpers. Other routes will return the
340
- appropriate HTTP status codes: 403, 404, or 405.
359
+ you normally would, define `resource` blocks with action helpers and `has_one`
360
+ and `has_many` relationship blocks (with their own action helpers). Sinja will
361
+ draw and enable the appropriate routes based on the defined resources,
362
+ relationships, and action helpers. Other routes will return the appropriate
363
+ HTTP status codes: 403, 404, or 405.
341
364
 
342
365
  ### Configuration
343
366
 
@@ -350,11 +373,11 @@ elsewhere (e.g. with [Rack::URLMap][4]), or host them as a completely separate
350
373
  web service. It may not be feasible to have custom routes that don't conform to
351
374
  these settings.
352
375
 
353
- * Registers [Sinatra::Namespace][21]
376
+ * Registers [Sinatra::Namespace][21] and [Mustermann][25]
354
377
  * Disables [Rack::Protection][6] (can be reenabled with `enable :protection` or
355
378
  by manually `use`-ing the Rack::Protection middleware)
356
379
  * Disables static file routes (can be reenabled with `enable :static`)
357
- * Sets `:show_exceptions` to `:after_handler`
380
+ * Disables "classy" error pages (in favor of "classy" JSON:API error documents)
358
381
  * Adds an `:api_json` MIME-type (`Sinja::MIME_TYPE`)
359
382
  * Enforces strict checking of the `Accept` and `Content-Type` request headers
360
383
  * Sets the `Content-Type` response header to `:api_json` (can be overriden with
@@ -363,6 +386,8 @@ these settings.
363
386
  (this may be strictly enforced in future versions of Sinja)
364
387
  * Formats all errors to the proper JSON:API structure
365
388
  * Serializes all response bodies (including errors) to JSON
389
+ * Modifies `halt` and `not_found` to raise exceptions instead of just setting
390
+ the status code and body of the response
366
391
 
367
392
  #### Sinja
368
393
 
@@ -374,22 +399,84 @@ their defaults shown):
374
399
  configure_jsonapi do |c|
375
400
  #c.conflict_exceptions = [] # see "Conflicts" below
376
401
 
377
- #c.default_roles = {} # see "Authorization" below
402
+ # see "Validations" below
403
+ #c.validation_exceptions = []
404
+ #c.validation_formatter = ->{ [] }
405
+
406
+ #c.not_found_exceptions = [] # see "Missing Records" below
378
407
 
379
- # Set the "progname" used by Sinja when accessing the logger
380
- #c.logger_progname = 'sinja'
408
+ # see "Authorization" below
409
+ #c.default_roles = {}
410
+ #c.default_has_one_roles = {}
411
+ #c.default_has_many_roles = {}
412
+
413
+ # Set the error logger used by Sinja
414
+ #c.error_logger = ->(error_hash) { logger.error('sinja') { error_hash } }
381
415
 
382
416
  # A hash of options to pass to JSONAPI::Serializer.serialize
383
417
  #c.serializer_opts = {}
384
418
 
385
419
  # JSON methods to use when serializing response bodies and errors
386
420
  #c.json_generator = development? ? :pretty_generate : :generate
387
- #c.json_error_generator = development? ? :pretty_generate : :fast_generate
421
+ #c.json_error_generator = development? ? :pretty_generate : :generate
388
422
  end
389
423
  ```
390
424
 
391
- After Sinja is configured and all your resources are defined, you should call
392
- `freeze_jsonapi` to freeze the configuration store.
425
+ The above structures are mutable (e.g. you can do `c.conflict_exceptions <<
426
+ FooError` and `c.serializer_opts[:meta] = { foo: 'bar' }`) until you call
427
+ `freeze_jsonapi` to freeze the configuration store. You should always freeze
428
+ the store after Sinja is configured and all your resources are defined.
429
+
430
+ ### Resource Locator
431
+
432
+ Much of Sinja's advanced functionality (e.g. updating and destroying resources,
433
+ relationship routes) is dependent upon its ability to locate the corresponding
434
+ resource for a request. To enable these features, define an ordinary helper
435
+ method named `find` in your resource definition that takes a single ID argument
436
+ and returns the corresponding object. You can, of course, use this helper
437
+ method elsewhere in your application, such as in your `show` action helper.
438
+
439
+ ```ruby
440
+ resource :posts
441
+ helpers do
442
+ def find(id)
443
+ Post[id.to_i]
444
+ end
445
+ end
446
+
447
+ show do |id|
448
+ next find(id), include: 'comments'
449
+ end
450
+ end
451
+ ```
452
+
453
+ * What's the difference between `find` and `show`?
454
+
455
+ You can think of it as the difference between a Model and a View: `find`
456
+ retrieves the record, `show` presents it.
457
+
458
+ * Why separate the two? Why not use `show` as the resource locator?
459
+
460
+ For a variety of reasons, but primarily because the access rights for viewing
461
+ a resource are not always the same as those for updating and/or destroying a
462
+ resource, and vice-versa. For example, a user may be able to delete a
463
+ resource or subtract a relationship link without being able to see the
464
+ resource or its relationship linkage.
465
+
466
+ * How do I control access to the resource locator?
467
+
468
+ You don't. Instead, control access to the action helpers that use it:
469
+ `update`, `destroy`, and all of the relationship action helpers such as
470
+ `pluck` and `fetch`.
471
+
472
+ * What happens if I define an action helper that requires a resource locator,
473
+ but no resource locator?
474
+
475
+ Sinja will act as if you had not defined the action helper.
476
+
477
+ As a bit of syntactic sugar, if you define a `find` helper and subsequently
478
+ call `show` without a block, Sinja will generate a `show` action helper that
479
+ delegates to `find`.
393
480
 
394
481
  ### Action Helpers
395
482
 
@@ -402,27 +489,22 @@ omitted entirely. Any helper may additionally return an options hash to pass
402
489
  along to JSONAPI::Serializer.serialize (will be merged into the global
403
490
  `serializer_opts` described above).
404
491
 
405
- The `:include` and `:fields` query parameters are automatically passed through
406
- to JSONAPI::Serializers. You may also use the special `:exclude` option to
407
- prevent specific relationships from being included in the response. This
408
- accepts the same formats as JSONAPI::Serializers does for `:include`. If you
409
- exclude a relationship, any sub-relationships will also be excluded. The
492
+ The `:include` (see "Side-Unloading Related Resources" below) and `:fields`
493
+ query parameters are automatically passed through to JSONAPI::Serializers. The
410
494
  `:sort`, `:page`, and `:filter` query parameters must be handled manually (with
411
- the exception of the `:id` filter, discussed under "Coalesced Find Requests"
412
- below).
495
+ one exception, discussed under "Coalesced Find Requests" below).
413
496
 
414
497
  All arguments to action helpers are "tainted" and should be treated as
415
- potentially dangerous: IDs, attribute hashes, and [resource identifier
416
- objects][22].
417
-
418
- Finally, some routes will automatically invoke the `show` action helper on your
419
- behalf and make the selected resource available to other action helpers as
420
- `resource`. You've already told Sinja how to find a resource by ID, so why
421
- repeat yourself? For example, the `PATCH /<name>/:id` route looks up the
422
- resource with that ID using the `show` action helper and makes it available to
423
- the `update` action helper as `resource`. The same goes for the `DELETE
424
- /<name>/:id` route and the `destroy` action helper, and all of the `has_one`
425
- and `has_many` action helpers.
498
+ potentially dangerous: IDs, attribute hashes, and (arrays of) [resource
499
+ identifier object][22] hashes.
500
+
501
+ Finally, some routes will automatically invoke the resource locator on your
502
+ behalf and make the selected resource available to the corresponding action
503
+ helper(s) as `resource`. For example, the `PATCH /<name>/:id` route looks up
504
+ the resource with that ID using the `find` resource locator and makes it
505
+ available to the `update` action helper as `resource`. The same goes for the
506
+ `DELETE /<name>/:id` route and the `destroy` action helper, and all of the
507
+ `has_one` and `has_many` action helpers.
426
508
 
427
509
  #### `resource`
428
510
 
@@ -452,14 +534,16 @@ given resource block.)
452
534
  ##### `update {|attr| ..}` => Object?
453
535
 
454
536
  Take a hash of (dedasherized) attributes, update `resource`, and optionally
455
- return the updated resource.
537
+ return the updated resource. **Requires a resource locator.**
456
538
 
457
539
  ##### `destroy {..}`
458
540
 
459
- Delete or destroy `resource`.
541
+ Delete or destroy `resource`. **Requires a resource locator.**
460
542
 
461
543
  #### `has_one`
462
544
 
545
+ **Requires a resource locator.**
546
+
463
547
  ##### `pluck {..}` => Object
464
548
 
465
549
  Return the related object vis-&agrave;-vis `resource` to serialize on the
@@ -471,7 +555,7 @@ Remove the relationship from `resource`. To serialize the updated linkage on
471
555
  the response, refresh or reload `resource` (if necessary) and return a truthy
472
556
  value.
473
557
 
474
- For example, using Sequel:
558
+ For example, using [Sequel][13]:
475
559
 
476
560
  ```ruby
477
561
  has_one :qux do
@@ -484,12 +568,14 @@ end
484
568
 
485
569
  ##### `graft {|rio| ..}` => TrueClass?
486
570
 
487
- Take a [resource identifier object][22] and update the relationship on
571
+ Take a [resource identifier object][22] hash and update the relationship on
488
572
  `resource`. To serialize the updated linkage on the response, refresh or reload
489
573
  `resource` (if necessary) and return a truthy value.
490
574
 
491
575
  #### `has_many`
492
576
 
577
+ **Requires a resource locator.**
578
+
493
579
  ##### `fetch {..}` => Array
494
580
 
495
581
  Return an array of related objects vis-&agrave;-vis `resource` to serialize on
@@ -501,7 +587,7 @@ Remove all relationships from `resource`. To serialize the updated linkage on
501
587
  the response, refresh or reload `resource` (if necessary) and return a truthy
502
588
  value.
503
589
 
504
- For example, using Sequel:
590
+ For example, using [Sequel][13]:
505
591
 
506
592
  ```ruby
507
593
  has_many :bars do
@@ -513,17 +599,66 @@ end
513
599
 
514
600
  ##### `merge {|rios| ..}` => TrueClass?
515
601
 
516
- Take an array of [resource identifier objects][22] and update (add unless
602
+ Take an array of [resource identifier object][22] hashes and update (add unless
517
603
  already present) the relationships on `resource`. To serialize the updated
518
604
  linkage on the response, refresh or reload `resource` (if necessary) and return
519
605
  a truthy value.
520
606
 
521
607
  ##### `subtract {|rios| ..}` => TrueClass?
522
608
 
523
- Take an array of [resource identifier objects][22] and update (remove unless
524
- already missing) the relationships on `resource`. To serialize the updated
525
- linkage on the response, refresh or reload `resource` (if necessary) and return
526
- a truthy value.
609
+ Take an array of [resource identifier object][22] hashes and update (remove
610
+ unless already missing) the relationships on `resource`. To serialize the
611
+ updated linkage on the response, refresh or reload `resource` (if necessary)
612
+ and return a truthy value.
613
+
614
+ ### Action Helper Hooks &amp; Utilities
615
+
616
+ You may remove a previously-registered action helper with `remove_<action>`:
617
+
618
+ ```ruby
619
+ resource :foos do
620
+ index do
621
+ # ..
622
+ end
623
+
624
+ remove_index
625
+ end
626
+ ```
627
+
628
+ You may invoke an action helper keyword without a block to modify the options
629
+ (i.e. roles) of a previously-registered action helper while preseving the
630
+ existing behavior:
631
+
632
+ ```ruby
633
+ resource :bars do
634
+ show do |id|
635
+ # ..
636
+ end
637
+
638
+ show(roles: :admin) # restrict the above action helper to the `admin' role
639
+ end
640
+ ```
641
+
642
+ You may define an ordinary helper method named `before_<action>` (in the
643
+ resource or relationship scope or any parent scopes) that takes the same
644
+ arguments as the corresponding block:
645
+
646
+ ```ruby
647
+ helpers do
648
+ def before_create(attr)
649
+ halt 400 unless valid_key?(attr.delete(:special_key))
650
+ end
651
+ end
652
+
653
+ resource :quxes do
654
+ create do |attr|
655
+ attr.key?(:special_key) # => false
656
+ end
657
+ end
658
+ ```
659
+
660
+ Any changes made to attribute hashes or (arrays of) resource identifier object
661
+ hashes in a `before` hook will be persisted to the action helper.
527
662
 
528
663
  ### Authorization
529
664
 
@@ -537,7 +672,7 @@ Users can be in one or more roles, and action helpers can be restricted to one
537
672
  or more roles for maximum flexibility. There are three main components to the
538
673
  scheme:
539
674
 
540
- #### `default_roles` configurable
675
+ #### `default_roles` configurables
541
676
 
542
677
  You set the default roles for the entire Sinja application in the top-level
543
678
  configuration. Action helpers without any default roles are unrestricted by
@@ -545,20 +680,24 @@ default.
545
680
 
546
681
  ```ruby
547
682
  configure_jsonapi do |c|
683
+ # Resource roles
548
684
  c.default_roles = {
549
- # Resource roles
550
685
  index: :user,
551
686
  show: :user,
552
687
  create: :admin,
553
688
  update: :admin,
554
- destroy: :super,
689
+ destroy: :super
690
+ }
555
691
 
556
- # To-one relationship roles
692
+ # To-one relationship roles
693
+ c.default_has_one_roles = {
557
694
  pluck: :user,
558
695
  prune: :admin,
559
- graft: :admin,
696
+ graft: :admin
697
+ }
560
698
 
561
- # To-many relationship roles
699
+ # To-many relationship roles
700
+ c.default_has_many_roles = {
562
701
  fetch: :user,
563
702
  clear: :admin,
564
703
  merge: :admin,
@@ -569,21 +708,21 @@ end
569
708
 
570
709
  #### `:roles` Action Helper option
571
710
 
572
- To override the default roles for any given action helper, simply specify a
573
- `:roles` option when defining it. To remove all restrictions from an action
574
- helper, set `:roles` to an empty array. For example, to manage access to
575
- `show` at different levels of granularity (with the above `default_roles`):
711
+ To override the default roles for any given action helper, specify a `:roles`
712
+ option when defining it. To remove all restrictions from an action helper, set
713
+ `:roles` to an empty array. For example, to manage access to `show` at
714
+ different levels of granularity (with the above default roles):
576
715
 
577
716
  ```ruby
578
717
  resource :foos do
579
718
  show do
580
- # any logged-in user (with the :user role) can access /foos/:id
719
+ # any logged-in user (with the `user' role) can access /foos/:id
581
720
  end
582
721
  end
583
722
 
584
723
  resource :bars do
585
724
  show(roles: :admin) do
586
- # only logged-in users with the :admin role can access /bars/:id
725
+ # only logged-in users with the `admin' role can access /bars/:id
587
726
  end
588
727
  end
589
728
 
@@ -599,8 +738,8 @@ end
599
738
  Finally, define a `role` helper in your application that returns the user's
600
739
  role(s) (if any). You can handle login failures in your middleware, elsewhere
601
740
  in the application (i.e. a `before` filter), or within the helper, either by
602
- halting or raising an error or by simply letting Sinja halt 403 on restricted
603
- action helpers when `role` returns `nil` (the default behavior).
741
+ raising an error or by letting Sinja raise an error on restricted action
742
+ helpers when `role` returns `nil` (the default behavior).
604
743
 
605
744
  ```ruby
606
745
  helpers do
@@ -614,33 +753,101 @@ end
614
753
  ```
615
754
 
616
755
  If you need more fine-grained control, for example if your action helper logic
617
- varies by the user's role, you can use a simple switch statement along with the
618
- `RoleList` utility class:
756
+ varies by the user's role, you can use a switch statement on `role` along with
757
+ the `Sinja::Roles` utility class:
619
758
 
620
759
  ```ruby
621
- index(roles: []) do
760
+ index(roles: [:user, :admin, :super]) do
622
761
  case role
623
- when RoleList[:user]
624
- # logic specific to user role
625
- when RoleList[:admin, :super]
762
+ when Sinja::Roles[:user]
763
+ # logic specific to the `user' role
764
+ when Sinja::Roles[:admin, :super]
626
765
  # logic specific to administrative roles
627
- else
628
- halt 403, 'Access denied!'
629
766
  end
630
767
  end
631
768
  ```
632
769
 
770
+ Or use the `role?` helper:
771
+
772
+ ```ruby
773
+ show do |id|
774
+ exclude = []
775
+ exclude << 'secrets' unless role?(:admin)
776
+
777
+ next find(id), exclude: exclude
778
+ end
779
+ ```
780
+
781
+ You can append resource- or even relationship-specific roles by defining a
782
+ nested helper and calling `super` (keeping in mind that `resource` may be
783
+ `nil`).
784
+
785
+ ```ruby
786
+ helpers do
787
+ def role
788
+ [:user] if logged_in_user
789
+ end
790
+ end
791
+
792
+ resource :foos do
793
+ helpers do
794
+ def role
795
+ if resource&.owner == logged_in_user
796
+ [*super].push(:owner)
797
+ else
798
+ super
799
+ end
800
+ end
801
+ end
802
+
803
+ create(roles: :user) { |attr| .. }
804
+ update(roles: :owner) { |attr| .. }
805
+ end
806
+ ```
807
+
633
808
  ### Conflicts
634
809
 
635
810
  If your database driver raises exceptions on constraint violations, you should
636
811
  specify which exception class(es) should be handled and return HTTP status code
637
812
  409.
638
813
 
639
- For example, using Sequel:
814
+ For example, using [Sequel][13]:
815
+
816
+ ```ruby
817
+ configure_jsonapi do |c|
818
+ c.conflict_exceptions << Sequel::ConstraintViolation
819
+ end
820
+ ```
821
+
822
+ ### Validations
823
+
824
+ If your ORM raises exceptions on validation errors, you should specify which
825
+ exception class(es) should be handled and return HTTP status code 422, along
826
+ with a formatter proc that transforms the exception object into an array of
827
+ two-element arrays containing the name or symbol of the attribute that failed
828
+ validation and the detailed errror message for that attribute.
829
+
830
+ For example, using [Sequel][13]:
831
+
832
+ ```ruby
833
+ configure_jsonapi do |c|
834
+ c.validation_exceptions << Sequel::ValidationFailed
835
+ c.validation_formatter = ->(e) { e.errors.keys.zip(e.errors.full_messages) }
836
+ end
837
+ ```
838
+
839
+ ### Missing Records
840
+
841
+ If your database driver raises exceptions on missing records, you should
842
+ specify which exception class(es) should be handled and return HTTP status code
843
+ 404. This is particularly useful for relationship action helpers, which don't
844
+ have access to a dedicated subresource locator.
845
+
846
+ For example, using [Sequel][13]:
640
847
 
641
848
  ```ruby
642
849
  configure_jsonapi do |c|
643
- c.conflict_exceptions = [Sequel::ConstraintViolation]
850
+ c.not_found_exceptions << Sequel::NoMatchingRow
644
851
  end
645
852
  ```
646
853
 
@@ -655,7 +862,8 @@ If any step in that process fails, ideally the parent resource and any
655
862
  relationships would be rolled back before returning an error message to the
656
863
  requester.
657
864
 
658
- For example, using Sequel with the database handle stored in the constant `DB`:
865
+ For example, using [Sequel][13] with the database handle stored in the constant
866
+ `DB`:
659
867
 
660
868
  ```ruby
661
869
  helpers do
@@ -665,6 +873,201 @@ helpers do
665
873
  end
666
874
  ```
667
875
 
876
+ ### Side-Unloading Related Resources
877
+
878
+ You may pass an `:include` serializer option (which can be either a
879
+ comma-delimited string or array of strings) when returning resources from
880
+ action helpers. This instructs JSONAPI::Serializers to include a default set of
881
+ related resources along with the primary resource. If the client specifies an
882
+ `include` query parameter, Sinja will automatically pass it to
883
+ JSONAPI::Serializer.serialize, replacing any default value. You may also pass a
884
+ Sinja-specific `:exclude` option to prevent certain related resources from
885
+ being included in the response. If you exclude a resource, its descendents will
886
+ be automatically excluded as well. Feedback welcome.
887
+
888
+ Sinja will attempt to automatically exclude related resources based on the
889
+ current user's role(s) and any available `pluck` and `fetch` action helper
890
+ roles. For example, if resource Foo has many Bars and the current user does not
891
+ have access to Foo.Bars#fetch, the user will not be able to include Bars. It
892
+ will traverse the roles configuration, so if the current user has access to
893
+ Foo.Bars#fetch but not Bars.Qux#pluck, the user will be able to include Bars
894
+ but not Bars.Qux. This feature is experimental. Note that in contrast to the
895
+ `:exclude` option, if a related resource is excluded by this mechanism, its
896
+ descendents will _not_ be automatically excluded.
897
+
898
+ ### Side-Loading Relationships
899
+
900
+ Sinja works hard to DRY up your business logic. As mentioned above, when a
901
+ request comes in to create or update a resource and that request includes
902
+ relationships, Sinja will try to farm out the work to your defined relationship
903
+ routes. Let's look at this example request from the JSON:API specification:
904
+
905
+ ```
906
+ POST /photos HTTP/1.1
907
+ Content-Type: application/vnd.api+json
908
+ Accept: application/vnd.api+json
909
+ ```
910
+
911
+ ```json
912
+ {
913
+ "data": {
914
+ "type": "photos",
915
+ "attributes": {
916
+ "title": "Ember Hamster",
917
+ "src": "http://example.com/images/productivity.png"
918
+ },
919
+ "relationships": {
920
+ "photographer": {
921
+ "data": { "type": "people", "id": "9" }
922
+ }
923
+ }
924
+ }
925
+ }
926
+ ```
927
+
928
+ Assuming a `:photos` resource with a `has_one :photographer` relationship in
929
+ the application, and `graft` is configured to sideload on `create` (more on
930
+ this in a moment), Sinja will invoke the following action helpers in turn:
931
+
932
+ 1. `create` on the Photos resource (with `data.attributes`)
933
+ 1. `graft` on the Photographer relationship (with
934
+ `data.relationships.photographer.data`)
935
+
936
+ If any step of the process fails&mdash;for example, if the `graft` action
937
+ helper is not defined in the Photographer relationship, or if it does not
938
+ permit sideloading from `create`, or if it raises an error&mdash;the entire
939
+ request will fail and any database changes will be rolled back (given a
940
+ `transaction` helper). Note that the user's role must grant them access to call
941
+ either `graft` or `create`.
942
+
943
+ `create` and `update` are the only two action helpers that trigger sideloading;
944
+ `graft`, `merge`, and `clear` are the only action helpers invoked by
945
+ sideloading. You must indicate which combinations are valid using the
946
+ `:sideload_on` action helper option. (Note that if you want to sideload `merge`
947
+ on `update`, you must define a `clear` action helper as well.) For example:
948
+
949
+ ```ruby
950
+ resource :photos do
951
+ helpers do
952
+ def find(id) ..; end
953
+ end
954
+
955
+ create { |attr| .. }
956
+ update { |attr| .. }
957
+
958
+ has_one :photographer do
959
+ # Allow `create' to sideload the Photographer
960
+ graft(sideload_on: :create) { |rio| .. }
961
+ end
962
+
963
+ has_many :tags do
964
+ # Allow `create' and `update' to sideload Tags
965
+ merge(sideload_on: [:create, :update]) { |rios| .. }
966
+
967
+ # Allow `update' to clear Tags before sideloading them
968
+ clear(sideload_on: :update) { .. }
969
+ end
970
+ end
971
+ ```
972
+
973
+ #### Avoiding Null Foreign Keys
974
+
975
+ Now, let's say our DBA is forward-thinking and wants to make the foreign key
976
+ constraint between the `photographer_id` column on the Photos table and the
977
+ People table non-nullable. Unfortunately, that will break Sinja, because the
978
+ Photo will be inserted first, with a null Photographer. (Deferrable constraints
979
+ would be a perfect solution to this problem, but `NOT NULL` constraints are not
980
+ deferrable in Postgres, and constraints in general are not deferrable in
981
+ MySQL.)
982
+
983
+ Instead, we'll need to enforce our non-nullable relationships at the
984
+ application level. To accomplish this, define an ordinary helper named
985
+ `validate!` (in the resource scope or any parent scopes). This method, if
986
+ present, is invoked from within the transaction after the entire request has
987
+ been processed, and so can abort the transaction (following your ORM's
988
+ semantics). For example:
989
+
990
+ ```ruby
991
+ resource :photos do
992
+ helpers do
993
+ def validate!
994
+ fail 'Invalid Photographer for Photo' if resource.photographer.nil?
995
+ end
996
+ end
997
+ end
998
+ ```
999
+
1000
+ If your ORM supports validation&mdash;and "deferred validation"&mdash;you can
1001
+ easily handle all such situations (as well as other types of validations) at
1002
+ the top-level of your application. (Make sure to define your validation
1003
+ exceptions and formatter as described above.) For example, using [Sequel][13]:
1004
+
1005
+ ```ruby
1006
+ class Photo < Sequel::Model
1007
+ many_to_one :photographer
1008
+
1009
+ # http://sequel.jeremyevans.net/rdoc/files/doc/validations_rdoc.html
1010
+ def validate
1011
+ super
1012
+ errors.add(:photographer, 'cannot be null') if photographer.nil?
1013
+ end
1014
+ end
1015
+
1016
+ helpers do
1017
+ def validate!
1018
+ raise Sequel::ValidationFailed, resource.errors unless resource.valid?
1019
+ end
1020
+ end
1021
+
1022
+ resource :photos do
1023
+ create do |attr|
1024
+ photo = Photo.new
1025
+ photo.set(attr)
1026
+ photo.save(validate: false) # defer validation
1027
+ end
1028
+
1029
+ has_one :photographer do
1030
+ graft(sideload_on: :create) do |rio|
1031
+ resource.photographer = People.with_pk!(rio[:id].to_i)
1032
+ resource.save_changes(validate: !sideloaded?) # defer validation if sideloaded
1033
+ end
1034
+ end
1035
+ end
1036
+ ```
1037
+
1038
+ Note that the `validate!` hook is _only_ invoked from within transactions
1039
+ involving the `create` and `update` action helpers (and any dependent `graft`
1040
+ and `merge` action helpers), so this deferred validation pattern is only
1041
+ appropriate in those cases. You must use immedate validation in all other
1042
+ cases. The `sideloaded?` helper is provided to help disambiguate edge cases.
1043
+
1044
+ > TODO: The following three sections are a little confusing. Rewrite them.
1045
+
1046
+ ##### Many-to-One
1047
+
1048
+ Example: Photo belongs to (has one) Photographer; Photo.Photographer cannot be
1049
+ null.
1050
+
1051
+ * Don't define `prune` relationship action helper
1052
+ * Define `graft` relationship action helper to enable reassigning the Photographer
1053
+ * Define `destroy` resource action helper to enable removing the Photo
1054
+ * Use `validate!` helper to check for nulls
1055
+
1056
+ ##### One-to-Many
1057
+
1058
+ Example: Photographer has many Photos; Photo.Photographer cannot be null.
1059
+
1060
+ * Don't define `clear` relationship action helper
1061
+ * Don't define `subtract` relationship action helper
1062
+ * Delegate removing Photos and reassigning Photographers to Photo resource
1063
+
1064
+ ##### Many-to-Many
1065
+
1066
+ Example: Photo has many Tags.
1067
+
1068
+ Nothing to worry about here! Feel free to use `NOT NULL` foreign key
1069
+ constraints on the join table.
1070
+
668
1071
  ### Coalesced Find Requests
669
1072
 
670
1073
  If your JSON:API client coalesces find requests, the `show` action helper will
@@ -674,7 +1077,28 @@ are supported: `?filter[id]=1,2` and `?filter[id][]=1&filter[id][]=2`. If any
674
1077
  ID is not found (i.e. `show` returns `nil`), the route will halt with HTTP
675
1078
  status code 404.
676
1079
 
677
- ### Module Namespaces
1080
+ ### Patchless Clients
1081
+
1082
+ JSON:API [recommends][23] supporting patchless clients by using the
1083
+ `X-HTTP-Method-Override` request header to coerce a `POST` into a `PATCH`. To
1084
+ support this in Sinja, add the Sinja::MethodOverride middleware (which is a
1085
+ stripped-down version of [Rack::MethodOverride][24]) into your application (or
1086
+ Rackup configuration):
1087
+
1088
+ ```ruby
1089
+ require 'sinja'
1090
+ require 'sinja/method_override'
1091
+
1092
+ class MyApp < Sinatra::Base
1093
+ use Sinja::MethodOverride
1094
+
1095
+ register Sinja
1096
+
1097
+ # ..
1098
+ end
1099
+ ```
1100
+
1101
+ ### Sinja or Sinatra::JSONAPI
678
1102
 
679
1103
  Everything is dual-namespaced under both Sinatra::JSONAPI and Sinja, and Sinja
680
1104
  requires Sinatra::Base, so this:
@@ -725,14 +1149,14 @@ should be relatively painless. For example:
725
1149
  ```ruby
726
1150
  # controllers/foo_controller.rb
727
1151
  FooController = proc do
728
- index do
729
- Foo.all
730
- end
731
-
732
1152
  show do |id|
733
1153
  Foo[id.to_i]
734
1154
  end
735
1155
 
1156
+ index do
1157
+ Foo.all
1158
+ end
1159
+
736
1160
  # ..
737
1161
  end
738
1162
 
@@ -795,3 +1219,6 @@ License](http://opensource.org/licenses/MIT).
795
1219
  [20]: http://roda.jeremyevans.net
796
1220
  [21]: http://www.sinatrarb.com/contrib/namespace.html
797
1221
  [22]: http://jsonapi.org/format/#document-resource-identifier-objects
1222
+ [23]: http://jsonapi.org/recommendations/#patchless-clients
1223
+ [24]: http://www.rubydoc.info/github/rack/rack/Rack/MethodOverride
1224
+ [25]: http://www.sinatrarb.com/mustermann/