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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9054bed54d4dd7bf2e166d3708c3726ba9c92d89
4
- data.tar.gz: 1ade5511cf29f452d7f3d4555e1a153ea895c9a7
3
+ metadata.gz: 913d17d891ed044d0a90c58ec618ee2efaa8a1d2
4
+ data.tar.gz: d80b13ee2135073aaac421932bd8004ba646dbd1
5
5
  SHA512:
6
- metadata.gz: 8cf864d75cf3232f2ab1f2699ea61e02db3a7503a446b07a6d1fe38261625b911f316e8a8851b44f13b7539d39b96a537699964c516918dd9676af0c6cf98ff0
7
- data.tar.gz: 980b31489e1efd32956b33691bf78eb6c8cd1097a76af04cec58ab03abb567f4dd0d59ee42592079de8aa224b70beb29a3e7b6f7ee7bfb56c6453d33b09c056f
6
+ metadata.gz: 53b8ccb9ddc7265783ed327554e24a6ebc1fa0f69f54f4f7149dcb435688ee29ced03784e45bd575df682b94899bb4a0f3e90bc09d104f0e1e975fb60dd55dc1
7
+ data.tar.gz: 7342a8ed796aa8bd8122c815e2dc4cccf66adf8574e7e5343a744b82c623b9315dd722ab88be533195ad0dda4e9cb2508753dc763f7711005844d6bdb981402a
data/.gitignore CHANGED
@@ -8,3 +8,4 @@ Gemfile.lock
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  /test*.rb
11
+ .ruby-version
@@ -1,8 +1,8 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.3
5
- - 2.4.0
4
+ - 2.3.4
5
+ - 2.4.1
6
6
  - ruby-head
7
7
  - jruby-9.1.7.0
8
8
  - jruby-head
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 require you to register the
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 modular)
269
- register this extension. Instead of defining routes with `get`, `post`, etc. as
270
- you normally would, define `resource` blocks with action helpers and `has_one`
271
- and `has_many` relationship blocks (with their own action helpers). Sinja will
272
- draw and enable the appropriate routes based on the defined resources,
273
- relationships, and action helpers. Other routes will return the appropriate
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 /foos/1`) and similarly-structured custom
377
- routes (e.g. `GET /foos/recent`).
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 no resource locator?
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 marked with a question mark below may be
472
- omitted entirely. Any helper may additionally return an options hash to pass
473
- along to JSONAPI::Serializer.serialize (which will be merged into the global
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`. The same goes for the
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. There are three main components to the
680
- scheme:
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
- replace do |rios|
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
@@ -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
@@ -6,6 +6,8 @@ RUN apk --no-cache upgrade
6
6
  RUN apk --no-cache add \
7
7
  sqlite-libs
8
8
 
9
+ RUN gem update --system
10
+
9
11
  COPY Gemfile /app/
10
12
  RUN apk --no-cache add --virtual build-dependencies \
11
13
  build-base \
@@ -2,7 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  gem 'jdbc-sqlite3', '~> 3.8', :platform=>:jruby
4
4
  gem 'json', '~> 2.0'
5
- gem 'sequel', '~> 4.44'
5
+ gem 'sequel', '~> 4.46'
6
6
  gem 'sinatra', '>= 2.0.0.beta2', '< 3'
7
7
  gem 'sinatra-contrib', '>= 2.0.0.beta2', '< 3'
8
8
  gem 'sinja',
@@ -10,12 +10,14 @@ require_relative 'classes/comment'
10
10
  require_relative 'classes/post'
11
11
  require_relative 'classes/tag'
12
12
 
13
- Sequel::Model.finalize_associations
14
- Sequel::Model.freeze
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, :pkre=>/[\w-]+/, &PostController
41
+ resource :posts, pkre: /[\w-]+/, &PostController
40
42
  resource :tags, &TagController
41
43
 
42
44
  freeze_jsonapi
@@ -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
- DateTime :created_at
11
- DateTime :updated_at
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[id.to_i]
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 << :myself if resource == current_user
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.set_fields(attr, settable_fields)
68
- next_pk author.save(validate: false)
70
+ author = Author.new(attr)
71
+ author.save(validate: false)
72
+ next_pk author
69
73
  end
70
74
 
71
- update(roles: %i[myself superuser]) do |attr|
72
- resource.update_fields(attr, settable_fields, validate: false, missing: :skip)
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[myself superuser]) do
80
+ destroy(roles: %i[self superuser]) do
76
81
  resource.destroy
77
82
  end
78
83
 
@@ -8,3 +8,4 @@ end
8
8
 
