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.
@@ -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
- client, default, excluded =
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
- return included if included.empty?
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._resource_roles
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
- roles = settings._resource_roles
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 roles = settings._sinja.resource_roles.fetch(term.pluralize.to_sym, nil)
80
+ unless config = settings._sinja.resource_config.fetch(term.pluralize.to_sym, nil)
82
81
  end
83
82
 
84
- roles =
85
- roles.dig(:has_many, last_term.pluralize.to_sym, :fetch) ||
86
- roles.dig(:has_one, last_term.singularize.to_sym, :pluck)
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 model,
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
- ::JSONAPI::Serializer.serialize [*models],
119
- settings._sinja.serializer_opts.merge(options)
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, rel_path, options={})
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] = rel_path.to_s
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['data']['relationships'][rel_path.to_s].tap do |linkage|
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.def_action_helpers(ACTIONS, 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
- serialize_models(*fetch)
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.def_action_helpers(ACTIONS, 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
 
@@ -12,15 +12,17 @@ require 'sinja/resource_routes'
12
12
 
13
13
  module Sinja
14
14
  module Resource
15
- SIDELOAD_ACTIONS = Set.new(%i[graft merge clear]).freeze
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
- resource_roles.merge!(action=>opts[:roles]) if opts.key?(:roles)
23
- resource_sideload.merge!(action=>opts[:sideload_on]) if opts.key?(:sideload_on)
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 block signature for `#{action}' action helper" \
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) if respond_to?("before_#{action}")
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
- rel_path = rel.to_s
84
+ rel = rel.to_s
85
85
  .send(rel_type == :has_one ? :singularize : :pluralize)
86
86
  .dasherize
87
87
  .to_sym
88
88
 
89
- _resource_roles[rel_type][rel.to_sym] # trigger default proc
89
+ config = _resource_config[rel_type][rel] # trigger default proc
90
90
 
91
- namespace %r{/[^/]+(?<r>/relationships)?/#{rel_path}} do
92
- define_singleton_method(:resource_roles) do
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.to_sym)
96
+ super(*args, rel_type, rel)
99
97
  end
100
98
 
101
99
  define_method(:serialize_linkage) do |*args|
102
- super(resource, rel_path, *args)
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.def_action_helpers(ACTIONS, 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 '', :pfilters=>:id do
12
+ app.head '', :qcapture=>{ :filter=>:id } do
10
13
  allow :get=>:show
11
14
  end
12
15
 
13
- app.get '', :pfilters=>:id, :actions=>:show do
14
- ids = params['filter'].delete('id')
15
- ids = ids.split(',') if ids.respond_to?(:split)
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
- opts = {}
18
- resources = [*ids].tap(&:uniq!).map! do |id|
19
- tmp, opts = show(id)
20
- raise NotFoundError, "Resource '#{id}' not found" unless tmp
21
- tmp
22
- end
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
- # TODO: Serialize collection with opts from last model found?
25
- serialize_models(resources, opts)
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
- serialize_models(*index)
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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Sinja
3
- VERSION = '1.0.0.pre2'
3
+ VERSION = '1.1.0.pre1'
4
4
  end
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.resource_roles[resource_name]
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(:_resource_roles) do
42
- _sinja.resource_roles[resource_name]
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
- define_method(:can?) do |*args|
55
- super(resource_name, *args)
56
- end
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 :_resource_roles, nil # dummy value overridden in each resource
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 :pfilters do |*pfilters|
105
+ app.set :qcapture do |*index|
120
106
  condition do
121
- pfilters.all? do |pfilter|
122
- params.key?('filter') && params['filter'].key?(pfilter.to_s)
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
- lookup = settings._sinja.resource_roles[resource_name]
155
- # TODO: This is... problematic.
156
- roles = (lookup[rel_type][rel][action] if rel_type && rel) || lookup[:resource][action]
157
- roles.nil? || roles.empty? || roles === memoized_role
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 JSON:API request payload'
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.fetch('sinja.passthru', 'unknown').to_sym
201
- settings._sinja.resource_sideload[resource_name][child]&.
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, JSON:API-compliant web services in Sinatra'
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