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