9
9
  Sequel::Model.plugin :tactical_eager_loading
10
10
  Sequel::Model.plugin :validation_helpers
11
+ Sequel::Model.plugin :whitelist_security
@@ -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
- DateTime :created_at
10
- DateTime :updated_at
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[id.to_i]
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.update_fields(attr, settable_fields, validate: false, missing: :skip)
59
+ resource.set(attr)
60
+ resource.save_changes(validate: false)
62
61
  end
63
62
 
64
63
  destroy(roles: %i[owner superuser]) do
@@ -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
- DateTime :created_at
10
- DateTime :updated_at
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[slug.to_s]
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
- post = Post.new
76
- post.set_fields(attr.merge(slug: slug), settable_fields)
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.update_fields(attr, settable_fields, validate: false, missing: :skip)
82
+ resource.set(attr)
83
+ resource.save_changes(validate: false)
83
84
  end
84
85
 
85
86
  destroy(roles: %i[owner superuser]) do
@@ -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
- attribute :name
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[id.to_i]
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
@@ -2,6 +2,8 @@
2
2
  require 'logger'
3
3
  require_relative 'boot'
4
4
 
5
+ Sequel.single_threaded = true # WEBrick is single-threaded
6
+
5
7
  DB = Sequel.connect ENV.fetch 'DATABASE_URL',
6
8
  defined?(JRUBY_VERSION) ? 'jdbc:sqlite::memory:' : 'sqlite:/'
7
9
 
@@ -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 { |a| a.first.nil? }
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 :nullif do |nullish|
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
- return params[:filter] \
174
- if settings.resource_config[action][:filter_by].empty? ||
175
- params[:filter].keys.to_set.subset?(settings.resource_config[action][:filter_by])
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
- return params[:sort] \
196
- if settings.resource_config[action][:sort_by].empty? ||
197
- params[:sort].keys.to_set.subset?(settings.resource_config[action][:sort_by])
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
- return params[:page] \
217
- if (params[:page].keys - settings._sinja.page_using.keys).empty?
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
- alias configure_jsonapi sinja
374
+ def configure_jsonapi(&block)
375
+ _sinja.configure(&block)
376
+ end
377
+
372
378
  def freeze_jsonapi
373
379
  _sinja.freeze
374
380
  end
@@ -168,6 +168,10 @@ module Sinja
168
168
  @serializer_opts.replace(deep_copy(DEFAULT_SERIALIZER_OPTS).merge!(h))
169
169
  end
170
170
 
171
+ def configure
172
+ yield self
173
+ end
174
+
171
175
  def freeze
172
176
  @query_params.freeze
173
177
  @error_logger.freeze
@@ -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.path.dup
9
- path_info << "/#{id}" unless request.path.end_with?("/#{id}")
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 *[mime_type(:api_json), mime_type(:json), mime_type(:javascript)].freeze
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.any?
221
- if abody.all? { |error| error.is_a?(Hash) }
222
- # `halt' with a hash or array of hashes
223
- abody.flat_map(&method(:error_hash))
224
- elsif not_found?
225
- # `not_found' or `halt 404'
226
- message = abody.first.to_s
227
- error_hash \
228
- :title=>'Not Found Error',
229
- :detail=>(message unless message == '<h1>Not Found</h1>')
230
- else
231
- # `halt'
232
- error_hash \
233
- :title=>'Unknown Error',
234
- :detail=>abody.first.to_s
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
- end
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'] if env.key?('HTTP_X_HTTP_METHOD_OVERRIDE') &&
10
- env['REQUEST_METHOD'] == 'POST' && env['HTTP_X_HTTP_METHOD_OVERRIDE'].tap(&:upcase!) == 'PATCH'
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 '', :nullif=>proc(&:empty?), :actions=>:clear do
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 '', :nullif=>proc(&:nil?), :actions=>:prune do
26
+ app.patch '', :on=>proc { data.nil? }, :actions=>:prune do
29
27
  serialize_linkage?(*prune)
30
28
  end
31
29
 
@@ -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
- # TODO: Move this to a constant or configurable?
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
- resources, opts =
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 == resources.length
36
+ unless ids.length == collection.length
37
37
 
38
- serialize_models(resources, opts)
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
- if data.key?(:id)
62
- raise ForbiddenError, 'Client-generated ID not supported'
63
- else
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 })
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Sinja
3
- VERSION = '1.2.5'
3
+ VERSION = '1.3.0'
4
4
  end
@@ -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.6'
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', '~> 4.41'
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.2.5
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-03-08 00:00:00.000000000 Z
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: '0.6'
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: '0.6'
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.41'
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.41'
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.10
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