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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +755 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/sinatra/jsonapi.rb +127 -0
- data/lib/sinatra/jsonapi/config.rb +125 -0
- data/lib/sinatra/jsonapi/helpers/relationships.rb +23 -0
- data/lib/sinatra/jsonapi/helpers/sequel.rb +62 -0
- data/lib/sinatra/jsonapi/helpers/serializers.rb +131 -0
- data/lib/sinatra/jsonapi/relationship_routes/has_many.rb +35 -0
- data/lib/sinatra/jsonapi/relationship_routes/has_one.rb +25 -0
- data/lib/sinatra/jsonapi/resource.rb +137 -0
- data/lib/sinatra/jsonapi/resource_routes.rb +63 -0
- data/lib/sinja.rb +13 -0
- data/lib/sinja/version.rb +6 -0
- data/sinja.gemspec +30 -0
- metadata +163 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -0,0 +1,755 @@
|
|
1
|
+
# Sinja (Sinatra::JSONAPI)
|
2
|
+
|
3
|
+
[](https://travis-ci.org/mwpastore/sinja)
|
4
|
+
[](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-à-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-à-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
|