sinja 1.2.5 → 1.3.0
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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +2 -2
- data/README.md +31 -26
- data/contrib/bench.sh +28 -0
- data/contrib/generate-posts +45 -0
- data/demo-app/Dockerfile +2 -0
- data/demo-app/Gemfile +1 -1
- data/demo-app/app.rb +6 -4
- data/demo-app/classes/author.rb +21 -16
- data/demo-app/classes/base.rb +1 -0
- data/demo-app/classes/comment.rb +11 -12
- data/demo-app/classes/post.rb +12 -11
- data/demo-app/classes/tag.rb +9 -9
- data/demo-app/database.rb +2 -0
- data/lib/sinja.rb +20 -14
- data/lib/sinja/config.rb +4 -0
- data/lib/sinja/helpers/relationships.rb +3 -2
- data/lib/sinja/helpers/serializers.rb +31 -19
- data/lib/sinja/method_override.rb +4 -2
- data/lib/sinja/relationship_routes/has_many.rb +2 -4
- data/lib/sinja/relationship_routes/has_one.rb +2 -4
- data/lib/sinja/resource.rb +9 -8
- data/lib/sinja/resource_routes.rb +6 -8
- data/lib/sinja/version.rb +1 -1
- data/sinja.gemspec +2 -2
- metadata +17 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 913d17d891ed044d0a90c58ec618ee2efaa8a1d2
|
4
|
+
data.tar.gz: d80b13ee2135073aaac421932bd8004ba646dbd1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53b8ccb9ddc7265783ed327554e24a6ebc1fa0f69f54f4f7149dcb435688ee29ced03784e45bd575df682b94899bb4a0f3e90bc09d104f0e1e975fb60dd55dc1
|
7
|
+
data.tar.gz: 7342a8ed796aa8bd8122c815e2dc4cccf66adf8574e7e5343a744b82c623b9315dd722ab88be533195ad0dda4e9cb2508753dc763f7711005844d6bdb981402a
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -100,7 +100,8 @@ resource :posts do
|
|
100
100
|
end
|
101
101
|
|
102
102
|
create do |attr|
|
103
|
-
Post.create(attr)
|
103
|
+
post = Post.create(attr)
|
104
|
+
next post.id, post
|
104
105
|
end
|
105
106
|
end
|
106
107
|
|
@@ -118,8 +119,8 @@ all other {json:api} endpoints returning 404 or 405):
|
|
118
119
|
The resource locator and other action helpers, documented below, enable other
|
119
120
|
endpoints.
|
120
121
|
|
121
|
-
Of course, "modular"-style Sinatra aplications
|
122
|
-
extension:
|
122
|
+
Of course, "modular"-style Sinatra aplications (subclassing Sinatra::Base)
|
123
|
+
require you to register the extension:
|
123
124
|
|
124
125
|
```ruby
|
125
126
|
require 'sinatra/base'
|
@@ -265,13 +266,13 @@ end
|
|
265
266
|
|
266
267
|
You'll need a database schema and models (using the engine and ORM of your
|
267
268
|
choice) and [serializers][3] to get started. Create a new Sinatra application
|
268
|
-
(classic or modular) to hold all your {json:api} controllers and (if
|
269
|
-
register this extension. Instead of defining routes
|
270
|
-
you normally would, define `resource` blocks with
|
271
|
-
and `has_many` relationship blocks (with their own
|
272
|
-
draw and enable the appropriate routes based on the
|
273
|
-
relationships, and action helpers. Other routes will return
|
274
|
-
HTTP statuses: 403, 404, or 405.
|
269
|
+
(classic or modular) to hold all your {json:api} controllers and (if
|
270
|
+
subclassing Sinatra::Base) register this extension. Instead of defining routes
|
271
|
+
with `get`, `post`, etc. as you normally would, define `resource` blocks with
|
272
|
+
action helpers and `has_one` and `has_many` relationship blocks (with their own
|
273
|
+
action helpers). Sinja will draw and enable the appropriate routes based on the
|
274
|
+
defined resources, relationships, and action helpers. Other routes will return
|
275
|
+
the appropriate HTTP statuses: 403, 404, or 405.
|
275
276
|
|
276
277
|
### Configuration
|
277
278
|
|
@@ -373,8 +374,8 @@ end
|
|
373
374
|
```
|
374
375
|
|
375
376
|
This helps Sinja (and Sinatra) disambiguate between standard {json:api} routes
|
376
|
-
used to fetch resources (e.g. `GET /
|
377
|
-
routes (e.g. `GET /
|
377
|
+
used to fetch resources (e.g. `GET /foo-bars/1`) and similarly-structured
|
378
|
+
custom routes (e.g. `GET /foo-bars/recent`).
|
378
379
|
|
379
380
|
### Resource Locators
|
380
381
|
|
@@ -420,7 +421,7 @@ end
|
|
420
421
|
`pluck` and `fetch`.
|
421
422
|
|
422
423
|
* What happens if I define an action helper that requires a resource locator,
|
423
|
-
but
|
424
|
+
but don't define a resource locator?
|
424
425
|
|
425
426
|
Sinja will act as if you had not defined the action helper.
|
426
427
|
|
@@ -468,9 +469,9 @@ Action helpers should be defined within the appropriate block contexts
|
|
468
469
|
(`resource`, `has_one`, or `has_many`) using the given keywords and arguments
|
469
470
|
below. Implicitly return the expected values as described below (as an array if
|
470
471
|
necessary) or use the `next` keyword (instead of `return` or `break`) to exit
|
471
|
-
the action helper. Return values
|
472
|
-
|
473
|
-
|
472
|
+
the action helper. Return values with a question mark below may be omitted
|
473
|
+
entirely. Any helper may additionally return an options hash to pass along to
|
474
|
+
JSONAPI::Serializer.serialize (which will be merged into the global
|
474
475
|
`serializer_opts` described above). The `:include` (see "Side-Unloading Related
|
475
476
|
Resources" below) and `:fields` (for sparse fieldsets) query parameters are
|
476
477
|
automatically passed through to JSONAPI::Serializers.
|
@@ -483,9 +484,7 @@ Finally, some routes will automatically invoke the resource locator on your
|
|
483
484
|
behalf and make the selected resource available to the corresponding action
|
484
485
|
helper(s) as `resource`. For example, the `PATCH /<name>/:id` route looks up
|
485
486
|
the resource with that ID using the `find` resource locator and makes it
|
486
|
-
available to the `update` action helper as `resource`.
|
487
|
-
`DELETE /<name>/:id` route and the `destroy` action helper, and all of the
|
488
|
-
`has_one` and `has_many` action helpers.
|
487
|
+
available to the `update` action helper as `resource`.
|
489
488
|
|
490
489
|
#### `resource`
|
491
490
|
|
@@ -676,8 +675,12 @@ read-only action helpers), but only administrators have access to `create`,
|
|
676
675
|
`update`, etc. (the read-write action helpers). You can have as many roles as
|
677
676
|
you'd like, e.g. a super-administrator role to restrict access to `destroy`.
|
678
677
|
Users can be in one or more roles, and action helpers can be restricted to one
|
679
|
-
or more roles for maximum flexibility.
|
680
|
-
|
678
|
+
or more roles for maximum flexibility.
|
679
|
+
|
680
|
+
The scheme is 100% opt-in. If you prefer to use [Pundit][34] or some other gem
|
681
|
+
to handle authorization, go nuts!
|
682
|
+
|
683
|
+
There are three main components to Sinja's built-in scheme:
|
681
684
|
|
682
685
|
#### `default_roles` configurables
|
683
686
|
|
@@ -1228,14 +1231,14 @@ the built-in `defer` helper to affect the order of operations:
|
|
1228
1231
|
|
1229
1232
|
```ruby
|
1230
1233
|
has_one :author do
|
1231
|
-
graft do |rio|
|
1234
|
+
graft(sideload_on: :create) do |rio|
|
1232
1235
|
resource.author = Author.with_pk!(rio[:id].to_i)
|
1233
1236
|
resource.save_changes
|
1234
1237
|
end
|
1235
1238
|
end
|
1236
1239
|
|
1237
1240
|
has_many :tags do
|
1238
|
-
|
1241
|
+
merge(sideload_on: :create) do |rios|
|
1239
1242
|
defer unless resource.author # come back to this if the author isn't set yet
|
1240
1243
|
|
1241
1244
|
tags = resource.author.preferred_tags
|
@@ -1298,6 +1301,7 @@ resource :photos do
|
|
1298
1301
|
photo = Photo.new
|
1299
1302
|
photo.set(attr)
|
1300
1303
|
photo.save(validate: false) # defer validation
|
1304
|
+
next photo.id, photo
|
1301
1305
|
end
|
1302
1306
|
|
1303
1307
|
has_one :photographer do
|
@@ -1359,7 +1363,7 @@ access to `show`. This feature is experimental.
|
|
1359
1363
|
Collections assembled during coalesced find requests will not be filtered,
|
1360
1364
|
sorted, or paged. The easiest way to limit the number of records that can be
|
1361
1365
|
queried is to define a `show_many` action helper and validate the length of the
|
1362
|
-
passed array in the `before_show_many` hook:
|
1366
|
+
passed array in the `before_show_many` hook. For example, using [Sequel][13]:
|
1363
1367
|
|
1364
1368
|
```ruby
|
1365
1369
|
resource :foos do
|
@@ -1370,7 +1374,7 @@ resource :foos do
|
|
1370
1374
|
end
|
1371
1375
|
|
1372
1376
|
show_many do |ids|
|
1373
|
-
|
1377
|
+
Foo.where_all(id: ids.map!(&:to_i))
|
1374
1378
|
end
|
1375
1379
|
end
|
1376
1380
|
```
|
@@ -1499,7 +1503,7 @@ require 'sinja'
|
|
1499
1503
|
class App < Sinatra::Base
|
1500
1504
|
register Sinja
|
1501
1505
|
|
1502
|
-
sinja do |c|
|
1506
|
+
sinja.configure do |c|
|
1503
1507
|
# ..
|
1504
1508
|
end
|
1505
1509
|
|
@@ -1650,3 +1654,4 @@ License](http://opensource.org/licenses/MIT).
|
|
1650
1654
|
[31]: http://jsonapi.org/implementations/#server-libraries-ruby
|
1651
1655
|
[32]: http://emberjs.com
|
1652
1656
|
[33]: https://github.com/fotinakis/jsonapi-serializers#more-customizations
|
1657
|
+
[34]: https://github.com/elabs/pundit
|
data/contrib/bench.sh
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -eou pipefail
|
4
|
+
|
5
|
+
port=3333
|
6
|
+
posts=100
|
7
|
+
|
8
|
+
echo "Starting Rack..."
|
9
|
+
pushd ../demo-app
|
10
|
+
APP_ENV=test bundle exec ruby app.rb -p $port -e test -q &
|
11
|
+
ruby_pid=$!
|
12
|
+
popd
|
13
|
+
echo "Done."
|
14
|
+
|
15
|
+
function cleanup {
|
16
|
+
kill $ruby_pid
|
17
|
+
wait $ruby_pid
|
18
|
+
}
|
19
|
+
trap cleanup EXIT
|
20
|
+
|
21
|
+
sleep 15
|
22
|
+
echo "Generating Posts..."
|
23
|
+
./generate-posts -count=$posts -url="http://0.0.0.0:$port/posts"
|
24
|
+
echo "Done."
|
25
|
+
|
26
|
+
sleep 15
|
27
|
+
ab -n 10000 -c 1 -k -H 'Accept: application/vnd.api+json' \
|
28
|
+
"http://0.0.0.0:$port/authors/1/posts?page[size]=5&page[number]=3&page[record-count]=$posts&include=tags"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby -s
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'net/http'
|
6
|
+
require 'pp'
|
7
|
+
require 'securerandom'
|
8
|
+
require 'uri'
|
9
|
+
|
10
|
+
abort "usage: $0 -count=<count> -url=<url>" \
|
11
|
+
unless $count && $url
|
12
|
+
|
13
|
+
uri = URI($url)
|
14
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
15
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
16
|
+
request.initialize_http_header(
|
17
|
+
'Accept'=>'application/vnd.api+json',
|
18
|
+
'Content-Type'=>'application/vnd.api+json',
|
19
|
+
'X-Email'=>'all@yourbase.com'
|
20
|
+
)
|
21
|
+
|
22
|
+
Array.new(Integer($count)) do
|
23
|
+
request.body = JSON.generate(data: {
|
24
|
+
type: :posts,
|
25
|
+
id: SecureRandom.urlsafe_base64,
|
26
|
+
attributes: {
|
27
|
+
title: SecureRandom.base64(32),
|
28
|
+
body: SecureRandom.base64(500)
|
29
|
+
},
|
30
|
+
relationships: {
|
31
|
+
author: {
|
32
|
+
data: {
|
33
|
+
id: 1
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
})
|
38
|
+
|
39
|
+
response = http.request(request)
|
40
|
+
|
41
|
+
if response.code.to_i != 201
|
42
|
+
pp JSON.parse(response.body)
|
43
|
+
abort
|
44
|
+
end
|
45
|
+
end
|
data/demo-app/Dockerfile
CHANGED
data/demo-app/Gemfile
CHANGED
data/demo-app/app.rb
CHANGED
@@ -10,12 +10,14 @@ require_relative 'classes/comment'
|
|
10
10
|
require_relative 'classes/post'
|
11
11
|
require_relative 'classes/tag'
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
[Author, Comment, Post, Tag].tap do |model_classes|
|
14
|
+
model_classes.each(&:finalize_associations)
|
15
|
+
model_classes.each(&:freeze)
|
16
|
+
end
|
15
17
|
|
16
18
|
DB.freeze
|
17
19
|
|
18
|
-
configure :development do
|
20
|
+
configure :development, :test do
|
19
21
|
set :server_settings, AccessLog: [] # avoid WEBrick double-logging issue
|
20
22
|
end
|
21
23
|
|
@@ -36,7 +38,7 @@ end
|
|
36
38
|
|
37
39
|
resource :authors, &AuthorController
|
38
40
|
resource :comments, &CommentController
|
39
|
-
resource :posts, :
|
41
|
+
resource :posts, pkre: /[\w-]+/, &PostController
|
40
42
|
resource :tags, &TagController
|
41
43
|
|
42
44
|
freeze_jsonapi
|
data/demo-app/classes/author.rb
CHANGED
@@ -7,14 +7,18 @@ DB.create_table?(:authors) do
|
|
7
7
|
String :real_name
|
8
8
|
String :display_name
|
9
9
|
TrueClass :admin, default: false
|
10
|
-
|
11
|
-
|
10
|
+
Float :created_at
|
11
|
+
Float :updated_at
|
12
12
|
end
|
13
13
|
|
14
14
|
class Author < Sequel::Model
|
15
|
+
plugin :auto_validations, not_null: :presence
|
15
16
|
plugin :boolean_readers
|
17
|
+
plugin :finder
|
16
18
|
plugin :timestamps
|
17
19
|
|
20
|
+
set_allowed_columns :email, :real_name, :display_name, :admin
|
21
|
+
|
18
22
|
finder def self.by_email(arg)
|
19
23
|
where(email: arg)
|
20
24
|
end
|
@@ -35,19 +39,19 @@ end
|
|
35
39
|
|
36
40
|
AuthorController = proc do
|
37
41
|
helpers do
|
42
|
+
def before_create(attr)
|
43
|
+
halt 403, 'Only admins can admin admins' if attr.key?(:admin) && !role?(:superuser)
|
44
|
+
end
|
45
|
+
|
46
|
+
alias before_update before_create
|
47
|
+
|
38
48
|
def find(id)
|
39
|
-
Author
|
49
|
+
Author.with_pk(id.to_i)
|
40
50
|
end
|
41
51
|
|
42
52
|
def role
|
43
53
|
Array(super).tap do |a|
|
44
|
-
a << :
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def settable_fields
|
49
|
-
%i[email real_name display_name].tap do |a|
|
50
|
-
a << :admin if role?(:superuser)
|
54
|
+
a << :self if resource == current_user
|
51
55
|
end
|
52
56
|
end
|
53
57
|
end
|
@@ -63,16 +67,17 @@ AuthorController = proc do
|
|
63
67
|
end
|
64
68
|
|
65
69
|
create do |attr|
|
66
|
-
author = Author.new
|
67
|
-
author.
|
68
|
-
next_pk author
|
70
|
+
author = Author.new(attr)
|
71
|
+
author.save(validate: false)
|
72
|
+
next_pk author
|
69
73
|
end
|
70
74
|
|
71
|
-
update(roles: %i[
|
72
|
-
resource.
|
75
|
+
update(roles: %i[self superuser]) do |attr|
|
76
|
+
resource.set(attr)
|
77
|
+
resource.save_changes(validate: false)
|
73
78
|
end
|
74
79
|
|
75
|
-
destroy(roles: %i[
|
80
|
+
destroy(roles: %i[self superuser]) do
|
76
81
|
resource.destroy
|
77
82
|
end
|
78
83
|
|
data/demo-app/classes/base.rb
CHANGED
data/demo-app/classes/comment.rb
CHANGED
@@ -3,16 +3,19 @@ require_relative 'base'
|
|
3
3
|
|
4
4
|
DB.create_table?(:comments) do
|
5
5
|
primary_key :id
|
6
|
-
foreign_key :author_id, :authors, on_delete: :cascade
|
7
|
-
foreign_key :post_slug, :posts, type: String, on_delete: :cascade, on_update: :cascade
|
6
|
+
foreign_key :author_id, :authors, index: true, on_delete: :cascade
|
7
|
+
foreign_key :post_slug, :posts, type: String, index: true, on_delete: :cascade, on_update: :cascade
|
8
8
|
String :body, text: true, null: false
|
9
|
-
|
10
|
-
|
9
|
+
Float :created_at
|
10
|
+
Float :updated_at
|
11
11
|
end
|
12
12
|
|
13
13
|
class Comment < Sequel::Model
|
14
|
+
plugin :auto_validations, not_null: :presence
|
14
15
|
plugin :timestamps
|
15
16
|
|
17
|
+
set_allowed_columns :body
|
18
|
+
|
16
19
|
many_to_one :author
|
17
20
|
many_to_one :post
|
18
21
|
|
@@ -32,7 +35,7 @@ end
|
|
32
35
|
CommentController = proc do
|
33
36
|
helpers do
|
34
37
|
def find(id)
|
35
|
-
Comment
|
38
|
+
Comment.with_pk(id.to_i)
|
36
39
|
end
|
37
40
|
|
38
41
|
def role
|
@@ -40,10 +43,6 @@ CommentController = proc do
|
|
40
43
|
a << :owner if resource&.author == current_user
|
41
44
|
end
|
42
45
|
end
|
43
|
-
|
44
|
-
def settable_fields
|
45
|
-
%i[body]
|
46
|
-
end
|
47
46
|
end
|
48
47
|
|
49
48
|
show do
|
@@ -51,14 +50,14 @@ CommentController = proc do
|
|
51
50
|
end
|
52
51
|
|
53
52
|
create(roles: :logged_in) do |attr|
|
54
|
-
comment = Comment.new
|
55
|
-
comment.set_fields(attr, settable_fields)
|
53
|
+
comment = Comment.new(attr)
|
56
54
|
comment.save(validate: false)
|
57
55
|
next_pk comment
|
58
56
|
end
|
59
57
|
|
60
58
|
update(roles: %i[owner superuser]) do |attr|
|
61
|
-
resource.
|
59
|
+
resource.set(attr)
|
60
|
+
resource.save_changes(validate: false)
|
62
61
|
end
|
63
62
|
|
64
63
|
destroy(roles: %i[owner superuser]) do
|
data/demo-app/classes/post.rb
CHANGED
@@ -3,17 +3,20 @@ require_relative 'base'
|
|
3
3
|
|
4
4
|
DB.create_table?(:posts) do
|
5
5
|
String :slug, primary_key: true
|
6
|
-
foreign_key :author_id, :authors, on_delete: :cascade
|
6
|
+
foreign_key :author_id, :authors, index: true, on_delete: :cascade
|
7
7
|
String :title, null: false
|
8
8
|
String :body, text: true, null: false
|
9
|
-
|
10
|
-
|
9
|
+
Float :created_at
|
10
|
+
Float :updated_at
|
11
11
|
end
|
12
12
|
|
13
13
|
class Post < Sequel::Model
|
14
|
+
plugin :auto_validations, not_null: :presence
|
14
15
|
plugin :timestamps
|
15
16
|
plugin :update_primary_key
|
16
17
|
|
18
|
+
set_allowed_columns :slug, :title, :body
|
19
|
+
|
17
20
|
unrestrict_primary_key # allow client-generated slugs
|
18
21
|
|
19
22
|
# jdbc-sqlite3 reports unexpected record counts with cascading updates, which
|
@@ -45,7 +48,7 @@ end
|
|
45
48
|
PostController = proc do
|
46
49
|
helpers do
|
47
50
|
def find(slug)
|
48
|
-
Post
|
51
|
+
Post.with_pk(slug.to_s)
|
49
52
|
end
|
50
53
|
|
51
54
|
def role
|
@@ -53,10 +56,6 @@ PostController = proc do
|
|
53
56
|
a << :owner if resource&.author == current_user
|
54
57
|
end
|
55
58
|
end
|
56
|
-
|
57
|
-
def settable_fields
|
58
|
-
%i[slug title body]
|
59
|
-
end
|
60
59
|
end
|
61
60
|
|
62
61
|
show do
|
@@ -72,14 +71,16 @@ PostController = proc do
|
|
72
71
|
end
|
73
72
|
|
74
73
|
create(roles: :logged_in) do |attr, slug|
|
75
|
-
|
76
|
-
|
74
|
+
attr[:slug] = slug
|
75
|
+
|
76
|
+
post = Post.new(attr)
|
77
77
|
post.save(validate: false)
|
78
78
|
next_pk post
|
79
79
|
end
|
80
80
|
|
81
81
|
update(roles: %i[owner superuser]) do |attr|
|
82
|
-
resource.
|
82
|
+
resource.set(attr)
|
83
|
+
resource.save_changes(validate: false)
|
83
84
|
end
|
84
85
|
|
85
86
|
destroy(roles: %i[owner superuser]) do
|
data/demo-app/classes/tag.rb
CHANGED
@@ -5,6 +5,7 @@ require_relative 'post' # make sure we create the posts table before the join ta
|
|
5
5
|
DB.create_table?(:tags) do
|
6
6
|
primary_key :id
|
7
7
|
String :name, null: false, unique: true
|
8
|
+
String :description
|
8
9
|
end
|
9
10
|
|
10
11
|
DB.create_table?(:posts_tags) do
|
@@ -15,11 +16,15 @@ DB.create_table?(:posts_tags) do
|
|
15
16
|
end
|
16
17
|
|
17
18
|
class Tag < Sequel::Model
|
19
|
+
plugin :auto_validations, not_null: :presence
|
20
|
+
|
21
|
+
set_allowed_columns :name, :description
|
22
|
+
|
18
23
|
many_to_many :posts, right_key: :post_slug
|
19
24
|
end
|
20
25
|
|
21
26
|
class TagSerializer < BaseSerializer
|
22
|
-
|
27
|
+
attributes :name, :description
|
23
28
|
|
24
29
|
has_many :posts
|
25
30
|
end
|
@@ -27,23 +32,18 @@ end
|
|
27
32
|
TagController = proc do
|
28
33
|
helpers do
|
29
34
|
def find(id)
|
30
|
-
Tag
|
31
|
-
end
|
32
|
-
|
33
|
-
def settable_fields
|
34
|
-
%i[name]
|
35
|
+
Tag.with_pk(id.to_i)
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
39
|
show
|
39
40
|
|
40
|
-
index(sort_by: :name, filter_by: :name) do
|
41
|
+
index(sort_by: :name, filter_by: [:name, :description]) do
|
41
42
|
Tag.dataset
|
42
43
|
end
|
43
44
|
|
44
45
|
create(roles: :logged_in) do |attr|
|
45
|
-
tag = Tag.new
|
46
|
-
tag.set_fields(attr, settable_fields)
|
46
|
+
tag = Tag.new(attr)
|
47
47
|
tag.save(validate: false)
|
48
48
|
next_pk tag
|
49
49
|
end
|
data/demo-app/database.rb
CHANGED
data/lib/sinja.rb
CHANGED
@@ -17,10 +17,13 @@ module Sinja
|
|
17
17
|
ERROR_CODES = ObjectSpace.each_object(Class).to_a
|
18
18
|
.keep_if { |klass| klass < HttpError }
|
19
19
|
.map! { |c| [(c.const_get(:HTTP_STATUS) rescue nil), c] }
|
20
|
-
.delete_if { |
|
20
|
+
.delete_if { |s, _| s.nil? }
|
21
21
|
.to_h.freeze
|
22
22
|
|
23
23
|
def self.registered(app)
|
24
|
+
abort "Sinatra::JSONAPI (Sinja) is already registered on #{app}!" \
|
25
|
+
if app.respond_to?(:_sinja)
|
26
|
+
|
24
27
|
app.register Sinatra::Namespace
|
25
28
|
|
26
29
|
app.disable :protection, :show_exceptions, :static
|
@@ -107,9 +110,7 @@ module Sinja
|
|
107
110
|
end
|
108
111
|
end
|
109
112
|
|
110
|
-
app.set
|
111
|
-
condition { nullish.(data) }
|
112
|
-
end
|
113
|
+
app.set(:on) { |block| condition(&block) }
|
113
114
|
|
114
115
|
app.mime_type :api_json, MIME_TYPE
|
115
116
|
|
@@ -170,9 +171,9 @@ module Sinja
|
|
170
171
|
def filter_by?(action)
|
171
172
|
return if params[:filter].empty?
|
172
173
|
|
173
|
-
|
174
|
-
|
175
|
-
|
174
|
+
filter = params[:filter].map { |k, v| [k.to_sym, v] }.to_h
|
175
|
+
filter_by = settings.resource_config[action][:filter_by]
|
176
|
+
return filter if filter_by.empty? || filter_by.superset?(filter.keys.to_set)
|
176
177
|
|
177
178
|
raise BadRequestError, "Invalid `filter' query parameter(s)"
|
178
179
|
end
|
@@ -192,9 +193,9 @@ module Sinja
|
|
192
193
|
def sort_by?(action)
|
193
194
|
return if params[:sort].empty?
|
194
195
|
|
195
|
-
|
196
|
-
|
197
|
-
|
196
|
+
sort = params[:sort].map { |k, v| [k.to_sym, v] }.to_h
|
197
|
+
sort_by = settings.resource_config[action][:sort_by]
|
198
|
+
return sort if sort_by.empty? || sort_by.superset?(sort.keys.to_set)
|
198
199
|
|
199
200
|
raise BadRequestError, "Invalid `sort' query parameter(s)"
|
200
201
|
end
|
@@ -213,8 +214,8 @@ module Sinja
|
|
213
214
|
def page_using?
|
214
215
|
return if params[:page].empty?
|
215
216
|
|
216
|
-
|
217
|
-
|
217
|
+
page = params[:page].map { |k, v| [k.to_sym, v] }.to_h
|
218
|
+
return page if (page.keys - settings._sinja.page_using.keys).empty?
|
218
219
|
|
219
220
|
raise BadRequestError, "Invalid `page' query parameter(s)"
|
220
221
|
end
|
@@ -272,7 +273,7 @@ module Sinja
|
|
272
273
|
|
273
274
|
app.before do
|
274
275
|
unless sideloaded?
|
275
|
-
raise NotAcceptableError unless request.preferred_type.entry == MIME_TYPE
|
276
|
+
raise NotAcceptableError unless request.preferred_type.entry == MIME_TYPE || request.options?
|
276
277
|
raise UnsupportedTypeError if content? && (
|
277
278
|
request.media_type != MIME_TYPE || request.media_type_params.keys.any? { |k| k != 'charset' }
|
278
279
|
)
|
@@ -362,13 +363,18 @@ module Sinja
|
|
362
363
|
|
363
364
|
def sinja
|
364
365
|
if block_given?
|
366
|
+
warn "DEPRECATED: Pass a block to `sinja.configure' instead."
|
367
|
+
|
365
368
|
yield _sinja
|
366
369
|
else
|
367
370
|
_sinja
|
368
371
|
end
|
369
372
|
end
|
370
373
|
|
371
|
-
|
374
|
+
def configure_jsonapi(&block)
|
375
|
+
_sinja.configure(&block)
|
376
|
+
end
|
377
|
+
|
372
378
|
def freeze_jsonapi
|
373
379
|
_sinja.freeze
|
374
380
|
end
|
data/lib/sinja/config.rb
CHANGED
@@ -5,9 +5,10 @@ module Sinja
|
|
5
5
|
module Helpers
|
6
6
|
module Relationships
|
7
7
|
def dispatch_relationship_request(id, path, **opts)
|
8
|
-
path_info = request.
|
9
|
-
path_info << "/#{id}" unless
|
8
|
+
path_info = request.path_info.dup
|
9
|
+
path_info << "/#{id}" unless path_info.end_with?("/#{id}")
|
10
10
|
path_info << "/relationships/#{path}"
|
11
|
+
path_info.freeze
|
11
12
|
|
12
13
|
fakenv = env.merge 'PATH_INFO'=>path_info
|
13
14
|
fakenv['REQUEST_METHOD'] = opts[:method].to_s.tap(&:upcase!) if opts[:method]
|
@@ -37,7 +37,7 @@ module Sinja
|
|
37
37
|
|
38
38
|
def serialize_response_body
|
39
39
|
case response.content_type[/^[^;]+/]
|
40
|
-
when
|
40
|
+
when /json$/, /javascript$/
|
41
41
|
JSON.send(settings._sinja.json_generator, response.body)
|
42
42
|
else
|
43
43
|
Array(response.body).map!(&:to_s)
|
@@ -99,7 +99,6 @@ module Sinja
|
|
99
99
|
|
100
100
|
def serialize_model(model=nil, options={})
|
101
101
|
options[:is_collection] = false
|
102
|
-
options[:skip_collection_check] = defined?(::Sequel) && model.is_a?(::Sequel::Model)
|
103
102
|
options[:include] = include_exclude!(options)
|
104
103
|
options[:fields] ||= params[:fields] unless params[:fields].empty?
|
105
104
|
options = settings._sinja.serializer_opts.merge(options)
|
@@ -174,7 +173,6 @@ module Sinja
|
|
174
173
|
|
175
174
|
def serialize_linkage(model, rel, options={})
|
176
175
|
options[:is_collection] = false
|
177
|
-
options[:skip_collection_check] = defined?(::Sequel::Model) && model.is_a?(::Sequel::Model)
|
178
176
|
options = settings._sinja.serializer_opts.merge(options)
|
179
177
|
|
180
178
|
options[:serializer] ||= ::JSONAPI::Serializer.find_serializer_class(model, options)
|
@@ -217,23 +215,37 @@ module Sinja
|
|
217
215
|
|
218
216
|
abody = Array(body)
|
219
217
|
error_hashes =
|
220
|
-
if abody.
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
:
|
234
|
-
|
218
|
+
if abody.all? { |error| error.is_a?(Hash) }
|
219
|
+
# `halt' with a hash or array of hashes
|
220
|
+
abody.flat_map(&method(:error_hash))
|
221
|
+
elsif not_found?
|
222
|
+
# `not_found' or `halt 404'
|
223
|
+
message = abody.first.to_s
|
224
|
+
error_hash \
|
225
|
+
:title=>'Not Found Error',
|
226
|
+
:detail=>(message unless message == '<h1>Not Found</h1>')
|
227
|
+
elsif abody.all? { |error| error.is_a?(String) }
|
228
|
+
# Try to repackage a JSON-encoded middleware error
|
229
|
+
begin
|
230
|
+
abody.flat_map do |error|
|
231
|
+
miderr = JSON.parse(error, :symbolize_names=>true)
|
232
|
+
error_hash \
|
233
|
+
:title=>'Middleware Error',
|
234
|
+
:detail=>(miderr.key?(:error) ? miderr[:error] : error)
|
235
|
+
end
|
236
|
+
rescue JSON::ParserError
|
237
|
+
abody.flat_map do |error|
|
238
|
+
error_hash \
|
239
|
+
:title=>'Middleware Error',
|
240
|
+
:detail=>error
|
241
|
+
end
|
235
242
|
end
|
236
|
-
|
243
|
+
else
|
244
|
+
# `halt'
|
245
|
+
error_hash \
|
246
|
+
:title=>'Unknown Error(s)',
|
247
|
+
:detail=>abody.to_s
|
248
|
+
end unless abody.empty?
|
237
249
|
|
238
250
|
# Exception already contains formatted errors
|
239
251
|
error_hashes ||= env['sinatra.error'].error_hashes \
|
@@ -6,8 +6,10 @@ module Sinja
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def call(env)
|
9
|
-
env['REQUEST_METHOD'] = env['HTTP_X_HTTP_METHOD_OVERRIDE']
|
10
|
-
|
9
|
+
env['REQUEST_METHOD'] = env['HTTP_X_HTTP_METHOD_OVERRIDE'] \
|
10
|
+
if env.key?('HTTP_X_HTTP_METHOD_OVERRIDE') \
|
11
|
+
&& env['REQUEST_METHOD'] == 'POST' \
|
12
|
+
&& env['HTTP_X_HTTP_METHOD_OVERRIDE'].tap(&:upcase!) == 'PATCH'
|
11
13
|
|
12
14
|
@app.call(env)
|
13
15
|
end
|
@@ -17,9 +17,7 @@ module Sinja
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
app.get '', :actions=>:show do
|
21
|
-
pass unless relationship_link?
|
22
|
-
|
20
|
+
app.get '', :on=>proc { relationship_link? }, :actions=>:show do
|
23
21
|
serialize_linkage
|
24
22
|
end
|
25
23
|
|
@@ -30,7 +28,7 @@ module Sinja
|
|
30
28
|
serialize_models(collection, opts, pagination)
|
31
29
|
end
|
32
30
|
|
33
|
-
app.patch '', :
|
31
|
+
app.patch '', :on=>proc { data.empty? }, :actions=>:clear do
|
34
32
|
serialize_linkages?(*clear)
|
35
33
|
end
|
36
34
|
|
@@ -15,9 +15,7 @@ module Sinja
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
app.get '', :actions=>:show do
|
19
|
-
pass unless relationship_link?
|
20
|
-
|
18
|
+
app.get '', :on=>proc { relationship_link? }, :actions=>:show do
|
21
19
|
serialize_linkage
|
22
20
|
end
|
23
21
|
|
@@ -25,7 +23,7 @@ module Sinja
|
|
25
23
|
serialize_model(*pluck)
|
26
24
|
end
|
27
25
|
|
28
|
-
app.patch '', :
|
26
|
+
app.patch '', :on=>proc { data.nil? }, :actions=>:prune do
|
29
27
|
serialize_linkage?(*prune)
|
30
28
|
end
|
31
29
|
|
data/lib/sinja/resource.rb
CHANGED
@@ -13,6 +13,13 @@ require 'sinja/resource_routes'
|
|
13
13
|
|
14
14
|
module Sinja
|
15
15
|
module Resource
|
16
|
+
ARITIES = {
|
17
|
+
:create=>2,
|
18
|
+
:index=>-1,
|
19
|
+
:fetch=>-1,
|
20
|
+
:show_many=>-1
|
21
|
+
}.tap { |h| h.default = 1 }.freeze
|
22
|
+
|
16
23
|
def self.registered(app)
|
17
24
|
app.helpers Helpers::Relationships do
|
18
25
|
attr_accessor :resource
|
@@ -39,19 +46,13 @@ module Sinja
|
|
39
46
|
proc { resource } if method_defined?(:find)
|
40
47
|
end
|
41
48
|
|
42
|
-
|
43
|
-
required_arity = {
|
44
|
-
:create=>2,
|
45
|
-
:index=>-1,
|
46
|
-
:fetch=>-1,
|
47
|
-
:show_many=>-1
|
48
|
-
}.freeze[action] || 1
|
49
|
+
required_arity = ARITIES[action]
|
49
50
|
|
50
51
|
define_method(action) do |*args|
|
51
52
|
raise ArgumentError, "Unexpected argument(s) for `#{action}' action helper" \
|
52
53
|
unless args.length == block.arity
|
53
54
|
|
54
|
-
public_send("before_#{action}", *args) \
|
55
|
+
public_send("before_#{action}", *args.take(method("before_#{action}").arity.abs)) \
|
55
56
|
if respond_to?("before_#{action}")
|
56
57
|
|
57
58
|
case result = instance_exec(*args, &block)
|
@@ -18,7 +18,7 @@ module Sinja
|
|
18
18
|
ids = ids.split(',') if ids.instance_of?(String)
|
19
19
|
ids = Array(ids).tap(&:uniq!)
|
20
20
|
|
21
|
-
|
21
|
+
collection, opts =
|
22
22
|
if respond_to?(:show_many)
|
23
23
|
show_many(ids)
|
24
24
|
else
|
@@ -33,9 +33,9 @@ module Sinja
|
|
33
33
|
end
|
34
34
|
|
35
35
|
raise NotFoundError, "Resource(s) not found" \
|
36
|
-
unless ids.length ==
|
36
|
+
unless ids.length == collection.length
|
37
37
|
|
38
|
-
serialize_models(
|
38
|
+
serialize_models(collection, opts)
|
39
39
|
end
|
40
40
|
|
41
41
|
app.options '' do
|
@@ -58,11 +58,9 @@ module Sinja
|
|
58
58
|
begin
|
59
59
|
create(*[attributes].tap { |a| a << data[:id] if data.key?(:id) })
|
60
60
|
rescue ArgumentError
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
raise ForbiddenError, 'Client-generated ID not provided'
|
65
|
-
end
|
61
|
+
kind = data.key?(:id) ? 'supported' : 'provided'
|
62
|
+
|
63
|
+
raise ForbiddenError, "Client-generated ID not #{kind}"
|
66
64
|
end
|
67
65
|
|
68
66
|
dispatch_relationship_requests!(id, :from=>:create, :methods=>{ :has_many=>:post })
|
data/lib/sinja/version.rb
CHANGED
data/sinja.gemspec
CHANGED
@@ -48,9 +48,9 @@ Gem::Specification.new do |spec|
|
|
48
48
|
spec.add_development_dependency 'minitest', '~> 5.9'
|
49
49
|
spec.add_development_dependency 'minitest-hooks', '~> 1.4'
|
50
50
|
#spec.add_development_dependency 'munson', '~> 0.4' # in Gemfile
|
51
|
-
spec.add_development_dependency 'rack-test', '~> 0.
|
51
|
+
spec.add_development_dependency 'rack-test', '~> 0.7.0'
|
52
52
|
spec.add_development_dependency 'rake', '~> 12.0'
|
53
|
-
spec.add_development_dependency 'sequel', '
|
53
|
+
spec.add_development_dependency 'sequel', '>= 4.49', '< 6'
|
54
54
|
#spec.add_development_dependency 'sinja-sequel', '~> 0.1' # in Gemfile
|
55
55
|
spec.add_development_dependency 'sqlite3', '~> 1.3' if !defined?(JRUBY_VERSION)
|
56
56
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sinja
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Pastore
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-10-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -158,14 +158,14 @@ dependencies:
|
|
158
158
|
requirements:
|
159
159
|
- - "~>"
|
160
160
|
- !ruby/object:Gem::Version
|
161
|
-
version:
|
161
|
+
version: 0.7.0
|
162
162
|
type: :development
|
163
163
|
prerelease: false
|
164
164
|
version_requirements: !ruby/object:Gem::Requirement
|
165
165
|
requirements:
|
166
166
|
- - "~>"
|
167
167
|
- !ruby/object:Gem::Version
|
168
|
-
version:
|
168
|
+
version: 0.7.0
|
169
169
|
- !ruby/object:Gem::Dependency
|
170
170
|
name: rake
|
171
171
|
requirement: !ruby/object:Gem::Requirement
|
@@ -184,16 +184,22 @@ dependencies:
|
|
184
184
|
name: sequel
|
185
185
|
requirement: !ruby/object:Gem::Requirement
|
186
186
|
requirements:
|
187
|
-
- - "
|
187
|
+
- - ">="
|
188
188
|
- !ruby/object:Gem::Version
|
189
|
-
version: '4.
|
189
|
+
version: '4.49'
|
190
|
+
- - "<"
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: '6'
|
190
193
|
type: :development
|
191
194
|
prerelease: false
|
192
195
|
version_requirements: !ruby/object:Gem::Requirement
|
193
196
|
requirements:
|
194
|
-
- - "
|
197
|
+
- - ">="
|
195
198
|
- !ruby/object:Gem::Version
|
196
|
-
version: '4.
|
199
|
+
version: '4.49'
|
200
|
+
- - "<"
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
version: '6'
|
197
203
|
- !ruby/object:Gem::Dependency
|
198
204
|
name: sqlite3
|
199
205
|
requirement: !ruby/object:Gem::Requirement
|
@@ -238,6 +244,8 @@ files:
|
|
238
244
|
- Rakefile
|
239
245
|
- bin/console
|
240
246
|
- bin/setup
|
247
|
+
- contrib/bench.sh
|
248
|
+
- contrib/generate-posts
|
241
249
|
- demo-app/.dockerignore
|
242
250
|
- demo-app/Dockerfile
|
243
251
|
- demo-app/Gemfile
|
@@ -286,7 +294,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
286
294
|
version: '0'
|
287
295
|
requirements: []
|
288
296
|
rubyforge_project:
|
289
|
-
rubygems_version: 2.6.
|
297
|
+
rubygems_version: 2.6.13
|
290
298
|
signing_key:
|
291
299
|
specification_version: 4
|
292
300
|
summary: RESTful, {json:api}-compliant web services in Sinatra
|