sinja 0.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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