sinja 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b8ac4687f2a8eea409209c9da5ac107ccdb2b886
4
+ data.tar.gz: c5e3e902e1e54a0e93a0888411d09e8046e80a20
5
+ SHA512:
6
+ metadata.gz: 370e3e8f1f13a4ee3f216a1d3bf92c16b0fbf343e317d7774f45b6287479be69d59055ae690266ac8f570d514e60ae7c5051fa534af8f6ecb2fd13eae13105ec
7
+ data.tar.gz: 823218b82a74b85566d10a3df1d1ac7ef92ffb3b110c19f739dd5eec39c53d289df4a23e101ba29ff3f3cbc59fff702c64f0db00de5b57d72a183907756085c6
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.13.2
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Mike Pastore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,755 @@
1
+ # Sinja (Sinatra::JSONAPI)
2
+
3
+ [![Build Status](https://travis-ci.org/mwpastore/sinja.svg?branch=master)](https://travis-ci.org/mwpastore/sinja)
4
+ [![Gem Version](https://badge.fury.io/rb/sinja.svg)](https://badge.fury.io/rb/sinja)
5
+
6
+ Sinja is a [Sinatra 2.0][1] [extension][10] for quickly building [RESTful][11],
7
+ [JSON:API][2]-[compliant][7] web services, leveraging the excellent
8
+ [JSONAPI::Serializers][3] gem and [Sinatra::Namespace][21] extension. It
9
+ enhances Sinatra's DSL to enable resource-, relationship-, and role-centric
10
+ definition of applications, and it configures Sinatra with the proper settings,
11
+ MIME-types, filters, conditions, and error-handling to implement JSON:API.
12
+ Sinja aims to be lightweight (to the extent that Sinatra is), ORM-agnostic (to
13
+ the extent that JSONAPI::Serializers is), and opinionated (to the extent that
14
+ the JSON:API specification is).
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
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
21
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
22
+
23
+
24
+ - [Synopsis](#synopsis)
25
+ - [Installation](#installation)
26
+ - [Features](#features)
27
+ - [Extensibility](#extensibility)
28
+ - [Public APIs](#public-apis)
29
+ - [Performance](#performance)
30
+ - [Comparison with JSONAPI::Resources (JR)](#comparison-with-jsonapiresources-jr)
31
+ - [Usage](#usage)
32
+ - [Configuration](#configuration)
33
+ - [Sinatra](#sinatra)
34
+ - [Sinja](#sinja)
35
+ - [Action Helpers](#action-helpers)
36
+ - [`resource`](#resource)
37
+ - [`index {..}` => Array](#index---array)
38
+ - [`show {|id| ..}` => Object](#show-id---object)
39
+ - [`create {|attr, id| ..}` => id, Object?](#create-attr-id---id-object)
40
+ - [`create {|attr| ..}` => id, Object](#create-attr---id-object)
41
+ - [`update {|attr| ..}` => Object?](#update-attr---object)
42
+ - [`destroy {..}`](#destroy-)
43
+ - [`has_one`](#has_one)
44
+ - [`pluck {..}` => Object](#pluck---object)
45
+ - [`prune {..}` => TrueClass?](#prune---trueclass)
46
+ - [`graft {|rio| ..}` => TrueClass?](#graft-rio---trueclass)
47
+ - [`has_many`](#has_many)
48
+ - [`fetch {..}` => Array](#fetch---array)
49
+ - [`clear {..}` => TrueClass?](#clear---trueclass)
50
+ - [`merge {|rios| ..}` => TrueClass?](#merge-rios---trueclass)
51
+ - [`subtract {|rios| ..}` => TrueClass?](#subtract-rios---trueclass)
52
+ - [Authorization](#authorization)
53
+ - [`default_roles` configurable](#default_roles-configurable)
54
+ - [`:roles` Action Helper option](#roles-action-helper-option)
55
+ - [`role` helper](#role-helper)
56
+ - [Conflicts](#conflicts)
57
+ - [Transactions](#transactions)
58
+ - [Module Namespaces](#module-namespaces)
59
+ - [Code Organization](#code-organization)
60
+ - [Development](#development)
61
+ - [Contributing](#contributing)
62
+ - [License](#license)
63
+
64
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
65
+
66
+ ## Synopsis
67
+
68
+ ```ruby
69
+ require 'sinatra'
70
+ require 'sinatra/jsonapi'
71
+
72
+ resource :posts do
73
+ index do
74
+ Post.all
75
+ end
76
+
77
+ show do |id|
78
+ Post[id.to_i]
79
+ end
80
+
81
+ create do |attr|
82
+ Post.create(attr)
83
+ end
84
+ end
85
+
86
+ freeze_jsonapi
87
+ ```
88
+
89
+ Assuming the presence of a `Post` model and serializer, running the above
90
+ "classic"-style Sinatra application would enable the following endpoints (with
91
+ all other JSON:API endpoints returning 404 or 405):
92
+
93
+ * `GET /posts`
94
+ * `GET /posts/<id>`
95
+ * `POST /posts`
96
+
97
+ Of course, "modular"-style Sinatra aplications require you to register the
98
+ extension:
99
+
100
+ ```ruby
101
+ require 'sinatra/base'
102
+ require 'sinatra/jsonapi'
103
+
104
+ class App < Sinatra::Base
105
+ register Sinatra::JSONAPI
106
+
107
+ resource :posts do
108
+ # ..
109
+ end
110
+
111
+ freeze_jsonapi
112
+ end
113
+ ```
114
+
115
+ ## Installation
116
+
117
+ Add this line to your application's Gemfile:
118
+
119
+ ```ruby
120
+ gem 'sinja'
121
+ ```
122
+
123
+ And then execute:
124
+
125
+ ```sh
126
+ $ bundle
127
+ ```
128
+
129
+ Or install it yourself as:
130
+
131
+ ```sh
132
+ $ gem install sinja
133
+ ```
134
+
135
+ ## Features
136
+
137
+ * ORM-agnostic
138
+ * Role-based authorization
139
+ * To-one and to-many relationships
140
+ * Side-loaded relationships on resource creation
141
+ * Conflict (constraint violation) handling
142
+ * Plus all the features of JSONAPI::Serializers!
143
+
144
+ Its main competitors in the Ruby space are [ActiveModelSerializers][12] (AMS)
145
+ with the JsonApi adapter and [JSONAPI::Resources][8] (JR), both of which are
146
+ designed to work with [Rails][16] and [ActiveRecord][17]/[ActiveModel][18]
147
+ (although they may work with [Sequel][13] via [sequel-rails][14] and Sequel's
148
+ [`:active_model` plugin][15]). Otherwise, you might use something like Sinatra,
149
+ [Roda][20], or [Grape][19] with JSONAPI::Serializers, your own routes, and a
150
+ ton of boilerplate. The goal of this extension is to provide most or all of the
151
+ boilerplate for a Sintara application and automate the drawing of routes based
152
+ on the resource definitions.
153
+
154
+ ### Extensibility
155
+
156
+ The "power" of implementing this functionality as a Sinatra extension is that
157
+ all of Sinatra's usual features are available within your resource definitions.
158
+ The action helpers blocks get compiled into Sinatra helpers, and the
159
+ `resource`, `has_one`, and `has_many` keywords simply build
160
+ [Sinatra::Namespace][21] blocks. You can manage caching directives, set
161
+ headers, and even `halt` (or `not_found`) out of action helpers as desired.
162
+
163
+ ```ruby
164
+ class App < Sinatra::Base
165
+ register Sinatra::JSONAPI
166
+
167
+ # <- This is a Sinatra::Base class definition. (Duh.)
168
+
169
+ resource :books do
170
+ # <- This is a Sinatra::Namespace block.
171
+
172
+ show do |id|
173
+ # <- This is a Sinatra helper, scoped to the resource namespace.
174
+ end
175
+
176
+ has_one :author do
177
+ # <- This is a Sinatra::Namespace block, nested under the resource namespace.
178
+
179
+ pluck do
180
+ # <- This is a Sinatra helper, scoped to the nested namespace.
181
+ end
182
+ end
183
+ end
184
+
185
+ freeze_jsonapi
186
+ end
187
+ ```
188
+
189
+ This lets you easily pepper in all the syntactic sugar you might expect to see
190
+ in a typical Sinatra application:
191
+
192
+ ```ruby
193
+ class App < Sinatra::Base
194
+ register Sinatra::JSONAPI
195
+
196
+ configure :development do
197
+ enable :logging
198
+ end
199
+
200
+ helpers do
201
+ def foo; true end
202
+ end
203
+
204
+ before do
205
+ cache_control :public, max_age: 3_600
206
+ end
207
+
208
+ # define a custom /status route
209
+ get('/status', provides: :json) { 'OK' }
210
+
211
+ resource :books do
212
+ show do |id|
213
+ book = Book[id.to_i]
214
+ not_found "Book #{id} not found!" unless book
215
+ headers 'X-ISBN'=>book.isbn
216
+ last_modified book.updated_at
217
+ next book, include: %w[author]
218
+ end
219
+
220
+ has_one :author do
221
+ helpers do
222
+ def bar; false end
223
+ end
224
+
225
+ before do
226
+ cache_control :private
227
+ halt 403 unless foo || bar
228
+ end
229
+
230
+ pluck do
231
+ etag resource.author.hash, :weak
232
+ resource.author
233
+ end
234
+ end
235
+
236
+ # define a custom /books/top10 route
237
+ get '/top10' do
238
+ serialize_models Book.where{}.reverse_order(:recent_sales).limit(10).all
239
+ end
240
+ end
241
+
242
+ freeze_jsonapi
243
+ end
244
+ ```
245
+
246
+ #### Public APIs
247
+
248
+ **data**
249
+ : Returns the `data` key of the deserialized request payload (with symbolized
250
+ names).
251
+
252
+ **serialize_model**
253
+ : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
254
+ a serialized model.
255
+
256
+ **serialize_model?**
257
+ : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
258
+ a serialized model if non-`nil`, or the root metadata if present, or a HTTP
259
+ status 204.
260
+
261
+ **serialize_models**
262
+ : Takes an array of models (and optional hash of JSONAPI::Serializers options)
263
+ and returns a serialized collection.
264
+
265
+ **serialize_models?**
266
+ : Takes an array of models (and optional hash of JSONAPI::Serializers options)
267
+ and returns a serialized collection if non-empty, or the root metadata if
268
+ present, or a HTTP status 204.
269
+
270
+ ### Performance
271
+
272
+ Although there is some heavy metaprogramming happening at boot time, the end
273
+ result is simply a collection of Sinatra namespaces, routes, filters,
274
+ conditions, helpers, etc., and Sinja applications should perform as if you had
275
+ written them verbosely. The main caveat is that there are quite a few block
276
+ closures, which don't perform as well as normal methods in Ruby. Feedback
277
+ welcome.
278
+
279
+ ### Comparison with JSONAPI::Resources (JR)
280
+
281
+ | Feature | JR | Sinja |
282
+ | :-------------- | :--------------------------- | :------------------------------------------------ |
283
+ | Serializer | Built-in | [JSONAPI::Serializers][3] |
284
+ | Framework | Rails | Sinatra, but easy to mount within others |
285
+ | Routing | ActionDispatch::Routing | Mustermann |
286
+ | Caching | ActiveSupport::Cache | BYO |
287
+ | ORM | ActiveRecord/ActiveModel | BYO |
288
+ | Authorization | [Pundit][9] | Role-based (`roles` keyword and `role` helper) |
289
+ | Immutability | `immutable` method | Omit mutator action helpers |
290
+ | Fetchability | `fetchable_fields` method | Omit attributes in Serializer |
291
+ | Creatability | `creatable_fields` method | Handle in `create` action helper or Model\* |
292
+ | Updatability | `updatable_fields` method | Handle in `update` action helper or Model\* |
293
+ | Sortability | `sortable_fields` method | Handle `params[:sort]` in `index` action helper |
294
+ | Default sorting | `default_sort` method | Set default for `params[:sort]` |
295
+ | Context | `context` method | Rack middleware (e.g. `env['context']`) |
296
+ | Attributes | Define in Model and Resource | Define in Model\* and Serializer |
297
+ | Formatting | `format` attribute keyword | Define attribute as a method in Serialier |
298
+ | Relationships | Define in Model and Resource | Define in Model, Resource, and Serializer |
299
+ | Filters | `filter(s)` keywords | Handle `params[:filter]` in `index` action helper |
300
+ | Default filters | `default` filter keyword | Set default for `params[:filter]` |
301
+
302
+ \* - Depending on your ORM.
303
+
304
+ This list is incomplete. TODO:
305
+
306
+ * Primary keys
307
+ * Pagination
308
+ * Custom links
309
+ * Meta
310
+ * Side-loading (on request and response)
311
+ * Namespaces
312
+ * Configuration
313
+ * Validation
314
+
315
+ ## Usage
316
+
317
+ You'll need a database schema and models (using the engine and ORM of your
318
+ choice) and [serializers][3] to get started. Create a new Sinatra application
319
+ (classic or modular) to hold all your JSON:API endpoints and (if modular)
320
+ register this extension. Instead of defining routes with `get`, `post`, etc. as
321
+ you normally would, simply define `resource` blocks with action helpers and
322
+ `has_one` and `has_many` relationship blocks (with their own action helpers).
323
+ Sinja will draw and enable the appropriate routes based on the defined
324
+ resources, relationships, and action helpers. Other routes will return the
325
+ appropriate HTTP status codes: 403, 404, or 405.
326
+
327
+ ### Configuration
328
+
329
+ #### Sinatra
330
+
331
+ Registering this extension has a number of application-wide implications,
332
+ detailed below. If you have any non-JSON:API routes, you may want to keep them
333
+ in a separate application and incorporate them as middleware or mount them
334
+ elsewhere (e.g. with [Rack::URLMap][4]), or host them as a completely separate
335
+ web service. It may not be feasible to have custom routes that don't conform to
336
+ these settings.
337
+
338
+ * Registers [Sinatra::Namespace][21]
339
+ * Disables [Rack::Protection][6] (can be reenabled with `enable :protection` or
340
+ by manually `use`-ing the Rack::Protection middleware)
341
+ * Disables static file routes (can be reenabled with `enable :static`)
342
+ * Sets `:show_exceptions` to `:after_handler`
343
+ * Adds an `:api_json` MIME-type (`Sinatra::JSONAPI::MIME_TYPE`)
344
+ * Enforces strict checking of the `Accept` and `Content-Type` request headers
345
+ * Sets the `Content-Type` response header to `:api_json` (can be overriden with
346
+ the `content_type` helper)
347
+ * Normalizes query parameters to reflect the features supported by JSON:API
348
+ (this may be strictly enforced in future versions of Sinja)
349
+ * Formats all errors to the proper JSON:API structure
350
+ * Serializes all response bodies (including errors) to JSON
351
+
352
+ #### Sinja
353
+
354
+ Sinja provides its own configuration store that can be accessed through the
355
+ `configure_jsonapi` block. The following configurables are available (with
356
+ their defaults shown):
357
+
358
+ ```ruby
359
+ configure_jsonapi do |c|
360
+ #c.conflict_exceptions = [] # see "Conflicts" below
361
+
362
+ #c.default_roles = {} # see "Authorization" below
363
+
364
+ # Set the "progname" used by Sinja when accessing the logger
365
+ #c.logger_progname = 'sinja'
366
+
367
+ # A hash of options to pass to JSONAPI::Serializer.serialize
368
+ #c.serializer_opts = {}
369
+
370
+ # JSON methods to use when serializing response bodies and errors
371
+ #c.json_generator = development? ? :pretty_generate : :generate
372
+ #c.json_error_generator = development? ? :pretty_generate : :fast_generate
373
+ end
374
+ ```
375
+
376
+ After Sinja is configured and all your resources are defined, you should call
377
+ `freeze_jsonapi` to freeze the configuration store.
378
+
379
+ ### Action Helpers
380
+
381
+ Action helpers should be defined within the appropriate block contexts
382
+ (`resource`, `has_one`, or `has_many`) using the given keywords and arguments
383
+ below. Implicitly return the expected values as described below (as an array if
384
+ necessary) or use the `next` keyword (instead of `return` or `break`) to exit
385
+ the action helper. Return values marked with a question mark below may be
386
+ omitted entirely. Any helper may additionally return an options hash to pass
387
+ along to JSONAPI::Serializers.
388
+
389
+ The `:include` and `:fields` query parameters are automatically passed through
390
+ to JSONAPI::Serializers. You may also use the special `:exclude` option to
391
+ prevent specific relationships from being included in the response. This
392
+ accepts the same formats as JSONAPI::Serializers does for `:include`. If you
393
+ exclude a relationship, any sub-relationships will also be excluded. The
394
+ `:sort`, `:page`, and `:filter` query parameters must be handled manually.
395
+
396
+ All arguments to action helpers are "tainted" and should be treated as
397
+ potentially dangerous: IDs, attribute hashes, and [resource identifier
398
+ objects][22].
399
+
400
+ Finally, some routes will automatically invoke the `show` action helper on your
401
+ behalf and make the selected resource available to other action helpers as
402
+ `resource`. You've already told Sinja how to find a resource by ID, so why
403
+ repeat yourself? For example, the `PATCH /<name>/:id` route looks up the
404
+ resource with that ID using the `show` action helper and makes it available to
405
+ the `update` action helper as `resource`. The same goes for the `DELETE
406
+ /<name>/:id` route and the `destroy` action helper, and all of the `has_one`
407
+ and `has_many` action helpers.
408
+
409
+ #### `resource`
410
+
411
+ ##### `index {..}` => Array
412
+
413
+ Return an array of zero or more objects to serialize on the response.
414
+
415
+ ##### `show {|id| ..}` => Object
416
+
417
+ Take an ID and return the corresponding object (or `nil` if not found) to
418
+ serialize on the response.
419
+
420
+ ##### `create {|attr, id| ..}` => id, Object?
421
+
422
+ With client-generated IDs: Take a hash of attributes and a client-generated ID,
423
+ create a new resource, and return the ID and optionally the created resource.
424
+ (Note that only one or the other `create` action helpers is allowed in any
425
+ given resource block.)
426
+
427
+ ##### `create {|attr| ..}` => id, Object
428
+
429
+ Without client-generated IDs: Take a hash of attributes, create a new resource,
430
+ and return the server-generated ID and the created resource. (Note that only
431
+ one or the other `create` action helpers is allowed in any given resource
432
+ block.)
433
+
434
+ ##### `update {|attr| ..}` => Object?
435
+
436
+ Take a hash of attributes, update `resource`, and optionally return the updated
437
+ resource.
438
+
439
+ ##### `destroy {..}`
440
+
441
+ Delete or destroy `resource`.
442
+
443
+ #### `has_one`
444
+
445
+ ##### `pluck {..}` => Object
446
+
447
+ Return the related object vis-&agrave;-vis `resource` to serialize on the
448
+ response. Defined by default as `resource.send(<to-one>)`; can be either
449
+ overridden or disabled entirely with `pluck(&nil)`.
450
+
451
+ ##### `prune {..}` => TrueClass?
452
+
453
+ Remove the relationship from `resource`. To serialize the updated linkage on
454
+ the response, refresh or reload `resource` (if necessary) and return a truthy
455
+ value.
456
+
457
+ For example, using Sequel:
458
+
459
+ ```ruby
460
+ has_one :qux do
461
+ prune do
462
+ resource.qux = nil
463
+ resource.save_changes # will return truthy if the relationship was present
464
+ end
465
+ end
466
+ ```
467
+
468
+ ##### `graft {|rio| ..}` => TrueClass?
469
+
470
+ Take a [resource identifier object][22] and update the relationship on
471
+ `resource`. To serialize the updated linkage on the response, refresh or reload
472
+ `resource` (if necessary) and return a truthy value.
473
+
474
+ #### `has_many`
475
+
476
+ ##### `fetch {..}` => Array
477
+
478
+ Return an array of related objects vis-&agrave;-vis `resource` to serialize on
479
+ the response. Defined by default as `resource.send(<to-many>)`; can be either
480
+ overridden or disabled entirely with `fetch(&nil)`.
481
+
482
+ ##### `clear {..}` => TrueClass?
483
+
484
+ Remove all relationships from `resource`. To serialize the updated linkage on
485
+ the response, refresh or reload `resource` (if necessary) and return a truthy
486
+ value.
487
+
488
+ For example, using Sequel:
489
+
490
+ ```ruby
491
+ has_many :bars do
492
+ clear do
493
+ resource.remove_all_bars # will return truthy if relationships were present
494
+ end
495
+ end
496
+ ```
497
+
498
+ ##### `merge {|rios| ..}` => TrueClass?
499
+
500
+ Take an array of [resource identifier objects][22] and update (add unless
501
+ already present) the relationships on `resource`. To serialize the updated
502
+ linkage on the response, refresh or reload `resource` (if necessary) and return
503
+ a truthy value.
504
+
505
+ ##### `subtract {|rios| ..}` => TrueClass?
506
+
507
+ Take an array of [resource identifier objects][22] and update (remove unless
508
+ already missing) the relationships on `resource`. To serialize the updated
509
+ linkage on the response, refresh or reload `resource` (if necessary) and return
510
+ a truthy value.
511
+
512
+ ### Authorization
513
+
514
+ Sinja provides a simple role-based authorization scheme to restrict access to
515
+ routes based on the action helpers they invoke. For example, you might say all
516
+ logged-in users have access to `index`, `show`, `pluck`, and `fetch` (the
517
+ read-only action helpers), but only administrators have access to `create`,
518
+ `update`, etc. (the read-write action helpers). You can have as many roles as
519
+ you'd like, e.g. a super-administrator role to restrict access to `destroy`.
520
+ Users can be in one or more roles, and action helpers can be restricted to one
521
+ or more roles for maximum flexibility. There are three main components to the
522
+ scheme:
523
+
524
+ #### `default_roles` configurable
525
+
526
+ You set the default roles for the entire Sinja application in the top-level
527
+ configuration. Action helpers without any default roles are unrestricted by
528
+ default.
529
+
530
+ ```ruby
531
+ configure_jsonapi do |c|
532
+ c.default_roles = {
533
+ # Resource roles
534
+ index: :user,
535
+ show: :user,
536
+ create: :admin,
537
+ update: :admin,
538
+ destroy: :super,
539
+
540
+ # To-one relationship roles
541
+ pluck: :user,
542
+ prune: :admin,
543
+ graft: :admin,
544
+
545
+ # To-many relationship roles
546
+ fetch: :user,
547
+ clear: :admin,
548
+ merge: :admin,
549
+ subtract: :admin
550
+ }
551
+ end
552
+ ```
553
+
554
+ #### `:roles` Action Helper option
555
+
556
+ To override the default roles for any given action helper, simply specify a
557
+ `:roles` option when defining it. To remove all restrictions from an action
558
+ helper, set `:roles` to an empty array. For example, to manage access to
559
+ `show` at different levels of granularity (with the above `default_roles`):
560
+
561
+ ```ruby
562
+ resource :foos do
563
+ show do
564
+ # any logged-in user (with the :user role) can access /foos/:id
565
+ end
566
+ end
567
+
568
+ resource :bars do
569
+ show(roles: :admin) do
570
+ # only logged-in users with the :admin role can access /bars/:id
571
+ end
572
+ end
573
+
574
+ resource :quxes do
575
+ show(roles: []) do
576
+ # anyone (bypassing the `role' helper) can access /quxes/:id
577
+ end
578
+ end
579
+ ```
580
+
581
+ #### `role` helper
582
+
583
+ Finally, define a `role` helper in your application that returns the user's
584
+ role(s) (if any). You can handle login failures in your middleware, elsewhere
585
+ in the application (i.e. a `before` filter), or within the helper, either by
586
+ halting or raising an error or by simply letting Sinja halt 403 on restricted
587
+ action helpers when `role` returns `nil` (the default behavior).
588
+
589
+ ```ruby
590
+ helpers do
591
+ def role
592
+ env['my_auth_middleware'].login!
593
+ session[:roles]
594
+ rescue MyAuthenticationFailure=>e
595
+ nil
596
+ end
597
+ end
598
+ ```
599
+
600
+ ### Conflicts
601
+
602
+ If your database driver raises exceptions on constraint violations, you should
603
+ specify which exception class(es) should be handled and return HTTP status code
604
+ 409.
605
+
606
+ For example, using Sequel:
607
+
608
+ ```ruby
609
+ configure_jsonapi do |c|
610
+ c.conflict_exceptions = [Sequel::ConstraintViolation]
611
+ end
612
+ ```
613
+
614
+ ### Transactions
615
+
616
+ If your database driver support transactions, you should define a yielding
617
+ `transaction` helper in your application for Sinja to use when working with
618
+ sideloaded data in the request. For example, if relationship data is provided
619
+ in the request payload when creating resources, Sinja will automatically farm
620
+ out to other routes to build those relationships after the resource is created.
621
+ If any step in that process fails, ideally the parent resource and any
622
+ relationships would be rolled back before returning an error message to the
623
+ requester.
624
+
625
+ For example, using Sequel with the database handle stored in the constant `DB`:
626
+
627
+ ```ruby
628
+ helpers do
629
+ def transaction
630
+ DB.transaction { yield }
631
+ end
632
+ end
633
+ ```
634
+
635
+ ### Module Namespaces
636
+
637
+ Everything is dual-namespaced under both Sinatra::JSONAPI and Sinja, and Sinja
638
+ requires Sinatra::Base, so this:
639
+
640
+ ```ruby
641
+ require 'sinatra/jsonapi'
642
+
643
+ class App < Sinatra::Base
644
+ register Sinatra::JSONAPI
645
+
646
+ configure_jsonapi do |c|
647
+ # ..
648
+ end
649
+
650
+ # ..
651
+
652
+ freeze_jsonapi
653
+ end
654
+ ```
655
+
656
+ Can also be written like this:
657
+
658
+ ```ruby
659
+ require 'sinja'
660
+
661
+ class App < Sinatra::Base
662
+ register Sinja
663
+
664
+ sinja do |c|
665
+ # ..
666
+ end
667
+
668
+ # ..
669
+
670
+ sinja.freeze
671
+ end
672
+ ```
673
+
674
+ ### Code Organization
675
+
676
+ Sinja applications might grow overly large with a block for each resource. I am
677
+ still working on a better way to handle this (as well as a way to provide
678
+ standalone resource controllers for e.g. cloud functions), but for the time
679
+ being you can store each resource block as its own Proc, and pass it to the
680
+ `resource` keyword in lieu of a block. The migration to some future solution
681
+ should be relatively painless. For example:
682
+
683
+ ```ruby
684
+ # controllers/foo_controller.rb
685
+ FooController = proc do
686
+ index do
687
+ Foo.all
688
+ end
689
+
690
+ show do |id|
691
+ Foo[id.to_i]
692
+ end
693
+
694
+ # ..
695
+ end
696
+
697
+ # app.rb
698
+ require 'sinatra/base'
699
+ require 'sinatra/jsonapi'
700
+
701
+ require_relative 'controllers/foo_controller'
702
+
703
+ class App < Sinatra::Base
704
+ register Sinatra::JSONAPI
705
+
706
+ resource :foos, FooController
707
+
708
+ freeze_jsonapi
709
+ end
710
+ ```
711
+
712
+ ## Development
713
+
714
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
715
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
716
+ prompt that will allow you to experiment.
717
+
718
+ To install this gem onto your local machine, run `bundle exec rake install`. To
719
+ release a new version, update the version number in `version.rb`, and then run
720
+ `bundle exec rake release`, which will create a git tag for the version, push
721
+ git commits and tags, and push the `.gem` file to
722
+ [rubygems.org](https://rubygems.org).
723
+
724
+ ## Contributing
725
+
726
+ Bug reports and pull requests are welcome on GitHub at
727
+ https://github.com/mwpastore/sinja.
728
+
729
+ ## License
730
+
731
+ The gem is available as open source under the terms of the [MIT
732
+ License](http://opensource.org/licenses/MIT).
733
+
734
+ [1]: http://www.sinatrarb.com
735
+ [2]: http://jsonapi.org
736
+ [3]: https://github.com/fotinakis/jsonapi-serializers
737
+ [4]: http://www.rubydoc.info/github/rack/rack/master/Rack/URLMap
738
+ [5]: http://rodauth.jeremyevans.net
739
+ [6]: https://github.com/sinatra/sinatra/tree/master/rack-protection
740
+ [7]: http://jsonapi.org/format/
741
+ [8]: https://github.com/cerebris/jsonapi-resources
742
+ [9]: https://github.com/cerebris/jsonapi-resources#authorization
743
+ [10]: http://www.sinatrarb.com/extensions-wild.html
744
+ [11]: https://en.wikipedia.org/wiki/Representational_state_transfer
745
+ [12]: https://github.com/rails-api/active_model_serializers
746
+ [13]: http://sequel.jeremyevans.net
747
+ [14]: http://talentbox.github.io/sequel-rails/
748
+ [15]: http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/ActiveModel.html
749
+ [16]: http://rubyonrails.org
750
+ [17]: https://github.com/rails/rails/tree/master/activerecord
751
+ [18]: https://github.com/rails/rails/tree/master/activemodel
752
+ [19]: http://www.ruby-grape.org
753
+ [20]: http://roda.jeremyevans.net
754
+ [21]: http://www.sinatrarb.com/contrib/namespace.html
755
+ [22]: http://jsonapi.org/format/#document-resource-identifier-objects