sinja 1.2.5 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|