sinja 1.2.1 → 1.2.2

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 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: []