sinja 1.2.1 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2baae47c1aa32d45265b3726e8a57163bf1d381e
4
- data.tar.gz: 2e3de4a123865207d3a9bc6090563fa979a8ee52
3
+ metadata.gz: adc1346572a9c1a9ae1d2b85c70931cd329cdd1e
4
+ data.tar.gz: 73db963c1a496a98de718d1ea17ebda739d80ad9
5
5
  SHA512:
6
- metadata.gz: 6fcfb53cdc190885a45594927e6fdb335c2fe44152884fe7051d3420f6431a7d7762215c924aea8526fd9486deb664a58486b3858d2b17f77e072f6d9b131f63
7
- data.tar.gz: 952f451d6e3f18c992015e14d82fde782e866cdd078b06bbff30a00944b1c4a623a49c673fc971630f51ad943be7aebe59c3f50e900dd7f002ad7432720f4afc
6
+ metadata.gz: 55dc7ba3bc974625bb2cf65b943ccf8cf57541226f39eb0d66c7e56320c507cf3f8d76d0fddba5605698677f459958ede489477ebc9baed97d82a50c6a1e76f2
7
+ data.tar.gz: f75638978ade62140b1306644e4a81a62b18d1a8ce09b388ffa183c6c4a5859d531ce108b71299e5b82e39b3759e6d3c0b381799dc12be0f845a63784ec2280a
data/README.md CHANGED
@@ -10,19 +10,23 @@
10
10
  [![Gem Version](https://badge.fury.io/rb/sinja.svg)](https://badge.fury.io/rb/sinja)
11
11
  [![Dependency Status](https://gemnasium.com/badges/github.com/mwpastore/sinja.svg)](https://gemnasium.com/github.com/mwpastore/sinja)
12
12
  [![Build Status](https://travis-ci.org/mwpastore/sinja.svg?branch=master)](https://travis-ci.org/mwpastore/sinja)
13
-
13
+ [![{json:api} version](https://img.shields.io/badge/%7Bjson%3Aapi%7D%20version-1.0-lightgrey.svg)][7]
14
14
  [![Chat in #sinja-rb on Gitter](https://badges.gitter.im/sinja-rb/Lobby.svg)](https://gitter.im/sinja-rb/Lobby)
15
- [![Chat in #-ember-data on Slack](https://ember-community-slackin.herokuapp.com/badge.svg)](https://ember-community-slackin.herokuapp.com/?channel=-ember-data)
16
15
 
17
16
  Sinja is a [Sinatra][1] [extension][10] for quickly building [RESTful][11],
18
- [{json:api}][2]-[compliant][7] web services, leveraging the excellent
19
- [JSONAPI::Serializers][3] gem and [Sinatra::Namespace][21] extension. It
20
- enhances Sinatra's DSL to enable resource-, relationship-, and role-centric
21
- definition of applications, and it configures Sinatra with the proper settings,
22
- MIME-types, filters, conditions, and error-handling to implement {json:api}.
23
- Sinja aims to be lightweight (to the extent that Sinatra is), ORM-agnostic (to
24
- the extent that JSONAPI::Serializers is), and opinionated (to the extent that
25
- the {json:api} specification is).
17
+ [{json:api}][2]-compliant web services, leveraging the excellent
18
+ [JSONAPI::Serializers][3] gem for payload serialization. It enhances Sinatra's
19
+ DSL to enable resource-, relationship-, and role-centric API development, and
20
+ it configures Sinatra with the proper settings, MIME-types, filters,
21
+ conditions, and error-handling.
22
+
23
+ There are [many][31] parsing (deserializing) and rendering (serializing) and
24
+ so-called "JSON API" libraries available for Ruby, but relatively few that
25
+ attempt to correctly implement the entire {json:api} specification, including
26
+ routing, request header and query parameter checking, and relationship
27
+ side-loading. Sinja lets you focus on the business logic of your applications
28
+ without worrying about the specification, and without pulling in a heavy
29
+ framework like [Rails][16]. It's lightweight and ORM-agnostic!
26
30
 
27
31
  <!-- START doctoc generated TOC please keep comment here to allow auto update -->
28
32
  <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
@@ -30,14 +34,7 @@ the {json:api} specification is).
30
34
 
31
35
  - [Synopsis](#synopsis)
32
36
  - [Installation](#installation)
33
- - [Features & Design](#features--design)
34
- - [Ol' Blue Eyes is Back](#ol-blue-eyes-is-back)
35
- - [Public APIs](#public-apis)
36
- - [Commonly Used](#commonly-used)
37
- - [Less-Commonly Used](#less-commonly-used)
38
- - [Performance](#performance)
39
- - [Extensions](#extensions)
40
- - [Comparison with JSONAPI::Resources](#comparison-with-jsonapiresources)
37
+ - [Ol' Blue Eyes is Back](#ol-blue-eyes-is-back)
41
38
  - [Basic Usage](#basic-usage)
42
39
  - [Configuration](#configuration)
43
40
  - [Sinatra](#sinatra)
@@ -45,24 +42,8 @@ the {json:api} specification is).
45
42
  - [Resource Locators](#resource-locators)
46
43
  - [Action Helpers](#action-helpers)
47
44
  - [`resource`](#resource)
48
- - [`index {..}` => Array](#index---array)
49
- - [`show {|id| ..}` => Object](#show-id---object)
50
- - [`show {..}` => Object](#show---object)
51
- - [`show_many {|ids| ..}` => Array](#show_many-ids---array)
52
- - [`create {|attr, id| ..}` => id, Object?](#create-attr-id---id-object)
53
- - [`create {|attr| ..}` => id, Object](#create-attr---id-object)
54
- - [`update {|attr| ..}` => Object?](#update-attr---object)
55
- - [`destroy {..}`](#destroy-)
56
45
  - [`has_one`](#has_one)
57
- - [`pluck {..}` => Object](#pluck---object)
58
- - [`prune {..}` => TrueClass?](#prune---trueclass)
59
- - [`graft {|rio| ..}` => TrueClass?](#graft-rio---trueclass)
60
46
  - [`has_many`](#has_many)
61
- - [`fetch {..}` => Array](#fetch---array)
62
- - [`clear {..}` => TrueClass?](#clear---trueclass)
63
- - [`replace {|rios| ..}` => TrueClass?](#replace-rios---trueclass)
64
- - [`merge {|rios| ..}` => TrueClass?](#merge-rios---trueclass)
65
- - [`subtract {|rios| ..}` => TrueClass?](#subtract-rios---trueclass)
66
47
  - [Advanced Usage](#advanced-usage)
67
48
  - [Action Helper Hooks & Utilities](#action-helper-hooks--utilities)
68
49
  - [Authorization](#authorization)
@@ -82,15 +63,19 @@ the {json:api} specification is).
82
63
  - [Side-Unloading Related Resources](#side-unloading-related-resources)
83
64
  - [Side-Loading Relationships](#side-loading-relationships)
84
65
  - [Avoiding Null Foreign Keys](#avoiding-null-foreign-keys)
85
- - [Many-to-One](#many-to-one)
86
- - [One-to-Many](#one-to-many)
87
- - [Many-to-Many](#many-to-many)
88
66
  - [Coalesced Find Requests](#coalesced-find-requests)
89
67
  - [Patchless Clients](#patchless-clients)
68
+ - [Extensions](#extensions)
69
+ - [Sequel](#sequel)
90
70
  - [Application Concerns](#application-concerns)
71
+ - [Performance](#performance)
72
+ - [Public APIs](#public-apis)
73
+ - [Commonly Used](#commonly-used)
74
+ - [Less-Commonly Used](#less-commonly-used)
91
75
  - [Sinja or Sinatra::JSONAPI](#sinja-or-sinatrajsonapi)
92
76
  - [Code Organization](#code-organization)
93
77
  - [Testing](#testing)
78
+ - [Comparison with JSONAPI::Resources](#comparison-with-jsonapiresources)
94
79
  - [Development](#development)
95
80
  - [Contributing](#contributing)
96
81
  - [License](#license)
@@ -100,7 +85,6 @@ the {json:api} specification is).
100
85
  ## Synopsis
101
86
 
102
87
  ```ruby
103
- require 'sinatra'
104
88
  require 'sinatra/jsonapi'
105
89
 
106
90
  resource :posts do
@@ -171,36 +155,12 @@ Or install it yourself as:
171
155
  $ gem install sinja
172
156
  ```
173
157
 
174
- ## Features & Design
175
-
176
- * ORM-agnostic
177
- * Simple role-based authorization
178
- * To-one and to-many relationships and related resources
179
- * Side-loaded relationships on resource creation and update
180
- * Error-handling
181
- * Conflicts (constraint violations)
182
- * Missing records
183
- * Validation failures
184
- * Filtering, sorting, and paging collections
185
- * Plus all the features of [JSONAPI::Serializers][3]!
186
-
187
- Its main competitors in the Ruby space are [ActiveModelSerializers][12] (AMS)
188
- with the JsonApi adapter, [JSONAPI::Resources][8] (JR), and
189
- [jsonapi-utils][26], all of which are designed to work with [Rails][16] and
190
- [ActiveRecord][17]/[ActiveModel][18] (although they may work with [Sequel][13]
191
- via [sequel-rails][14] and Sequel's [`:active_model` plugin][15]). Otherwise,
192
- you might use something like Sinatra, [Roda][20], or [Grape][19] with a
193
- (de)serialization library, your own routes, and a ton of boilerplate. The goal
194
- of this extension is to provide most or all of the boilerplate for a Sintara
195
- application and automate the drawing of routes based on the resource
196
- definitions.
197
-
198
- ### Ol' Blue Eyes is Back
158
+ ## Ol' Blue Eyes is Back
199
159
 
200
160
  The "power" so to speak of implementing this functionality as a Sinatra
201
161
  extension is that all of Sinatra's usual features are available within your
202
- resource definitions. The action helpers blocks get compiled into Sinatra
203
- helpers, and the `resource`, `has_one`, and `has_many` keywords build
162
+ resource definitions. Action helper blocks get compiled into Sinatra helpers,
163
+ and the `resource`, `has_one`, and `has_many` keywords build
204
164
  [Sinatra::Namespace][21] blocks. You can manage caching directives, set
205
165
  headers, and even `halt` (or `not_found`, although such cases are usually
206
166
  handled transparently by returning `nil` values or empty collections from
@@ -295,107 +255,11 @@ class App < Sinatra::Base
295
255
  end
296
256
  ```
297
257
 
298
- ### Public APIs
299
-
300
- Sinja makes a few APIs public to help you work around edge cases in your
301
- application.
302
-
303
- #### Commonly Used
304
-
305
- **can?**
306
- : Takes the symbol of an action helper and returns true if the current user has
307
- access to call that action helper for the current resource using the `role`
308
- helper and role definitions detailed under "Authorization" below.
309
-
310
- **role?**
311
- : Takes a list of role(s) and returns true if it has members in common with the
312
- current user's role(s).
313
-
314
- **sideloaded?**
315
- : Returns true if the request was invoked from another action helper.
316
-
317
- #### Less-Commonly Used
318
-
319
- These are helpful if you want to add some custom routes to your Sinja
320
- application.
321
-
322
- **data**
323
- : Returns the `data` key of the deserialized request payload (with symbolized
324
- names).
325
-
326
- **dedasherize**
327
- : Takes a string or symbol and returns the string or symbol with any and all
328
- dashes transliterated to underscores, and camelCase converted to snake_case.
329
-
330
- **dedasherize_names**
331
- : Takes a hash and returns the hash with its keys dedasherized (deeply).
332
-
333
- **serialize_model**
334
- : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
335
- a serialized model.
336
-
337
- **serialize_model?**
338
- : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
339
- a serialized model if non-`nil`, or the root metadata if present, or a HTTP
340
- status 204.
341
-
342
- **serialize_models**
343
- : Takes an array of models (and optional hash of JSONAPI::Serializers options)
344
- and returns a serialized collection.
345
-
346
- **serialize_models?**
347
- : Takes an array of models (and optional hash of JSONAPI::Serializers options)
348
- and returns a serialized collection if non-empty, or the root metadata if
349
- present, or a HTTP status 204.
350
-
351
- ### Performance
352
-
353
- Although there is some heavy metaprogramming happening at boot time, the end
354
- result is simply a collection of Sinatra namespaces, routes, filters,
355
- conditions, helpers, etc., and Sinja applications should perform as if you had
356
- written them verbosely. The main caveat is that there are quite a few block
357
- closures, which don't perform as well as normal methods in Ruby. Feedback
358
- welcome.
359
-
360
- ### Extensions
361
-
362
- Sinja extensions provide additional helpers, DSL, and configuration, packaging
363
- ORM-specific boilerplate as separate gems. At the moment, the only available
364
- extension is for [Sequel][30], but community contributions are welcome!
365
-
366
- ### Comparison with JSONAPI::Resources
367
-
368
- | Feature | JR | Sinja |
369
- | :-------------- | :------------------------------- | :------------------------------------------------ |
370
- | Serializer | Built-in | [JSONAPI::Serializers][3] |
371
- | Framework | Rails | Sinatra, but easy to mount within others |
372
- | Routing | ActionDispatch::Routing | Mustermann |
373
- | Caching | ActiveSupport::Cache | BYO |
374
- | ORM | ActiveRecord/ActiveModel | BYO |
375
- | Authorization | [Pundit][9] | Role-based |
376
- | Immutability | `immutable` method | Omit mutator action helpers (e.g. `update`) |
377
- | Fetchability | `fetchable_fields` method | Omit attributes in Serializer |
378
- | Creatability | `creatable_fields` method | Handle in `create` action helper or Model\* |
379
- | Updatability | `updatable_fields` method | Handle in `update` action helper or Model\* |
380
- | Sortability | `sortable_fields` method | `sort` helper and `:sort_by` option |
381
- | Default sorting | `default_sort` method | Set default for `params[:sort]` |
382
- | Context | `context` method | Rack middleware (e.g. `env['context']`) |
383
- | Attributes | Define in Model and Resource | Define in Model\* and Serializer |
384
- | Formatting | `:format` attribute keyword | Define attribute as a method in Serialier |
385
- | Relationships | Define in Model and Resource | Define in Model, Resource, and Serializer |
386
- | Filters | `filter(s)` keywords | `filter` helper and `:filter_by` option |
387
- | Default filters | `:default` filter keyword | Set default for `params[:filter]` |
388
- | Pagination | JSONAPI::Paginator | `page` helper and `page_using` configurable |
389
- | Meta | `meta` method | Serializer `:meta` option |
390
- | Primary keys | `resource_key_type` configurable | Serializer `id` method |
391
-
392
- \* - Depending on your ORM.
393
-
394
258
  ## Basic Usage
395
259
 
396
260
  You'll need a database schema and models (using the engine and ORM of your
397
261
  choice) and [serializers][3] to get started. Create a new Sinatra application
398
- (classic or modular) to hold all your {json:api} endpoints and (if modular)
262
+ (classic or modular) to hold all your {json:api} controllers and (if modular)
399
263
  register this extension. Instead of defining routes with `get`, `post`, etc. as
400
264
  you normally would, define `resource` blocks with action helpers and `has_one`
401
265
  and `has_many` relationship blocks (with their own action helpers). Sinja will
@@ -417,7 +281,8 @@ these settings.
417
281
  * Registers [Sinatra::Namespace][21] and [Mustermann][25]
418
282
  * Disables [Rack::Protection][6] (can be reenabled with `enable :protection` or
419
283
  by manually `use`-ing the Rack::Protection middleware)
420
- * Disables static file routes (can be reenabled with `enable :static`)
284
+ * Disables static file routes (can be reenabled with `enable :static`; be sure
285
+ to reenable Rack::Protection::PathTraversal as well)
421
286
  * Disables "classy" error pages (in favor of "classy" {json:api} error documents)
422
287
  * Adds an `:api_json` MIME-type (`application/vnd.api+json`)
423
288
  * Enforces strict checking of the `Accept` and `Content-Type` request headers
@@ -440,12 +305,12 @@ their defaults shown):
440
305
  configure_jsonapi do |c|
441
306
  #c.conflict_exceptions = [] # see "Conflicts" below
442
307
 
308
+ #c.not_found_exceptions = [] # see "Missing Records" below
309
+
443
310
  # see "Validations" below
444
311
  #c.validation_exceptions = []
445
312
  #c.validation_formatter = ->{ [] }
446
313
 
447
- #c.not_found_exceptions = [] # see "Missing Records" below
448
-
449
314
  # see "Authorization" below
450
315
  #c.default_roles = {}
451
316
  #c.default_has_one_roles = {}
@@ -481,8 +346,9 @@ Much of Sinja's advanced functionality (e.g. updating and destroying resources,
481
346
  relationship routes) is dependent upon its ability to locate the corresponding
482
347
  resource for a request. To enable these features, define an ordinary helper
483
348
  method named `find` in your resource definition that takes a single ID argument
484
- and returns the corresponding object. Once defined, a `resource` object will
485
- be made available in any action helpers that operate on a single resource.
349
+ and returns the corresponding object. Once defined, a `resource` object will be
350
+ made available in any action helpers that operate on a single (parent)
351
+ resource.
486
352
 
487
353
  ```ruby
488
354
  resource :posts do
@@ -574,13 +440,6 @@ any given resource block.)
574
440
  Take an array of IDs and return an equally-lengthed array of objects to
575
441
  serialize on the response. See "Coalesced Find Requests" below.
576
442
 
577
- ##### `create {|attr, id| ..}` => id, Object?
578
-
579
- With client-generated IDs: Take a hash of (dedasherized) attributes and a
580
- client-generated ID, create a new resource, and return the ID and optionally
581
- the created resource. (Note that only one or the other `create` action helpers
582
- is allowed in any given resource block.)
583
-
584
443
  ##### `create {|attr| ..}` => id, Object
585
444
 
586
445
  Without client-generated IDs: Take a hash of (dedasherized) attributes, create
@@ -588,6 +447,13 @@ a new resource, and return the server-generated ID and the created resource.
588
447
  (Note that only one or the other `create` action helpers is allowed in any
589
448
  given resource block.)
590
449
 
450
+ ##### `create {|attr, id| ..}` => id, Object?
451
+
452
+ With client-generated IDs: Take a hash of (dedasherized) attributes and a
453
+ client-generated ID, create a new resource, and return the ID and optionally
454
+ the created resource. (Note that only one or the other `create` action helpers
455
+ is allowed in any given resource block.)
456
+
591
457
  ##### `update {|attr| ..}` => Object?
592
458
 
593
459
  Take a hash of (dedasherized) attributes, update `resource`, and optionally
@@ -885,8 +751,8 @@ The {json:api} specification states that any unhandled query parameters should
885
751
  cause the request to abort with HTTP status 400. To enforce this requirement,
886
752
  Sinja maintains a global "whitelist" of acceptable query parameters as well as
887
753
  a per-route whitelist, and interrogates your application to see which features
888
- it supports; for example, a route may allow a `filter` query parameter, but you
889
- may not have defined a `filter` helper.
754
+ it supports; for example, a route may generally allow a `filter` query
755
+ parameter, but you may not have defined a `filter` helper.
890
756
 
891
757
  To let a custom query parameter through to the standard action helpers, add it
892
758
  to the `query_params` configurable with a `nil` value:
@@ -1235,7 +1101,6 @@ end
1235
1101
  The following matrix outlines which combinations of action helpers and
1236
1102
  `:sideload_on` options enable which behaviors:
1237
1103
 
1238
- <small>
1239
1104
  <table>
1240
1105
  <thead>
1241
1106
  <tr>
@@ -1274,7 +1139,6 @@ The following matrix outlines which combinations of action helpers and
1274
1139
  </tr>
1275
1140
  </tbody>
1276
1141
  </table>
1277
- </small>
1278
1142
 
1279
1143
  #### Avoiding Null Foreign Keys
1280
1144
 
@@ -1428,14 +1292,86 @@ class MyApp < Sinatra::Base
1428
1292
  end
1429
1293
  ```
1430
1294
 
1295
+ ## Extensions
1296
+
1297
+ Sinja extensions provide additional helpers, DSL, and ORM-specific boilerplate
1298
+ as separate gems. Community contributions welcome!
1299
+
1300
+ ### Sequel
1301
+
1302
+ Please see [Sinja::Sequel][30] for more information.
1303
+
1431
1304
  ## Application Concerns
1432
1305
 
1306
+ ### Performance
1307
+
1308
+ Although there is some heavy metaprogramming happening at boot time, the end
1309
+ result is simply a collection of Sinatra namespaces, routes, filters,
1310
+ conditions, helpers, etc., and Sinja applications should perform as if you had
1311
+ written them verbosely. The main caveat is that there are quite a few block
1312
+ closures, which don't perform as well as normal methods in Ruby. Feedback
1313
+ welcome.
1314
+
1315
+ ### Public APIs
1316
+
1317
+ Sinja makes a few APIs public to help you work around edge cases in your
1318
+ application.
1319
+
1320
+ #### Commonly Used
1321
+
1322
+ **can?**
1323
+ : Takes the symbol of an action helper and returns true if the current user has
1324
+ access to call that action helper for the current resource using the `role`
1325
+ helper and role definitions detailed under "Authorization" below.
1326
+
1327
+ **role?**
1328
+ : Takes a list of role(s) and returns true if it has members in common with the
1329
+ current user's role(s).
1330
+
1331
+ **sideloaded?**
1332
+ : Returns true if the request was invoked from another action helper.
1333
+
1334
+ #### Less-Commonly Used
1335
+
1336
+ These are helpful if you want to add some custom routes to your Sinja
1337
+ application.
1338
+
1339
+ **data**
1340
+ : Returns the `data` key of the deserialized request payload (with symbolized
1341
+ names).
1342
+
1343
+ **dedasherize**
1344
+ : Takes a string or symbol and returns the string or symbol with any and all
1345
+ dashes transliterated to underscores, and camelCase converted to snake_case.
1346
+
1347
+ **dedasherize_names**
1348
+ : Takes a hash and returns the hash with its keys dedasherized (deeply).
1349
+
1350
+ **serialize_model**
1351
+ : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
1352
+ a serialized model.
1353
+
1354
+ **serialize_model?**
1355
+ : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
1356
+ a serialized model if non-`nil`, or the root metadata if present, or a HTTP
1357
+ status 204.
1358
+
1359
+ **serialize_models**
1360
+ : Takes an array of models (and optional hash of JSONAPI::Serializers options)
1361
+ and returns a serialized collection.
1362
+
1363
+ **serialize_models?**
1364
+ : Takes an array of models (and optional hash of JSONAPI::Serializers options)
1365
+ and returns a serialized collection if non-empty, or the root metadata if
1366
+ present, or a HTTP status 204.
1367
+
1433
1368
  ### Sinja or Sinatra::JSONAPI
1434
1369
 
1435
1370
  Everything is dual-namespaced under both Sinatra::JSONAPI and Sinja, and Sinja
1436
1371
  requires Sinatra::Base, so this:
1437
1372
 
1438
1373
  ```ruby
1374
+ require 'sinatra/base'
1439
1375
  require 'sinatra/jsonapi'
1440
1376
 
1441
1377
  class App < Sinatra::Base
@@ -1527,6 +1463,34 @@ applications will behave according to the {json:api} spec (as long as you
1527
1463
  follow the usage documented in this README) and focus on testing your business
1528
1464
  logic.
1529
1465
 
1466
+ ## Comparison with JSONAPI::Resources
1467
+
1468
+ | Feature | JR | Sinja |
1469
+ | :-------------- | :------------------------------- | :------------------------------------------------ |
1470
+ | Serializer | Built-in | [JSONAPI::Serializers][3] |
1471
+ | Framework | Rails | Sinatra, but easy to mount within others |
1472
+ | Routing | ActionDispatch::Routing | Mustermann |
1473
+ | Caching | ActiveSupport::Cache | BYO |
1474
+ | ORM | ActiveRecord/ActiveModel | BYO |
1475
+ | Authorization | [Pundit][9] | Role-based |
1476
+ | Immutability | `immutable` method | Omit mutator action helpers (e.g. `update`) |
1477
+ | Fetchability | `fetchable_fields` method | Omit attributes in Serializer |
1478
+ | Creatability | `creatable_fields` method | Handle in `create` action helper or Model\* |
1479
+ | Updatability | `updatable_fields` method | Handle in `update` action helper or Model\* |
1480
+ | Sortability | `sortable_fields` method | `sort` helper and `:sort_by` option |
1481
+ | Default sorting | `default_sort` method | Set default for `params[:sort]` |
1482
+ | Context | `context` method | Rack middleware (e.g. `env['context']`) |
1483
+ | Attributes | Define in Model and Resource | Define in Model\* and Serializer |
1484
+ | Formatting | `:format` attribute keyword | Define attribute as a method in Serialier |
1485
+ | Relationships | Define in Model and Resource | Define in Model, Resource, and Serializer |
1486
+ | Filters | `filter(s)` keywords | `filter` helper and `:filter_by` option |
1487
+ | Default filters | `:default` filter keyword | Set default for `params[:filter]` |
1488
+ | Pagination | JSONAPI::Paginator | `page` helper and `page_using` configurable |
1489
+ | Meta | `meta` method | Serializer `:meta` option |
1490
+ | Primary keys | `resource_key_type` configurable | Serializer `id` method |
1491
+
1492
+ \* &ndash; Depending on your ORM.
1493
+
1530
1494
  ## Development
1531
1495
 
1532
1496
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
@@ -1555,7 +1519,7 @@ License](http://opensource.org/licenses/MIT).
1555
1519
  [4]: http://www.rubydoc.info/github/rack/rack/master/Rack/URLMap
1556
1520
  [5]: http://rodauth.jeremyevans.net
1557
1521
  [6]: https://github.com/sinatra/sinatra/tree/master/rack-protection
1558
- [7]: http://jsonapi.org/format/
1522
+ [7]: http://jsonapi.org/format/1.0/
1559
1523
  [8]: https://github.com/cerebris/jsonapi-resources
1560
1524
  [9]: https://github.com/cerebris/jsonapi-resources#authorization
1561
1525
  [10]: http://www.sinatrarb.com/extensions-wild.html
@@ -1574,8 +1538,9 @@ License](http://opensource.org/licenses/MIT).
1574
1538
  [23]: http://jsonapi.org/recommendations/#patchless-clients
1575
1539
  [24]: http://www.rubydoc.info/github/rack/rack/Rack/MethodOverride
1576
1540
  [25]: http://www.sinatrarb.com/mustermann/
1577
- [26]: https://github.com/tiagopog/jsonapi-utils
1541
+ [26]: https://jsonapi-suite.github.io/jsonapi_suite/
1578
1542
  [27]: https://github.com/coryodaniel/munson
1579
1543
  [28]: https://github.com/chingor13/json_api_client
1580
1544
  [29]: https://github.com/brynary/rack-test
1581
1545
  [30]: https://github.com/mwpastore/sinja-sequel
1546
+ [31]: http://jsonapi.org/implementations/#server-libraries-ruby
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- require 'sinatra'
2
+ require 'sinatra' unless defined?(Sinatra)
3
3
  require 'sinja'
4
4
 
5
5
  module Sinatra
@@ -14,71 +14,11 @@ require 'sinja/version'
14
14
 
15
15
  module Sinja
16
16
  MIME_TYPE = 'application/vnd.api+json'
17
- ERROR_CODES = [
18
- BadRequestError,
19
- ForbiddenError,
20
- NotFoundError,
21
- MethodNotAllowedError,
22
- NotAcceptableError,
23
- ConflictError,
24
- UnsupportedTypeError
25
- ].map! { |c| [c.new.http_status, c] }.to_h.tap do |h|
26
- h[422] = UnprocessibleEntityError
27
- end.freeze
28
-
29
- def resource(resource_name, konst=nil, &block)
30
- abort "Must supply proc constant or block for `resource'" \
31
- unless block = (konst if konst.is_a?(Proc)) || block
32
-
33
- resource_name = resource_name.to_s
34
- .pluralize
35
- .dasherize
36
- .to_sym
37
-
38
- # trigger default procs
39
- config = _sinja.resource_config[resource_name]
40
-
41
- namespace "/#{resource_name}" do
42
- define_singleton_method(:_resource_config) { config }
43
- define_singleton_method(:resource_config) { config[:resource] }
44
-
45
- helpers do
46
- define_method(:sanity_check!) do |*args|
47
- super(resource_name, *args)
48
- end
49
- end
50
-
51
- before %r{/(?<id>[^/]+)(?:/.+)?} do |id|
52
- self.resource =
53
- if env.key?('sinja.resource')
54
- env['sinja.resource']
55
- elsif respond_to?(:find)
56
- find(id)
57
- end
58
-
59
- raise NotFoundError, "Resource '#{id}' not found" unless resource
60
- end
61
-
62
- register Resource
63
-
64
- instance_eval(&block)
65
- end
66
- end
67
-
68
- alias_method :resources, :resource
69
-
70
- def sinja
71
- if block_given?
72
- yield _sinja
73
- else
74
- _sinja
75
- end
76
- end
77
-
78
- alias_method :configure_jsonapi, :sinja
79
- def freeze_jsonapi
80
- _sinja.freeze
81
- end
17
+ ERROR_CODES = ObjectSpace.each_object(Class).to_a
18
+ .keep_if { |klass| klass < HttpError }
19
+ .map! { |c| [(c.const_get(:HTTP_STATUS) rescue nil), c] }
20
+ .delete_if { |a| a.first.nil? }
21
+ .to_h.freeze
82
22
 
83
23
  def self.registered(app)
84
24
  app.register Mustermann if Sinatra::VERSION[/^\d+/].to_i < 2
@@ -101,20 +41,23 @@ module Sinja
101
41
  end
102
42
  end
103
43
 
104
- app.set :qcapture do |*index|
44
+ app.set :qcaptures do |*index|
105
45
  condition do
106
46
  @qcaptures ||= []
47
+
107
48
  index.to_h.all? do |key, subkeys|
108
- Hash === params[key.to_s] && params[key.to_s].any? &&
109
- [*subkeys].all? do |subkey|
110
- # TODO: What if deleting one is successful, but not another?
111
- # We'll need to restore the hash to its original state.
112
- @qcaptures << params[key.to_s].delete(subkey.to_s) \
113
- if params[key.to_s].key?(subkey.to_s)
114
- end.tap do |ok|
115
- # If us deleting key(s) causes the hash to be empty, delete it.
116
- params.delete(key.to_s) if ok && params[key.to_s].empty?
117
- end
49
+ key = key.to_s
50
+
51
+ Hash === params[key] && params[key].any? && [*subkeys].all? do |subkey|
52
+ subkey = subkey.to_s
53
+
54
+ # TODO: What if deleting one is successful, but not another?
55
+ # We'll need to restore the hash to its original state.
56
+ @qcaptures << params[key].delete(subkey) if params[key].key?(subkey)
57
+ end.tap do |ok|
58
+ # If us deleting key(s) causes the hash to become empty, delete it.
59
+ params.delete(key) if ok && params[key].empty?
60
+ end
118
61
  end
119
62
  end
120
63
  end
@@ -132,7 +75,7 @@ module Sinja
132
75
  # Ignore interal Sinatra query parameters (e.g. :captures) and any
133
76
  # "known" query parameter set to `nil' in the configurable.
134
77
  next if !env['rack.request.query_hash'].key?(key.to_s) ||
135
- settings._sinja.query_params.fetch(key, :_).nil?
78
+ settings._sinja.query_params.fetch(key, :__NOT_FOUND__).nil?
136
79
 
137
80
  raise BadRequestError, "`#{key}' query parameter not allowed" \
138
81
  unless allow_params.include?(key)
@@ -148,13 +91,13 @@ module Sinja
148
91
 
149
92
  return true if env['sinja.normalized'] == params.object_id
150
93
 
151
- settings._sinja.query_params.each do |key, value|
152
- next if value.nil?
94
+ settings._sinja.query_params.each do |key, default_value|
95
+ next if default_value.nil?
153
96
 
154
97
  if respond_to?("normalize_#{key}_params")
155
- params[key.to_s] = send("normalize_#{key}_params")
98
+ params[key.to_s] = send("normalize_#{key}_params", default_value)
156
99
  else
157
- params[key.to_s] ||= value
100
+ params[key.to_s] ||= default_value
158
101
  end
159
102
  end
160
103
 
@@ -186,6 +129,8 @@ module Sinja
186
129
 
187
130
  if method_defined?(:bad_request?)
188
131
  # This screws up our error-handling logic in Sinatra 2.0, so monkeypatch it.
132
+ # https://github.com/sinatra/sinatra/issues/1211
133
+ # https://github.com/sinatra/sinatra/pull/1212
189
134
  def bad_request?
190
135
  false
191
136
  end
@@ -209,8 +154,8 @@ module Sinja
209
154
  end
210
155
  end
211
156
 
212
- def normalize_filter_params
213
- return {} unless params[:filter]&.any?
157
+ def normalize_filter_params(default_value)
158
+ return default_value unless params[:filter]&.any?
214
159
 
215
160
  raise BadRequestError, "Unsupported `filter' query parameter(s)" \
216
161
  unless respond_to?(:filter)
@@ -230,7 +175,7 @@ module Sinja
230
175
  raise BadRequestError, "Invalid `filter' query parameter(s)"
231
176
  end
232
177
 
233
- def normalize_sort_params
178
+ def normalize_sort_params(_default_value)
234
179
  return {} unless params[:sort]&.any?
235
180
 
236
181
  raise BadRequestError, "Unsupported `sort' query parameter(s)" \
@@ -252,8 +197,8 @@ module Sinja
252
197
  raise BadRequestError, "Invalid `sort' query parameter(s)"
253
198
  end
254
199
 
255
- def normalize_page_params
256
- return {} unless params[:page]&.any?
200
+ def normalize_page_params(default_value)
201
+ return default_value unless params[:page]&.any?
257
202
 
258
203
  raise BadRequestError, "Unsupported `page' query parameter(s)" \
259
204
  unless respond_to?(:page)
@@ -267,25 +212,17 @@ module Sinja
267
212
  return if params[:page].empty?
268
213
 
269
214
  return params[:page] \
270
- if params[:page].keys.to_set.subset?(settings._sinja.page_using.keys.to_set)
215
+ if (params[:page].keys - settings._sinja.page_using.keys).empty?
271
216
 
272
217
  raise BadRequestError, "Invalid `page' query parameter(s)"
273
218
  end
274
219
 
275
220
  def filter_sort_page?(action)
276
- return enum_for(__callee__, action).to_h unless block_given?
221
+ return enum_for(__callee__, action) unless block_given?
277
222
 
278
- if filter = filter_by?(action)
279
- yield :filter, filter
280
- end
281
-
282
- if sort = sort_by?(action)
283
- yield :sort, sort
284
- end
285
-
286
- if page = page_using?
287
- yield :page, page
288
- end
223
+ if filter = filter_by?(action) then yield :filter, filter end
224
+ if sort = sort_by?(action) then yield :sort, sort end
225
+ if page = page_using? then yield :page, page end
289
226
  end
290
227
 
291
228
  def filter_sort_page(collection, opts)
@@ -347,7 +284,7 @@ module Sinja
347
284
  end
348
285
 
349
286
  app.after do
350
- body serialize_response_body if response.ok? || response.created?
287
+ body serialize_response_body if response.successful?
351
288
  end
352
289
 
353
290
  app.not_found do
@@ -376,6 +313,60 @@ module Sinja
376
313
  end
377
314
  end
378
315
 
316
+ def resource(resource_name, konst=nil, &block)
317
+ abort "Must supply proc constant or block for `resource'" \
318
+ unless block = (konst if konst.is_a?(Proc)) || block
319
+
320
+ resource_name = resource_name.to_s
321
+ .pluralize
322
+ .dasherize
323
+ .to_sym
324
+
325
+ # trigger default procs
326
+ config = _sinja.resource_config[resource_name]
327
+
328
+ namespace "/#{resource_name}" do
329
+ define_singleton_method(:_resource_config) { config }
330
+ define_singleton_method(:resource_config) { config[:resource] }
331
+
332
+ helpers do
333
+ define_method(:sanity_check!) do |*args|
334
+ super(resource_name, *args)
335
+ end
336
+ end
337
+
338
+ before %r{/(?<id>[^/]+)(?:/.+)?} do |id|
339
+ self.resource =
340
+ if env.key?('sinja.resource')
341
+ env['sinja.resource']
342
+ elsif respond_to?(:find)
343
+ find(id)
344
+ end
345
+
346
+ raise NotFoundError, "Resource '#{id}' not found" unless resource
347
+ end
348
+
349
+ register Resource
350
+
351
+ instance_eval(&block)
352
+ end
353
+ end
354
+
355
+ alias_method :resources, :resource
356
+
357
+ def sinja
358
+ if block_given?
359
+ yield _sinja
360
+ else
361
+ _sinja
362
+ end
363
+ end
364
+
365
+ alias_method :configure_jsonapi, :sinja
366
+ def freeze_jsonapi
367
+ _sinja.freeze
368
+ end
369
+
379
370
  def self.extended(base)
380
371
  def base.route(*, **opts)
381
372
  opts[:qparams] ||= []
@@ -27,34 +27,50 @@ module Sinja
27
27
  end
28
28
 
29
29
  class BadRequestError < HttpError
30
- def initialize(*args) super(400, *args) end
30
+ HTTP_STATUS = 400
31
+
32
+ def initialize(*args) super(HTTP_STATUS, *args) end
31
33
  end
32
34
 
33
35
  class ForbiddenError < HttpError
34
- def initialize(*args) super(403, *args) end
36
+ HTTP_STATUS = 403
37
+
38
+ def initialize(*args) super(HTTP_STATUS, *args) end
35
39
  end
36
40
 
37
41
  class NotFoundError < HttpError
38
- def initialize(*args) super(404, *args) end
42
+ HTTP_STATUS = 404
43
+
44
+ def initialize(*args) super(HTTP_STATUS, *args) end
39
45
  end
40
46
 
41
47
  class MethodNotAllowedError < HttpError
42
- def initialize(*args) super(405, *args) end
48
+ HTTP_STATUS = 405
49
+
50
+ def initialize(*args) super(HTTP_STATUS, *args) end
43
51
  end
44
52
 
45
53
  class NotAcceptableError < HttpError
46
- def initialize(*args) super(406, *args) end
54
+ HTTP_STATUS = 406
55
+
56
+ def initialize(*args) super(HTTP_STATUS, *args) end
47
57
  end
48
58
 
49
59
  class ConflictError < HttpError
50
- def initialize(*args) super(409, *args) end
60
+ HTTP_STATUS = 409
61
+
62
+ def initialize(*args) super(HTTP_STATUS, *args) end
51
63
  end
52
64
 
53
65
  class UnsupportedTypeError < HttpError
54
- def initialize(*args) super(415, *args) end
66
+ HTTP_STATUS = 415
67
+
68
+ def initialize(*args) super(HTTP_STATUS, *args) end
55
69
  end
56
70
 
57
71
  class UnprocessibleEntityError < HttpError
72
+ HTTP_STATUS = 422
73
+
58
74
  attr_reader :tuples
59
75
 
60
76
  def initialize(tuples=[])
@@ -63,7 +79,7 @@ module Sinja
63
79
  fail 'Tuples not properly formatted' \
64
80
  unless @tuples.any? && @tuples.all? { |t| Array === t && t.length == 2 }
65
81
 
66
- super(422)
82
+ super(HTTP_STATUS)
67
83
  end
68
84
  end
69
85
  end
@@ -26,7 +26,7 @@ module Sinja
26
26
  app.get '', :qparams=>%i[include fields filter sort page], :actions=>:fetch do
27
27
  fsp_opts = filter_sort_page?(:fetch)
28
28
  collection, opts = fetch
29
- collection, pagination = filter_sort_page(collection, fsp_opts)
29
+ collection, pagination = filter_sort_page(collection, fsp_opts.to_h)
30
30
  serialize_models(collection, opts, pagination)
31
31
  end
32
32
 
@@ -13,6 +13,14 @@ require 'sinja/resource_routes'
13
13
 
14
14
  module Sinja
15
15
  module Resource
16
+ def self.registered(app)
17
+ app.helpers Helpers::Relationships do
18
+ attr_accessor :resource
19
+ end
20
+
21
+ app.register ResourceRoutes
22
+ end
23
+
16
24
  def def_action_helper(context, action, allow_opts=[])
17
25
  abort "Action helper names can't overlap with Sinatra DSL" \
18
26
  if Sinatra::Base.respond_to?(action)
@@ -72,14 +80,6 @@ module Sinja
72
80
  end
73
81
  end
74
82
 
75
- def self.registered(app)
76
- app.helpers Helpers::Relationships do
77
- attr_accessor :resource
78
- end
79
-
80
- app.register ResourceRoutes
81
- end
82
-
83
83
  %i[has_one has_many].each do |rel_type|
84
84
  define_method(rel_type) do |rel, &block|
85
85
  rel = rel.to_s
@@ -9,11 +9,11 @@ module Sinja
9
9
  app.def_action_helper(app, :update, :roles)
10
10
  app.def_action_helper(app, :destroy, :roles)
11
11
 
12
- app.head '', :qcapture=>{ :filter=>:id } do
12
+ app.head '', :qcaptures=>{ :filter=>:id } do
13
13
  allow :get=>:show
14
14
  end
15
15
 
16
- app.get '', :qcapture=>{ :filter=>:id }, :qparams=>%i[include fields], :actions=>:show do
16
+ app.get '', :qcaptures=>{ :filter=>:id }, :qparams=>%i[include fields], :actions=>:show do
17
17
  ids = @qcaptures.first # TODO: Get this as a block parameter?
18
18
  ids = ids.split(',') if String === ids
19
19
  ids = [*ids].tap(&:uniq!)
@@ -45,7 +45,7 @@ module Sinja
45
45
  app.get '', :qparams=>%i[include fields filter sort page], :actions=>:index do
46
46
  fsp_opts = filter_sort_page?(:index)
47
47
  collection, opts = index
48
- collection, pagination = filter_sort_page(collection, fsp_opts)
48
+ collection, pagination = filter_sort_page(collection, fsp_opts.to_h)
49
49
  serialize_models(collection, opts, pagination)
50
50
  end
51
51
 
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Sinja
3
- VERSION = '1.2.1'
3
+ VERSION = '1.2.2'
4
4
  end
@@ -10,6 +10,22 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ['mike@oobak.org']
11
11
 
12
12
  spec.summary = 'RESTful, {json:api}-compliant web services in Sinatra'
13
+ spec.description = <<~EOF
14
+ Sinja is a Sinatra extension for quickly building RESTful,
15
+ {json:api}-compliant web services, leveraging the excellent
16
+ JSONAPI::Serializers gem for payload serialization. It enhances Sinatra's
17
+ DSL to enable resource-, relationship-, and role-centric API development,
18
+ and it configures Sinatra with the proper settings, MIME-types, filters,
19
+ conditions, and error-handling.
20
+
21
+ There are many parsing (deserializing) and rendering (serializing) and
22
+ so-called "JSON API" libraries available for Ruby, but relatively few that
23
+ attempt to correctly implement the entire {json:api} specification,
24
+ including routing, request header and query parameter checking, and
25
+ relationship side-loading. Sinja lets you focus on the business logic of
26
+ your applications without worrying about the specification, and without
27
+ pulling in a heavy framework like Rails. It's lightweight and ORM-agnostic!
28
+ EOF
13
29
  spec.homepage = 'https://github.com/mwpastore/sinja'
14
30
  spec.license = 'MIT'
15
31
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinja
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Pastore
@@ -208,7 +208,21 @@ dependencies:
208
208
  - - "~>"
209
209
  - !ruby/object:Gem::Version
210
210
  version: '1.3'
211
- description:
211
+ description: |
212
+ Sinja is a Sinatra extension for quickly building RESTful,
213
+ {json:api}-compliant web services, leveraging the excellent
214
+ JSONAPI::Serializers gem for payload serialization. It enhances Sinatra's
215
+ DSL to enable resource-, relationship-, and role-centric API development,
216
+ and it configures Sinatra with the proper settings, MIME-types, filters,
217
+ conditions, and error-handling.
218
+
219
+ There are many parsing (deserializing) and rendering (serializing) and
220
+ so-called "JSON API" libraries available for Ruby, but relatively few that
221
+ attempt to correctly implement the entire {json:api} specification,
222
+ including routing, request header and query parameter checking, and
223
+ relationship side-loading. Sinja lets you focus on the business logic of
224
+ your applications without worrying about the specification, and without
225
+ pulling in a heavy framework like Rails. It's lightweight and ORM-agnostic!
212
226
  email:
213
227
  - mike@oobak.org
214
228
  executables: []