sinja 1.0.0.pre2 → 1.1.0.pre1
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/README.md +313 -107
- data/demo-app/README.md +58 -0
- data/demo-app/app.rb +8 -4
- data/demo-app/boot.rb +3 -1
- data/demo-app/classes/author.rb +18 -15
- data/demo-app/classes/comment.rb +11 -9
- data/demo-app/classes/post.rb +21 -16
- data/demo-app/classes/tag.rb +12 -8
- data/demo-app/database.rb +4 -6
- data/lib/sinja/config.rb +85 -76
- data/lib/sinja/helpers/relationships.rb +5 -3
- data/lib/sinja/helpers/sequel.rb +43 -0
- data/lib/sinja/helpers/serializers.rb +39 -20
- data/lib/sinja/relationship_routes/has_many.rb +9 -5
- data/lib/sinja/relationship_routes/has_one.rb +4 -4
- data/lib/sinja/resource.rb +19 -21
- data/lib/sinja/resource_routes.rb +33 -20
- data/lib/sinja/version.rb +1 -1
- data/lib/sinja.rb +137 -48
- data/sinja.gemspec +1 -1
- metadata +4 -4
- data/demo-app/test.rb +0 -17
data/lib/sinja/helpers/sequel.rb
CHANGED
@@ -11,6 +11,12 @@ module Sinja
|
|
11
11
|
c.not_found_exceptions << ::Sequel::NoMatchingRow
|
12
12
|
c.validation_exceptions << ::Sequel::ValidationFailed
|
13
13
|
c.validation_formatter = ->(e) { e.errors.keys.zip(e.errors.full_messages) }
|
14
|
+
|
15
|
+
c.page_using = {
|
16
|
+
:number=>1,
|
17
|
+
:size=>10,
|
18
|
+
:record_count=>nil
|
19
|
+
} if ::Sequel::Database::EXTENSIONS.key?(:pagination)
|
14
20
|
end
|
15
21
|
|
16
22
|
def validate!
|
@@ -21,6 +27,43 @@ module Sinja
|
|
21
27
|
::Sequel::DATABASES.first
|
22
28
|
end
|
23
29
|
|
30
|
+
def filter(collection, **fields)
|
31
|
+
collection.where(fields)
|
32
|
+
end
|
33
|
+
|
34
|
+
def sort(collection, **fields)
|
35
|
+
collection.order(*fields.map { |k, v| ::Sequel.send(v, k) })
|
36
|
+
end
|
37
|
+
|
38
|
+
def page(collection, **opts)
|
39
|
+
opts = settings._sinja.page_using.merge(opts)
|
40
|
+
collection = collection.dataset \
|
41
|
+
unless collection.respond_to?(:paginate)
|
42
|
+
collection = collection.paginate \
|
43
|
+
opts[:number].to_i,
|
44
|
+
opts[:size].to_i,
|
45
|
+
(opts[:record_count].to_i if opts[:record_count])
|
46
|
+
|
47
|
+
# Attributes common to all pagination links
|
48
|
+
base = {
|
49
|
+
:size=>collection.page_size,
|
50
|
+
:record_count=>collection.pagination_record_count
|
51
|
+
}
|
52
|
+
pagination = {
|
53
|
+
:first=>base.merge(:number=>1),
|
54
|
+
:self=>base.merge(:number=>collection.current_page),
|
55
|
+
:last=>base.merge(:number=>collection.page_count)
|
56
|
+
}
|
57
|
+
pagination[:next] = base.merge(:number=>collection.next_page) if collection.next_page
|
58
|
+
pagination[:prev] = base.merge(:number=>collection.prev_page) if collection.prev_page
|
59
|
+
|
60
|
+
return collection, pagination
|
61
|
+
end if ::Sequel::Database::EXTENSIONS.key?(:pagination)
|
62
|
+
|
63
|
+
def finalize(collection)
|
64
|
+
collection.all
|
65
|
+
end
|
66
|
+
|
24
67
|
def transaction(&block)
|
25
68
|
database.transaction(&block)
|
26
69
|
end
|
@@ -39,17 +39,16 @@ module Sinja
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def include_exclude!(options)
|
42
|
-
|
42
|
+
included, default, excluded =
|
43
43
|
params[:include],
|
44
44
|
options.delete(:include) || [],
|
45
45
|
options.delete(:exclude) || []
|
46
46
|
|
47
|
-
included = Array === client ? client : client.split(',')
|
48
47
|
if included.empty?
|
49
48
|
included = Array === default ? default : default.split(',')
|
50
|
-
end
|
51
49
|
|
52
|
-
|
50
|
+
return included if included.empty?
|
51
|
+
end
|
53
52
|
|
54
53
|
excluded = Array === excluded ? excluded : excluded.split(',')
|
55
54
|
unless excluded.empty?
|
@@ -64,7 +63,7 @@ module Sinja
|
|
64
63
|
return included if included.empty?
|
65
64
|
end
|
66
65
|
|
67
|
-
return included unless settings.
|
66
|
+
return included unless settings._resource_config
|
68
67
|
|
69
68
|
# Walk the tree and try to exclude based on fetch and pluck permissions
|
70
69
|
included.keep_if do |termstr|
|
@@ -72,18 +71,19 @@ module Sinja
|
|
72
71
|
*terms, last_term = termstr.split('.')
|
73
72
|
|
74
73
|
# Start cursor at root of current resource
|
75
|
-
|
74
|
+
config = settings._resource_config
|
76
75
|
terms.each do |term|
|
77
76
|
# Move cursor through each term, avoiding the default proc,
|
78
77
|
# halting if no roles found, i.e. client asked to include
|
79
78
|
# something that Sinja doesn't know about
|
80
79
|
throw :keep?, true \
|
81
|
-
unless
|
80
|
+
unless config = settings._sinja.resource_config.fetch(term.pluralize.to_sym, nil)
|
82
81
|
end
|
83
82
|
|
84
|
-
roles =
|
85
|
-
|
86
|
-
|
83
|
+
roles = (
|
84
|
+
config.dig(:has_many, last_term.pluralize.to_sym, :fetch) ||
|
85
|
+
config.dig(:has_one, last_term.singularize.to_sym, :pluck)
|
86
|
+
)[:roles]
|
87
87
|
|
88
88
|
throw :keep?, roles && (roles.empty? || roles === memoized_role)
|
89
89
|
end
|
@@ -95,9 +95,9 @@ module Sinja
|
|
95
95
|
options[:skip_collection_check] = defined?(::Sequel) && ::Sequel::Model === model
|
96
96
|
options[:include] = include_exclude!(options)
|
97
97
|
options[:fields] ||= params[:fields] unless params[:fields].empty?
|
98
|
+
options = settings._sinja.serializer_opts.merge(options)
|
98
99
|
|
99
|
-
::JSONAPI::Serializer.serialize
|
100
|
-
settings._sinja.serializer_opts.merge(options)
|
100
|
+
::JSONAPI::Serializer.serialize(model, options)
|
101
101
|
end
|
102
102
|
|
103
103
|
def serialize_model?(model=nil, options={})
|
@@ -114,9 +114,29 @@ module Sinja
|
|
114
114
|
options[:is_collection] = true
|
115
115
|
options[:include] = include_exclude!(options)
|
116
116
|
options[:fields] ||= params[:fields] unless params[:fields].empty?
|
117
|
+
options = settings._sinja.serializer_opts.merge(options)
|
117
118
|
|
118
|
-
|
119
|
-
|
119
|
+
if options.key?(:links) && pagination = options[:links].delete(:pagination)
|
120
|
+
options[:links][:self] = request.url unless pagination.key?(:self)
|
121
|
+
|
122
|
+
query = Rack::Utils.build_nested_query \
|
123
|
+
env['rack.request.query_hash'].dup.tap { |h| h.delete('page') }
|
124
|
+
self_link, join_char =
|
125
|
+
if query.empty?
|
126
|
+
[request.path, ??]
|
127
|
+
else
|
128
|
+
["#{request.path}?#{query}", ?&]
|
129
|
+
end
|
130
|
+
|
131
|
+
%i[self first prev next last].each do |key|
|
132
|
+
next unless pagination.key?(key)
|
133
|
+
query = Rack::Utils.build_nested_query \
|
134
|
+
:page=>pagination[key]
|
135
|
+
options[:links][key] = "#{self_link}#{join_char}#{query}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
::JSONAPI::Serializer.serialize([*models], options)
|
120
140
|
end
|
121
141
|
|
122
142
|
def serialize_models?(models=[], options={})
|
@@ -129,16 +149,15 @@ module Sinja
|
|
129
149
|
end
|
130
150
|
end
|
131
151
|
|
132
|
-
def serialize_linkage(model,
|
152
|
+
def serialize_linkage(model, rel, options={})
|
133
153
|
options[:is_collection] = false
|
134
154
|
options[:skip_collection_check] = defined?(::Sequel) && ::Sequel::Model === model
|
135
|
-
options[:include] =
|
136
|
-
|
137
|
-
content = ::JSONAPI::Serializer.serialize model,
|
138
|
-
settings._sinja.serializer_opts.merge(options)
|
155
|
+
options[:include] = rel.to_s
|
156
|
+
options = settings._sinja.serializer_opts.merge(options)
|
139
157
|
|
140
158
|
# TODO: This is extremely wasteful. Refactor JAS to expose the linkage serializer?
|
141
|
-
content
|
159
|
+
content = ::JSONAPI::Serializer.serialize(model, options)
|
160
|
+
content['data']['relationships'][rel.to_s].tap do |linkage|
|
142
161
|
%w[meta jsonapi].each do |key|
|
143
162
|
linkage[key] = content[key] if content.key?(key)
|
144
163
|
end
|
@@ -2,10 +2,11 @@
|
|
2
2
|
module Sinja
|
3
3
|
module RelationshipRoutes
|
4
4
|
module HasMany
|
5
|
-
ACTIONS = %i[fetch clear merge subtract].freeze
|
6
|
-
|
7
5
|
def self.registered(app)
|
8
|
-
app.
|
6
|
+
app.def_action_helper(app, :fetch, %i[roles filter_by sort_by])
|
7
|
+
app.def_action_helper(app, :clear, %i[roles sideload_on])
|
8
|
+
app.def_action_helper(app, :merge, %i[roles sideload_on])
|
9
|
+
app.def_action_helper(app, :subtract, :roles)
|
9
10
|
|
10
11
|
app.head '' do
|
11
12
|
unless relationship_link?
|
@@ -21,8 +22,11 @@ module Sinja
|
|
21
22
|
serialize_linkage
|
22
23
|
end
|
23
24
|
|
24
|
-
app.get '', :actions=>:fetch do
|
25
|
-
|
25
|
+
app.get '', :qparams=>%i[include fields filter sort page], :actions=>:fetch do
|
26
|
+
collection, opts = fetch
|
27
|
+
collection, links = filter_sort_page(collection, :fetch)
|
28
|
+
(opts[:links] ||= {}).merge!(links)
|
29
|
+
serialize_models(collection, opts)
|
26
30
|
end
|
27
31
|
|
28
32
|
app.patch '', :nullif=>proc(&:empty?), :actions=>:clear do
|
@@ -2,10 +2,10 @@
|
|
2
2
|
module Sinja
|
3
3
|
module RelationshipRoutes
|
4
4
|
module HasOne
|
5
|
-
ACTIONS = %i[pluck prune graft].freeze
|
6
|
-
|
7
5
|
def self.registered(app)
|
8
|
-
app.
|
6
|
+
app.def_action_helper(app, :pluck, :roles)
|
7
|
+
app.def_action_helper(app, :prune, :roles)
|
8
|
+
app.def_action_helper(app, :graft, %i[roles sideload_on])
|
9
9
|
|
10
10
|
app.head '' do
|
11
11
|
unless relationship_link?
|
@@ -21,7 +21,7 @@ module Sinja
|
|
21
21
|
serialize_linkage
|
22
22
|
end
|
23
23
|
|
24
|
-
app.get '', :actions=>:pluck do
|
24
|
+
app.get '', :qparams=>%i[include fields], :actions=>:pluck do
|
25
25
|
serialize_model(*pluck)
|
26
26
|
end
|
27
27
|
|
data/lib/sinja/resource.rb
CHANGED
@@ -12,15 +12,17 @@ require 'sinja/resource_routes'
|
|
12
12
|
|
13
13
|
module Sinja
|
14
14
|
module Resource
|
15
|
-
|
16
|
-
|
17
|
-
def def_action_helper(action, context)
|
18
|
-
abort "JSONAPI action helpers can't be HTTP verbs!" \
|
15
|
+
def def_action_helper(context, action, allow_opts=[])
|
16
|
+
abort "Action helper names can't overlap with Sinatra DSL" \
|
19
17
|
if Sinatra::Base.respond_to?(action)
|
20
18
|
|
21
19
|
context.define_singleton_method(action) do |**opts, &block|
|
22
|
-
|
23
|
-
|
20
|
+
abort "Unexpected option(s) for `#{action}' action helper" \
|
21
|
+
unless (opts.keys - [*allow_opts]).empty?
|
22
|
+
|
23
|
+
resource_config[action].each do |k, v|
|
24
|
+
v.replace([*opts[k]]) if opts.key?(k)
|
25
|
+
end
|
24
26
|
|
25
27
|
return unless block ||=
|
26
28
|
case !method_defined?(action) && action
|
@@ -32,14 +34,16 @@ module Sinja
|
|
32
34
|
required_arity = {
|
33
35
|
:create=>2,
|
34
36
|
:index=>-1,
|
35
|
-
:fetch=>-1
|
37
|
+
:fetch=>-1,
|
38
|
+
:show_many=>-1
|
36
39
|
}.freeze[action] || 1
|
37
40
|
|
38
41
|
define_method(action) do |*args|
|
39
|
-
raise ArgumentError, "Unexpected
|
42
|
+
raise ArgumentError, "Unexpected argument(s) for `#{action}' action helper" \
|
40
43
|
unless args.length == block.arity
|
41
44
|
|
42
|
-
public_send("before_#{action}", *args)
|
45
|
+
public_send("before_#{action}", *args) \
|
46
|
+
if respond_to?("before_#{action}")
|
43
47
|
|
44
48
|
case result = instance_exec(*args, &block)
|
45
49
|
when Array
|
@@ -67,10 +71,6 @@ module Sinja
|
|
67
71
|
end
|
68
72
|
end
|
69
73
|
|
70
|
-
def def_action_helpers(actions, context=nil)
|
71
|
-
[*actions].each { |action| def_action_helper(action, context) }
|
72
|
-
end
|
73
|
-
|
74
74
|
def self.registered(app)
|
75
75
|
app.helpers Helpers::Relationships do
|
76
76
|
attr_accessor :resource
|
@@ -81,25 +81,23 @@ module Sinja
|
|
81
81
|
|
82
82
|
%i[has_one has_many].each do |rel_type|
|
83
83
|
define_method(rel_type) do |rel, &block|
|
84
|
-
|
84
|
+
rel = rel.to_s
|
85
85
|
.send(rel_type == :has_one ? :singularize : :pluralize)
|
86
86
|
.dasherize
|
87
87
|
.to_sym
|
88
88
|
|
89
|
-
|
89
|
+
config = _resource_config[rel_type][rel] # trigger default proc
|
90
90
|
|
91
|
-
namespace %r{/[^/]+(?<r>/relationships)?/#{
|
92
|
-
define_singleton_method(:
|
93
|
-
_resource_roles[rel_type][rel.to_sym]
|
94
|
-
end
|
91
|
+
namespace %r{/[^/]+(?<r>/relationships)?/#{rel}} do
|
92
|
+
define_singleton_method(:resource_config) { config }
|
95
93
|
|
96
94
|
helpers Helpers::Nested do
|
97
95
|
define_method(:can?) do |*args|
|
98
|
-
super(*args, rel_type, rel
|
96
|
+
super(*args, rel_type, rel)
|
99
97
|
end
|
100
98
|
|
101
99
|
define_method(:serialize_linkage) do |*args|
|
102
|
-
super(resource,
|
100
|
+
super(resource, rel, *args)
|
103
101
|
end
|
104
102
|
end
|
105
103
|
|
@@ -1,39 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Sinja
|
3
3
|
module ResourceRoutes
|
4
|
-
ACTIONS = %i[index show create update destroy].freeze
|
5
|
-
|
6
4
|
def self.registered(app)
|
7
|
-
app.
|
5
|
+
app.def_action_helper(app, :show, :roles)
|
6
|
+
app.def_action_helper(app, :show_many)
|
7
|
+
app.def_action_helper(app, :index, %i[roles filter_by sort_by])
|
8
|
+
app.def_action_helper(app, :create, :roles)
|
9
|
+
app.def_action_helper(app, :update, :roles)
|
10
|
+
app.def_action_helper(app, :destroy, :roles)
|
8
11
|
|
9
|
-
app.head '', :
|
12
|
+
app.head '', :qcapture=>{ :filter=>:id } do
|
10
13
|
allow :get=>:show
|
11
14
|
end
|
12
15
|
|
13
|
-
app.get '', :
|
14
|
-
ids =
|
15
|
-
ids = ids.split(',') if ids
|
16
|
+
app.get '', :qcapture=>{ :filter=>:id }, :qparams=>%i[include fields], :actions=>:show do
|
17
|
+
ids = @qcaptures.first # TODO: Get this as a block parameter?
|
18
|
+
ids = ids.split(',') if String === ids
|
19
|
+
ids = [*ids].tap(&:uniq!)
|
16
20
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
if respond_to?(:show_many)
|
22
|
+
resources, opts = show_many(ids)
|
23
|
+
raise NotFoundError, "Resource(s) not found" \
|
24
|
+
unless ids.length == resources.length
|
25
|
+
serialize_models(resources, opts)
|
26
|
+
else
|
27
|
+
opts = {}
|
28
|
+
resources = ids.map! do |id|
|
29
|
+
tmp, opts = show(id)
|
30
|
+
raise NotFoundError, "Resource '#{id}' not found" unless tmp
|
31
|
+
tmp
|
32
|
+
end
|
23
33
|
|
24
|
-
|
25
|
-
|
34
|
+
serialize_models(resources, opts)
|
35
|
+
end
|
26
36
|
end
|
27
37
|
|
28
38
|
app.head '' do
|
29
39
|
allow :get=>:index, :post=>:create
|
30
40
|
end
|
31
41
|
|
32
|
-
app.get '', :actions=>:index do
|
33
|
-
|
42
|
+
app.get '', :qparams=>%i[include fields filter sort page], :actions=>:index do
|
43
|
+
collection, opts = index
|
44
|
+
collection, links = filter_sort_page(collection, :index)
|
45
|
+
(opts[:links] ||= {}).merge!(links)
|
46
|
+
serialize_models(collection, opts)
|
34
47
|
end
|
35
48
|
|
36
|
-
app.post '', :actions=>:create do
|
49
|
+
app.post '', :qparams=>:include, :actions=>:create do
|
37
50
|
sanity_check!
|
38
51
|
|
39
52
|
opts = {}
|
@@ -72,13 +85,13 @@ module Sinja
|
|
72
85
|
allow :get=>:show, :patch=>:update, :delete=>:destroy
|
73
86
|
end
|
74
87
|
|
75
|
-
app.get '/:id', :actions=>:show do |id|
|
88
|
+
app.get '/:id', :qparams=>%i[include fields], :actions=>:show do |id|
|
76
89
|
tmp, opts = show(id)
|
77
90
|
raise NotFoundError, "Resource '#{id}' not found" unless tmp
|
78
91
|
serialize_model(tmp, opts)
|
79
92
|
end
|
80
93
|
|
81
|
-
app.patch '/:id', :actions=>:update do |id|
|
94
|
+
app.patch '/:id', :qparams=>:include, :actions=>:update do |id|
|
82
95
|
sanity_check!(id)
|
83
96
|
tmp, opts = transaction do
|
84
97
|
update(attributes).tap do
|
data/lib/sinja/version.rb
CHANGED
data/lib/sinja.rb
CHANGED
@@ -4,6 +4,7 @@ require 'mustermann'
|
|
4
4
|
require 'sinatra/base'
|
5
5
|
require 'sinatra/namespace'
|
6
6
|
|
7
|
+
require 'set'
|
7
8
|
require 'sinja/config'
|
8
9
|
require 'sinja/errors'
|
9
10
|
require 'sinja/helpers/serializers'
|
@@ -34,33 +35,17 @@ module Sinja
|
|
34
35
|
.to_sym
|
35
36
|
|
36
37
|
# trigger default procs
|
37
|
-
_sinja.
|
38
|
-
_sinja.resource_sideload[resource_name]
|
38
|
+
config = _sinja.resource_config[resource_name]
|
39
39
|
|
40
40
|
namespace "/#{resource_name}" do
|
41
|
-
define_singleton_method(:
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
define_singleton_method(:resource_roles) do
|
46
|
-
_resource_roles[:resource]
|
47
|
-
end
|
48
|
-
|
49
|
-
define_singleton_method(:resource_sideload) do
|
50
|
-
_sinja.resource_sideload[resource_name]
|
51
|
-
end
|
41
|
+
define_singleton_method(:_resource_config) { config }
|
42
|
+
define_singleton_method(:resource_config) { config[:resource] }
|
52
43
|
|
53
44
|
helpers do
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
define_method(:sanity_check!) do |*args|
|
59
|
-
super(resource_name, *args)
|
60
|
-
end
|
61
|
-
|
62
|
-
define_method(:sideload?) do |*args|
|
63
|
-
super(resource_name, *args)
|
45
|
+
%i[can? sanity_check! sideload?].each do |meth|
|
46
|
+
define_method(meth) do |*args|
|
47
|
+
super(resource_name, *args)
|
48
|
+
end
|
64
49
|
end
|
65
50
|
end
|
66
51
|
|
@@ -102,7 +87,7 @@ module Sinja
|
|
102
87
|
|
103
88
|
app.disable :protection, :show_exceptions, :static
|
104
89
|
app.set :_sinja, Sinja::Config.new
|
105
|
-
app.set :
|
90
|
+
app.set :_resource_config, nil # dummy value overridden in each resource
|
106
91
|
|
107
92
|
app.set :actions do |*actions|
|
108
93
|
condition do
|
@@ -112,18 +97,61 @@ module Sinja
|
|
112
97
|
raise MethodNotAllowedError, 'Action or method not implemented or supported' \
|
113
98
|
unless respond_to?(action)
|
114
99
|
end
|
100
|
+
|
115
101
|
true
|
116
102
|
end
|
117
103
|
end
|
118
104
|
|
119
|
-
app.set :
|
105
|
+
app.set :qcapture do |*index|
|
120
106
|
condition do
|
121
|
-
|
122
|
-
|
107
|
+
@qcaptures ||= []
|
108
|
+
index.to_h.all? do |key, subkeys|
|
109
|
+
params.key?(key.to_s) && [*subkeys].all? do |subkey|
|
110
|
+
@qcaptures << params[key.to_s].delete(subkey.to_s) \
|
111
|
+
if params[key.to_s].key?(subkey.to_s)
|
112
|
+
end
|
123
113
|
end
|
124
114
|
end
|
125
115
|
end
|
126
116
|
|
117
|
+
app.set :qparams do |*allow_params|
|
118
|
+
allow_params = allow_params.to_set
|
119
|
+
|
120
|
+
abort "Unexpected query parameter(s) in route definiton" \
|
121
|
+
unless allow_params.subset?(settings._sinja.query_params.keys.to_set)
|
122
|
+
|
123
|
+
condition do
|
124
|
+
params.each do |key, value|
|
125
|
+
key = key.to_sym
|
126
|
+
|
127
|
+
next if settings._sinja.query_params[key].nil?
|
128
|
+
|
129
|
+
raise BadRequestError, "`#{key}' query parameter not allowed" \
|
130
|
+
unless allow_params.include?(key) || value.empty?
|
131
|
+
|
132
|
+
if Array === settings._sinja.query_params[key] && String === value
|
133
|
+
value = (params[key.to_s] = value.split(','))
|
134
|
+
elsif !(settings._sinja.query_params[key].class === value)
|
135
|
+
raise BadRequestError, "`#{key}' query parameter malformed"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
settings._sinja.query_params.each do |key, value|
|
140
|
+
next if value.nil?
|
141
|
+
|
142
|
+
key = key.to_s
|
143
|
+
|
144
|
+
if respond_to?("normalize_#{key}_params")
|
145
|
+
params[key] = send("normalize_#{key}_params")
|
146
|
+
else
|
147
|
+
params[key] ||= value
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
true
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
127
155
|
app.set :nullif do |nullish|
|
128
156
|
condition { nullish.(data) }
|
129
157
|
end
|
@@ -151,10 +179,13 @@ module Sinja
|
|
151
179
|
end
|
152
180
|
|
153
181
|
def can?(resource_name, action, rel_type=nil, rel=nil)
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
182
|
+
config = settings._sinja.resource_config[resource_name]
|
183
|
+
config = rel_type && rel ? config[rel_type][rel] : config[:resource]
|
184
|
+
# JRuby issues with nil default_proc (fixed in 9.1.7.0?)
|
185
|
+
# https://github.com/jruby/jruby/issues/4302
|
186
|
+
#roles = config&.dig(action, :roles)
|
187
|
+
roles = config&.key?(action) && config[action][:roles]
|
188
|
+
!roles || roles.empty? || roles === memoized_role
|
158
189
|
end
|
159
190
|
|
160
191
|
def content?
|
@@ -166,8 +197,72 @@ module Sinja
|
|
166
197
|
@data[request.path] ||= begin
|
167
198
|
deserialize_request_body.fetch(:data)
|
168
199
|
rescue NoMethodError, KeyError
|
169
|
-
raise BadRequestError, 'Malformed
|
200
|
+
raise BadRequestError, 'Malformed {json:api} request payload'
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def normalize_filter_params
|
205
|
+
return {} unless params[:filter]&.any?
|
206
|
+
|
207
|
+
halt 400, "Unsupported `filter' query parameter(s)" \
|
208
|
+
unless respond_to?(:filter)
|
209
|
+
|
210
|
+
params[:filter].map do |k, v|
|
211
|
+
[dedasherize(k).to_sym, v]
|
212
|
+
end.to_h
|
213
|
+
end
|
214
|
+
|
215
|
+
def normalize_sort_params
|
216
|
+
return [] unless params[:sort]&.any?
|
217
|
+
|
218
|
+
halt 400, "Unsupported `sort' query parameter(s)" \
|
219
|
+
unless respond_to?(:sort)
|
220
|
+
|
221
|
+
params[:sort].map do |k|
|
222
|
+
dir = k.sub!(/^-/, '') ? :desc : :asc
|
223
|
+
[dedasherize(k).to_sym, dir]
|
224
|
+
end.to_h
|
225
|
+
end
|
226
|
+
|
227
|
+
def normalize_page_params
|
228
|
+
return {} unless params[:page]&.any?
|
229
|
+
|
230
|
+
halt 400, "Unsupported `page' query parameter(s)" \
|
231
|
+
unless respond_to?(:page)
|
232
|
+
|
233
|
+
h = params[:page].map do |k, v|
|
234
|
+
[dedasherize(k).to_sym, v]
|
235
|
+
end.to_h
|
236
|
+
|
237
|
+
return h if h.keys.to_set.subset?(settings._sinja.page_using.keys.to_set)
|
238
|
+
|
239
|
+
halt 400, "Invalid `page' query parameter(s)"
|
240
|
+
end
|
241
|
+
|
242
|
+
def filter_sort_page(collection, action)
|
243
|
+
unless params[:filter].empty?
|
244
|
+
halt 400, "Invalid `filter' query parameter(s)" \
|
245
|
+
unless settings.resource_config[action][:filter_by].empty? ||
|
246
|
+
params[:filter].keys.to_set.subset?(settings.resource_config[action][:filter_by])
|
247
|
+
|
248
|
+
collection = filter(collection, params[:filter])
|
249
|
+
end
|
250
|
+
|
251
|
+
unless params[:sort].empty?
|
252
|
+
halt 400, "Invalid `sort' query parameter(s)" \
|
253
|
+
unless settings.resource_config[action][:sort_by].empty? ||
|
254
|
+
params[:sort].keys.to_set.subset?(settings.resource_config[action][:sort_by])
|
255
|
+
|
256
|
+
collection = sort(collection, params[:sort])
|
170
257
|
end
|
258
|
+
|
259
|
+
unless params[:page].empty?
|
260
|
+
collection, pagination = page(collection, params[:page])
|
261
|
+
end
|
262
|
+
|
263
|
+
collection = finalize(collection) if respond_to?(:finalize)
|
264
|
+
|
265
|
+
return collection, :pagination=>pagination
|
171
266
|
end
|
172
267
|
|
173
268
|
def halt(code, body=nil)
|
@@ -184,22 +279,10 @@ module Sinja
|
|
184
279
|
@role ||= role
|
185
280
|
end
|
186
281
|
|
187
|
-
def normalize_params!
|
188
|
-
# TODO: halt 400 if other params, or params not implemented?
|
189
|
-
{
|
190
|
-
:fields=>{}, # passthru
|
191
|
-
:include=>[], # passthru
|
192
|
-
:filter=>{},
|
193
|
-
:page=>{},
|
194
|
-
:sort=>''
|
195
|
-
}.each { |k, v| params[k] ||= v }
|
196
|
-
end
|
197
|
-
|
198
282
|
def sideload?(resource_name, child)
|
199
283
|
return unless sideloaded?
|
200
|
-
parent = env
|
201
|
-
settings.
|
202
|
-
include?(parent) && can?(parent)
|
284
|
+
parent = env['sinja.passthru'].to_sym
|
285
|
+
settings.resource_config[child][:sideload_on]&.include?(parent) && can?(parent)
|
203
286
|
end
|
204
287
|
|
205
288
|
def sideloaded?
|
@@ -235,8 +318,6 @@ module Sinja
|
|
235
318
|
)
|
236
319
|
end
|
237
320
|
|
238
|
-
normalize_params!
|
239
|
-
|
240
321
|
content_type :api_json
|
241
322
|
end
|
242
323
|
|
@@ -265,4 +346,12 @@ module Sinja
|
|
265
346
|
serialize_errors(&settings._sinja.error_logger)
|
266
347
|
end
|
267
348
|
end
|
349
|
+
|
350
|
+
def self.extended(base)
|
351
|
+
def base.route(*, **opts)
|
352
|
+
opts[:qparams] ||= []
|
353
|
+
|
354
|
+
super
|
355
|
+
end
|
356
|
+
end
|
268
357
|
end
|
data/sinja.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ['Mike Pastore']
|
10
10
|
spec.email = ['mike@oobak.org']
|
11
11
|
|
12
|
-
spec.summary = 'RESTful,
|
12
|
+
spec.summary = 'RESTful, {json:api}-compliant web services in Sinatra'
|
13
13
|
spec.homepage = 'https://github.com/mwpastore/sinja'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|