sinja 0.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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-à-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
|