jsonapi_compliable 0.11.34 → 1.0.alpha.2

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.
Files changed (73) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +1 -2
  4. data/Rakefile +7 -3
  5. data/jsonapi_compliable.gemspec +7 -3
  6. data/lib/generators/jsonapi/resource_generator.rb +8 -79
  7. data/lib/generators/jsonapi/templates/application_resource.rb.erb +2 -1
  8. data/lib/generators/jsonapi/templates/controller.rb.erb +19 -64
  9. data/lib/generators/jsonapi/templates/resource.rb.erb +5 -47
  10. data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
  11. data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
  12. data/lib/jsonapi_compliable.rb +87 -18
  13. data/lib/jsonapi_compliable/adapters/abstract.rb +202 -45
  14. data/lib/jsonapi_compliable/adapters/active_record.rb +6 -130
  15. data/lib/jsonapi_compliable/adapters/active_record/base.rb +247 -0
  16. data/lib/jsonapi_compliable/adapters/active_record/belongs_to_sideload.rb +17 -0
  17. data/lib/jsonapi_compliable/adapters/active_record/has_many_sideload.rb +17 -0
  18. data/lib/jsonapi_compliable/adapters/active_record/has_one_sideload.rb +17 -0
  19. data/lib/jsonapi_compliable/adapters/active_record/inferrence.rb +12 -0
  20. data/lib/jsonapi_compliable/adapters/active_record/many_to_many_sideload.rb +30 -0
  21. data/lib/jsonapi_compliable/adapters/null.rb +177 -6
  22. data/lib/jsonapi_compliable/base.rb +33 -320
  23. data/lib/jsonapi_compliable/context.rb +16 -0
  24. data/lib/jsonapi_compliable/deserializer.rb +14 -39
  25. data/lib/jsonapi_compliable/errors.rb +227 -24
  26. data/lib/jsonapi_compliable/extensions/extra_attribute.rb +3 -1
  27. data/lib/jsonapi_compliable/filter_operators.rb +25 -0
  28. data/lib/jsonapi_compliable/hash_renderer.rb +57 -0
  29. data/lib/jsonapi_compliable/query.rb +190 -202
  30. data/lib/jsonapi_compliable/rails.rb +12 -6
  31. data/lib/jsonapi_compliable/railtie.rb +64 -0
  32. data/lib/jsonapi_compliable/renderer.rb +60 -0
  33. data/lib/jsonapi_compliable/resource.rb +35 -663
  34. data/lib/jsonapi_compliable/resource/configuration.rb +239 -0
  35. data/lib/jsonapi_compliable/resource/dsl.rb +138 -0
  36. data/lib/jsonapi_compliable/resource/interface.rb +32 -0
  37. data/lib/jsonapi_compliable/resource/polymorphism.rb +68 -0
  38. data/lib/jsonapi_compliable/resource/sideloading.rb +102 -0
  39. data/lib/jsonapi_compliable/resource_proxy.rb +127 -0
  40. data/lib/jsonapi_compliable/responders.rb +19 -0
  41. data/lib/jsonapi_compliable/runner.rb +25 -0
  42. data/lib/jsonapi_compliable/scope.rb +37 -79
  43. data/lib/jsonapi_compliable/scoping/extra_attributes.rb +29 -0
  44. data/lib/jsonapi_compliable/scoping/filter.rb +39 -58
  45. data/lib/jsonapi_compliable/scoping/filterable.rb +9 -14
  46. data/lib/jsonapi_compliable/scoping/paginate.rb +9 -3
  47. data/lib/jsonapi_compliable/scoping/sort.rb +16 -4
  48. data/lib/jsonapi_compliable/sideload.rb +221 -347
  49. data/lib/jsonapi_compliable/sideload/belongs_to.rb +34 -0
  50. data/lib/jsonapi_compliable/sideload/has_many.rb +16 -0
  51. data/lib/jsonapi_compliable/sideload/has_one.rb +9 -0
  52. data/lib/jsonapi_compliable/sideload/many_to_many.rb +24 -0
  53. data/lib/jsonapi_compliable/sideload/polymorphic_belongs_to.rb +108 -0
  54. data/lib/jsonapi_compliable/stats/payload.rb +4 -8
  55. data/lib/jsonapi_compliable/types.rb +172 -0
  56. data/lib/jsonapi_compliable/util/attribute_check.rb +88 -0
  57. data/lib/jsonapi_compliable/util/persistence.rb +29 -7
  58. data/lib/jsonapi_compliable/util/relationship_payload.rb +4 -4
  59. data/lib/jsonapi_compliable/util/render_options.rb +4 -32
  60. data/lib/jsonapi_compliable/util/serializer_attributes.rb +98 -0
  61. data/lib/jsonapi_compliable/util/validation_response.rb +15 -9
  62. data/lib/jsonapi_compliable/version.rb +1 -1
  63. metadata +105 -24
  64. data/lib/generators/jsonapi/field_generator.rb +0 -0
  65. data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +0 -29
  66. data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +0 -20
  67. data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +0 -22
  68. data/lib/generators/jsonapi/templates/payload.rb.erb +0 -39
  69. data/lib/generators/jsonapi/templates/serializer.rb.erb +0 -25
  70. data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +0 -19
  71. data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +0 -33
  72. data/lib/jsonapi_compliable/adapters/active_record_sideloading.rb +0 -152
  73. data/lib/jsonapi_compliable/scoping/extra_fields.rb +0 -58
@@ -1,22 +1,28 @@
1
- require 'jsonapi/rails'
2
-
3
1
  module JsonapiCompliable
4
2
  # Rails Integration. Mix this in to ApplicationController.
5
3
  #
6
4
  # * Mixes in Base
7
5
  # * Adds a global around_action (see Base#wrap_context)
8
- # * Uses Rails' +render+ for rendering
9
6
  #
10
7
  # @see Base#render_jsonapi
11
8
  # @see Base#wrap_context
12
9
  module Rails
13
10
  def self.included(klass)
14
- klass.send(:include, Base)
15
-
16
11
  klass.class_eval do
12
+ include JsonapiCompliable::Context
13
+ include JsonapiErrorable
17
14
  around_action :wrap_context
18
- alias_method :perform_render_jsonapi, :render
19
15
  end
20
16
  end
17
+
18
+ def wrap_context
19
+ JsonapiCompliable.with_context(jsonapi_context, action_name.to_sym) do
20
+ yield
21
+ end
22
+ end
23
+
24
+ def jsonapi_context
25
+ self
26
+ end
21
27
  end
22
28
  end
@@ -0,0 +1,64 @@
1
+ module JsonapiCompliable
2
+ class Railtie < ::Rails::Railtie
3
+
4
+ initializer "jsonapi_compliable.require_activerecord_adapter" do
5
+ config.after_initialize do |app|
6
+ ActiveSupport.on_load(:active_record) do
7
+ require 'jsonapi_compliable/adapters/active_record'
8
+ end
9
+ end
10
+ end
11
+
12
+ initializer 'jsonapi_compliable.init' do
13
+ if Mime[:jsonapi].nil? # rails 4
14
+ Mime::Type.register('application/vnd.api+json', :jsonapi)
15
+ end
16
+ register_parameter_parser
17
+ register_renderers
18
+ end
19
+
20
+ # from jsonapi-rails
21
+ PARSER = lambda do |body|
22
+ data = JSON.parse(body)
23
+ hash = { _jsonapi: data }
24
+
25
+ hash[:format] = :jsonapi
26
+ hash.with_indifferent_access
27
+ end
28
+
29
+ def register_parameter_parser
30
+ if ::Rails::VERSION::MAJOR >= 5
31
+ ActionDispatch::Request.parameter_parsers[:jsonapi] = PARSER
32
+ else
33
+ ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = PARSER
34
+ end
35
+ end
36
+
37
+ def register_renderers
38
+ ActiveSupport.on_load(:action_controller) do
39
+ ::ActionController::Renderers.add(:jsonapi) do |proxy, options|
40
+ self.content_type ||= Mime[:jsonapi]
41
+
42
+ opts = {}
43
+ if respond_to?(:default_jsonapi_render_options)
44
+ opts = default_jsonapi_render_options
45
+ end
46
+ proxy.to_jsonapi(options)
47
+ end
48
+ end
49
+
50
+ ActiveSupport.on_load(:action_controller) do
51
+ ::ActionController::Renderers.add(:jsonapi_errors) do |proxy, options|
52
+ self.content_type ||= Mime[:jsonapi]
53
+
54
+ validation = JsonapiErrorable::Serializers::Validation.new \
55
+ proxy.data, proxy.payload.relationships
56
+
57
+ render \
58
+ json: { errors: validation.errors },
59
+ status: :unprocessable_entity
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,60 @@
1
+ module JsonapiCompliable
2
+ class Renderer
3
+ CONTENT_TYPE = 'application/vnd.api+json'
4
+
5
+ attr_reader :proxy, :options
6
+
7
+ def initialize(proxy, options)
8
+ @proxy = proxy
9
+ @options = options
10
+ end
11
+
12
+ def records
13
+ @records ||= @proxy.data
14
+ end
15
+
16
+ def to_jsonapi
17
+ render(JSONAPI::Renderer.new).to_json
18
+ end
19
+
20
+ def to_json
21
+ render(JsonapiCompliable::HashRenderer.new(@proxy.resource)).to_json
22
+ end
23
+
24
+ def to_xml
25
+ render(JsonapiCompliable::HashRenderer.new(@proxy.resource)).to_xml(root: :data)
26
+ end
27
+
28
+ private
29
+
30
+ def render(implementation)
31
+ notify do
32
+ instance = JSONAPI::Serializable::Renderer.new(implementation)
33
+ options[:fields] = proxy.fields
34
+ options[:expose] ||= {}
35
+ options[:expose][:extra_fields] = proxy.extra_fields
36
+ options[:include] = proxy.include_hash
37
+ options[:meta] ||= {}
38
+ options[:meta].merge!(stats: proxy.stats) unless proxy.stats.empty?
39
+ instance.render(records, options)
40
+ end
41
+ end
42
+
43
+ # TODO: more generic notification pattern
44
+ # Likely comes out of debugger work
45
+ def notify
46
+ if defined?(ActiveSupport::Notifications)
47
+ opts = [
48
+ 'render.jsonapi-compliable',
49
+ records: records,
50
+ options: options
51
+ ]
52
+ ActiveSupport::Notifications.instrument(*opts) do
53
+ yield
54
+ end
55
+ else
56
+ yield
57
+ end
58
+ end
59
+ end
60
+ end
@@ -1,728 +1,100 @@
1
1
  module JsonapiCompliable
2
- # Resources hold configuration: How do you want to process incoming JSONAPI
3
- # requests?
4
- #
5
- # Let's say we start with an empty hash as our scope object:
6
- #
7
- # render_jsonapi({})
8
- #
9
- # Let's define the behavior of various parameters. Here we'll merge
10
- # options into our hash when the user filters, sorts, and paginates.
11
- # Then, we'll pass that hash off to an HTTP Client:
12
- #
13
- # class PostResource < ApplicationResource
14
- # type :posts
15
- # use_adapter JsonapiCompliable::Adapters::Null
16
- #
17
- # # What do do when filter[active] parameter comes in
18
- # allow_filter :active do |scope, value|
19
- # scope.merge(active: value)
20
- # end
21
- #
22
- # # What do do when sorting parameters come in
23
- # sort do |scope, attribute, direction|
24
- # scope.merge(order: { attribute => direction })
25
- # end
26
- #
27
- # # What do do when pagination parameters come in
28
- # page do |scope, current_page, per_page|
29
- # scope.merge(page: current_page, per_page: per_page)
30
- # end
31
- #
32
- # # Resolve the scope by passing the hash to an HTTP Client
33
- # def resolve(scope)
34
- # MyHttpClient.get(scope)
35
- # end
36
- # end
37
- #
38
- # This code can quickly become duplicative - we probably want to reuse
39
- # this logic for other objects that use the same HTTP client.
40
- #
41
- # That's why we also have *Adapters*. Adapters encapsulate common, reusable
42
- # resource configuration. That's why we don't need to specify the above code
43
- # when using +ActiveRecord+ - the default logic is already in the adapter.
44
- #
45
- # class PostResource < ApplicationResource
46
- # type :posts
47
- # use_adapter JsonapiCompliable::Adapters::ActiveRecord
48
- #
49
- # allow_filter :title
50
- # end
51
- #
52
- # Of course, we can always override the Resource directly for one-off
53
- # customizations:
54
- #
55
- # class PostResource < ApplicationResource
56
- # type :posts
57
- # use_adapter JsonapiCompliable::Adapters::ActiveRecord
58
- #
59
- # allow_filter :title_prefix do |scope, value|
60
- # scope.where(["title LIKE ?", "#{value}%"])
61
- # end
62
- # end
63
- #
64
- # Resources can also define *Sideloads*. Sideloads define the relationships between resources:
65
- #
66
- # allow_sideload :comments, resource: CommentResource do
67
- # # How to fetch the associated objects
68
- # # This will be further chained down the line
69
- # scope do |posts|
70
- # Comment.where(post_id: posts.map(&:id))
71
- # end
72
- #
73
- # # Now that we've resolved everything, how to assign the objects
74
- # assign do |posts, comments|
75
- # posts.each do |post|
76
- # relevant_comments = comments.select { |c| c.post_id === post.id }
77
- # post.comments = relevant_comments
78
- # end
79
- # end
80
- # end
81
- #
82
- # Once again, we can DRY this up using an Adapter:
83
- #
84
- # use_adapter JsonapiCompliable::Adapters::ActiveRecord
85
- #
86
- # has_many :comments,
87
- # scope: -> { Comment.all },
88
- # resource: CommentResource,
89
- # foreign_key: :post_id
90
2
  class Resource
91
- extend Forwardable
92
- attr_reader :context
93
-
94
- class << self
95
- extend Forwardable
96
- attr_accessor :config
97
-
98
- # @!method allow_sideload
99
- # @see Sideload#allow_sideload
100
- def_delegator :sideloading, :allow_sideload
101
- # @!method has_many
102
- # @see Adapters::ActiveRecordSideloading#has_many
103
- def_delegator :sideloading, :has_many
104
- # @!method has_one
105
- # @see Adapters::ActiveRecordSideloading#has_one
106
- def_delegator :sideloading, :has_one
107
- # @!method belongs_to
108
- # @see Adapters::ActiveRecordSideloading#belongs_to
109
- def_delegator :sideloading, :belongs_to
110
- # @!method has_and_belongs_to_many
111
- # @see Adapters::ActiveRecordSideloading#has_and_belongs_to_many
112
- def_delegator :sideloading, :has_and_belongs_to_many
113
- # @!method polymorphic_belongs_to
114
- # @see Adapters::ActiveRecordSideloading#polymorphic_belongs_to
115
- def_delegator :sideloading, :polymorphic_belongs_to
116
- # @!method polymorphic_has_many
117
- # @see Adapters::ActiveRecordSideloading#polymorphic_has_many
118
- def_delegator :sideloading, :polymorphic_has_many
119
- end
120
-
121
- # @!method sideload
122
- # @see Sideload#sideload
123
- def_delegator :sideloading, :sideload
124
-
125
- # @private
126
- def self.inherited(klass)
127
- klass.config = Util::Hash.deep_dup(self.config)
128
- end
129
-
130
- # @api private
131
- def self.sideloading
132
- @sideloading ||= Sideload.new(:base, resource: self)
133
- end
134
-
135
- # Whitelist a filter
136
- #
137
- # @example Basic Filtering
138
- # allow_filter :title
139
- #
140
- # # When using ActiveRecord, this code is equivalent
141
- # allow_filter :title do |scope, value|
142
- # scope.where(title: value)
143
- # end
144
- #
145
- # @example Custom Filtering
146
- # # All filters can be customized with a block
147
- # allow_filter :title_prefix do |scope, value|
148
- # scope.where('title LIKE ?', "#{value}%")
149
- # end
150
- #
151
- # @example Guarding Filters
152
- # # Only allow the current user to filter on a property
153
- # allow_filter :title, if: :admin?
154
- #
155
- # def admin?
156
- # current_user.role == 'admin'
157
- # end
158
- #
159
- # If a filter is not allowed, a +Jsonapi::Errors::BadFilter+ error will be raised.
160
- #
161
- # @overload allow_filter(name, options = {})
162
- # @param [Symbol] name The name of the filter
163
- # @param [Hash] options
164
- # @option options [Symbol] :if A method name on the current context - If the method returns false, +BadFilter+ will be raised.
165
- # @option options [Array<Symbol>] :aliases Allow the user to specify these aliases in the URL, then match to this filter. Mainly used for backwards-compatibility.
166
- #
167
- # @yieldparam scope The object being scoped
168
- # @yieldparam value The sanitized value from the URL
169
- def self.allow_filter(name, *args, &blk)
170
- opts = args.extract_options!
171
- aliases = [name, opts[:aliases]].flatten.compact
172
- config[:filters][name.to_sym] = {
173
- aliases: aliases,
174
- if: opts[:if],
175
- filter: blk,
176
- required: opts[:required].respond_to?(:call) ? opts[:required] : !!opts[:required]
177
- }
178
- end
3
+ include DSL
4
+ include Interface
5
+ include Configuration
6
+ include Sideloading
179
7
 
180
- # Whitelist a statistic.
181
- #
182
- # Statistics are requested like
183
- #
184
- # GET /posts?stats[total]=count
185
- #
186
- # And returned in +meta+:
187
- #
188
- # {
189
- # data: [...],
190
- # meta: { stats: { total: { count: 100 } } }
191
- # }
192
- #
193
- # Statistics take into account the current scope, *without pagination*.
194
- #
195
- # @example Total Count
196
- # allow_stat total: [:count]
197
- #
198
- # @example Average Rating
199
- # allow_stat rating: [:average]
200
- #
201
- # @example Custom Stat
202
- # allow_stat rating: [:average] do
203
- # standard_deviation { |scope, attr| ... }
204
- # end
205
- #
206
- # @param [Symbol, Hash] symbol_or_hash The attribute and metric
207
- # @yieldparam scope The object being scoped
208
- # @yieldparam [Symbol] attr The name of the metric
209
- def self.allow_stat(symbol_or_hash, &blk)
210
- dsl = Stats::DSL.new(config[:adapter], symbol_or_hash)
211
- dsl.instance_eval(&blk) if blk
212
- config[:stats][dsl.name] = dsl
213
- end
214
-
215
- # When you want a filter to always apply, on every request.
216
- #
217
- # @example Only Active Posts
218
- # default_filter :active do |scope|
219
- # scope.where(active: true)
220
- # end
221
- #
222
- # Default filters can be overridden *if* there is a corresponding +allow_filter+:
223
- #
224
- # @example Overriding Default Filters
225
- # allow_filter :active
226
- #
227
- # default_filter :active do |scope|
228
- # scope.where(active: true)
229
- # end
230
- #
231
- # # GET /posts?filter[active]=false
232
- # # Returns only active posts
233
- #
234
- # @see .allow_filter
235
- # @param [Symbol] name The default filter name
236
- # @yieldparam scope The object being scoped
237
- def self.default_filter(name, &blk)
238
- config[:default_filters][name.to_sym] = {
239
- filter: blk
240
- }
241
- end
242
-
243
- # The Model object associated with this class.
244
- #
245
- # This model will be utilized on write requests.
246
- #
247
- # Models need not be ActiveRecord ;)
248
- #
249
- # @example
250
- # class PostResource < ApplicationResource
251
- # # ... code ...
252
- # model Post
253
- # end
254
- #
255
- # @param [Class] klass The associated Model class
256
- def self.model(klass)
257
- config[:model] = klass
258
- end
259
-
260
- # Register a hook that fires AFTER all validation logic has run -
261
- # including validation of nested objects - but BEFORE the transaction
262
- # has closed.
263
- #
264
- # Helpful for things like "contact this external service after persisting
265
- # data, but roll everything back if there's an error making the service call"
266
- #
267
- # @param [Hash] +only: [:create, :update, :destroy]+
268
- def self.before_commit(only: [:create, :update, :destroy], &blk)
269
- Array(only).each do |verb|
270
- config[:before_commit][verb] = blk
271
- end
272
- end
273
-
274
- # Actually fire the before commit hooks
275
- #
276
- # @see .before_commit
277
- # @api private
278
- def before_commit(model, method)
279
- hook = self.class.config[:before_commit][method]
280
- hook.call(model) if hook
281
- end
282
-
283
- # Define custom sorting logic
284
- #
285
- # @example Sort on alternate table
286
- # # GET /employees?sort=title
287
- # sort do |scope, att, dir|
288
- # if att == :title
289
- # scope.joins(:current_position).order("title #{dir}")
290
- # else
291
- # scope.order(att => dir)
292
- # end
293
- # end
294
- #
295
- # @yieldparam scope The current object being scoped
296
- # @yieldparam [Symbol] att The requested sort attribute
297
- # @yieldparam [Symbol] dir The requested sort direction (:asc/:desc)
298
- def self.sort(&blk)
299
- config[:sorting] = blk
300
- end
301
-
302
- # Define custom pagination logic
303
- #
304
- # @example Use will_paginate instead of Kaminari
305
- # # GET /employees?page[size]=10&page[number]=2
306
- # paginate do |scope, current_page, per_page|
307
- # scope.paginate(page: current_page, per_page: per_page)
308
- # end
309
- #
310
- # @yieldparam scope The current object being scoped
311
- # @yieldparam [Integer] current_page The page[number] parameter value
312
- # @yieldparam [Integer] per_page The page[size] parameter value
313
- def self.paginate(&blk)
314
- config[:pagination] = blk
315
- end
316
-
317
- # Perform special logic when an extra field is requested.
318
- # Often used to eager load data that will be used to compute the
319
- # extra field.
320
- #
321
- # This is *not* required if you have no custom logic.
322
- #
323
- # @example Eager load if extra field is required
324
- # # GET /employees?extra_fields[employees]=net_worth
325
- # extra_field(employees: [:net_worth]) do |scope|
326
- # scope.includes(:assets)
327
- # end
328
- #
329
- # @see Scoping::ExtraFields
330
- #
331
- # @param [Symbol] name Name of the extra field
332
- # @yieldparam scope The current object being scoped
333
- # @yieldparam [Integer] current_page The page[number] parameter value
334
- # @yieldparam [Integer] per_page The page[size] parameter value
335
- def self.extra_field(name, &blk)
336
- config[:extra_fields][name] = blk
337
- end
338
-
339
- # Configure the adapter you want to use.
340
- #
341
- # @example ActiveRecord Adapter
342
- # require 'jsonapi_compliable/adapters/active_record'
343
- # use_adapter JsonapiCompliable::Adapters::ActiveRecord
344
- #
345
- # @param [Class] klass The adapter class
346
- def self.use_adapter(klass)
347
- config[:adapter] = klass.new
348
- end
349
-
350
- # Override default sort applied when not present in the query parameters.
351
- #
352
- # Default: [{ id: :asc }]
353
- #
354
- # @example Order by created_at descending by default
355
- # # GET /employees will order by created_at descending
356
- # default_sort([{ created_at: :desc }])
357
- #
358
- # @param [Array<Hash>] val Array of sorting criteria
359
- def self.default_sort(val)
360
- config[:default_sort] = val
361
- end
362
-
363
- # The JSONAPI Type. For instance if you queried:
364
- #
365
- # GET /employees?fields[positions]=title
366
- #
367
- # And/Or got back in the response
368
- #
369
- # { id: '1', type: 'positions' }
370
- #
371
- # The type would be :positions
372
- #
373
- # This should match the +type+ set in your serializer.
374
- #
375
- # @example
376
- # class PostResource < ApplicationResource
377
- # type :posts
378
- # end
379
- #
380
- # @param [Array<Hash>] value Array of sorting criteria
381
- def self.type(value = nil)
382
- config[:type] = value
383
- end
8
+ attr_reader :context
384
9
 
385
- # Set an alternative default page number. Defaults to 1.
386
- # @param [Integer] val The new default
387
- def self.default_page_number(val)
388
- config[:default_page_number] = val
10
+ def around_scoping(scope, query_hash)
11
+ yield scope
389
12
  end
390
13
 
391
- # Set an alternate default page size, when not specified in query parameters.
392
- #
393
- # @example
394
- # # GET /employees will only render 10 employees
395
- # default_page_size 10
396
- #
397
- # @param [Integer] val The new default page size.
398
- def self.default_page_size(val)
399
- config[:default_page_size] = val
14
+ def serializer_for(model)
15
+ serializer
400
16
  end
401
17
 
402
- # This is where we store all information set via DSL.
403
- # Useful for introspection.
404
- # Gets dup'd when inherited.
405
- #
406
- # @return [Hash] the current configuration
407
- def self.config
408
- @config ||= begin
409
- {
410
- filters: {},
411
- default_filters: {},
412
- extra_fields: {},
413
- stats: {},
414
- sorting: nil,
415
- pagination: nil,
416
- model: nil,
417
- before_commit: {},
418
- adapter: Adapters::Abstract.new
419
- }
420
- end
421
- end
422
-
423
- # Run code within a given context.
424
- # Useful for running code within, say, a Rails controller context
425
- #
426
- # When using Rails, controller actions are wrapped this way.
427
- #
428
- # @example Sinatra
429
- # get '/api/posts' do
430
- # resource.with_context self, :index do
431
- # scope = jsonapi_scope(Tweet.all)
432
- # render_jsonapi(scope.resolve, scope: false)
433
- # end
434
- # end
435
- #
436
- # @see Rails
437
- # @see Base#wrap_context
438
- # @param object The context (Rails controller or equivalent)
439
- # @param namespace One of index/show/etc
440
18
  def with_context(object, namespace = nil)
441
19
  JsonapiCompliable.with_context(object, namespace) do
442
20
  yield
443
21
  end
444
22
  end
445
23
 
446
- # The current context **object** set by +#with_context+. If you are
447
- # using Rails, this is a controller instance.
448
- #
449
- # This method is equivalent to +JsonapiCompliable.context[:object]+
450
- #
451
- # @see #with_context
452
- # @return the context object
453
24
  def context
454
25
  JsonapiCompliable.context[:object]
455
26
  end
456
27
 
457
- # The current context **namespace** set by +#with_context+. If you
458
- # are using Rails, this is the controller method name (e.g. +:index+)
459
- #
460
- # This method is equivalent to +JsonapiCompliable.context[:namespace]+
461
- #
462
- # @see #with_context
463
- # @return [Symbol] the context namespace
464
28
  def context_namespace
465
29
  JsonapiCompliable.context[:namespace]
466
30
  end
467
31
 
468
- # Build a scope using this Resource configuration
469
- #
470
- # Essentially "api private", but can be useful for testing.
471
- #
472
- # @see Scope
473
- # @see Query
474
- # @param base The base scope we are going to chain
475
- # @param query The relevant Query object
476
- # @param opts Opts passed to +Scope.new+
477
- # @return [Scope] a configured Scope instance
478
32
  def build_scope(base, query, opts = {})
479
33
  Scope.new(base, self, query, opts)
480
34
  end
481
35
 
482
- # Create the relevant model.
483
- # You must configure a model (see .model) to create.
484
- # If you override, you *must* return the created instance.
485
- #
486
- # @example Send e-mail on creation
487
- # def create(attributes)
488
- # instance = model.create(attributes)
489
- # UserMailer.welcome_email(instance).deliver_later
490
- # instance
491
- # end
492
- #
493
- # @see .model
494
- # @see Adapters::ActiveRecord#create
495
- # @param [Hash] create_params The relevant attributes, including id and foreign keys
496
- # @return [Object] an instance of the just-created model
36
+ def base_scope
37
+ adapter.base_scope(model)
38
+ end
39
+
40
+ def typecast(name, value, flag)
41
+ att = get_attr!(name, flag)
42
+ type = JsonapiCompliable::Types[att[:type]]
43
+ begin
44
+ flag = :read if flag == :readable
45
+ flag = :write if flag == :writable
46
+ flag = :params if [:sortable, :filterable].include?(flag)
47
+ type[flag][value]
48
+ rescue Exception => e
49
+ raise Errors::TypecastFailed.new(self, name, value, e)
50
+ end
51
+ end
52
+
497
53
  def create(create_params)
498
54
  adapter.create(model, create_params)
499
55
  end
500
56
 
501
- # Update the relevant model.
502
- # You must configure a model (see .model) to update.
503
- # If you override, you *must* return the updated instance.
504
- #
505
- # @example Send e-mail on update
506
- # def update(attributes)
507
- # instance = model.update_attributes(attributes)
508
- # UserMailer.profile_updated_email(instance).deliver_later
509
- # instance
510
- # end
511
- #
512
- # @see .model
513
- # @see Adapters::ActiveRecord#update
514
- # @param [Hash] update_params The relevant attributes, including id and foreign keys
515
- # @return [Object] an instance of the just-updated model
516
57
  def update(update_params)
517
58
  adapter.update(model, update_params)
518
59
  end
519
60
 
520
- # Destroy the relevant model.
521
- # You must configure a model (see .model) to destroy.
522
- # If you override, you *must* return the destroyed instance.
523
- #
524
- # @example Send e-mail on destroy
525
- # def destroy(attributes)
526
- # instance = model_class.find(id)
527
- # instance.destroy
528
- # UserMailer.goodbye_email(instance).deliver_later
529
- # instance
530
- # end
531
- #
532
- # @see .model
533
- # @see Adapters::ActiveRecord#destroy
534
- # @param [String] id The +id+ of the relevant Model
535
- # @return [Object] an instance of the just-destroyed model
536
61
  def destroy(id)
537
62
  adapter.destroy(model, id)
538
63
  end
539
64
 
540
- # Delegates #associate to adapter. Built for overriding.
541
- #
542
- # @see .use_adapter
543
- # @see Adapters::Abstract#associate
544
- # @see Adapters::ActiveRecord#associate
65
+ def associate_all(parent, children, association_name, type)
66
+ adapter.associate_all(parent, children, association_name, type)
67
+ end
68
+
545
69
  def associate(parent, child, association_name, type)
546
70
  adapter.associate(parent, child, association_name, type)
547
71
  end
548
72
 
549
- # Delegates #disassociate to adapter. Built for overriding.
550
- #
551
- # @see .use_adapter
552
- # @see Adapters::Abstract#disassociate
553
- # @see Adapters::ActiveRecord#disassociate
554
73
  def disassociate(parent, child, association_name, type)
555
74
  adapter.disassociate(parent, child, association_name, type)
556
75
  end
557
76
 
558
- # @api private
559
77
  def persist_with_relationships(meta, attributes, relationships, caller_model = nil)
560
78
  persistence = JsonapiCompliable::Util::Persistence \
561
79
  .new(self, meta, attributes, relationships, caller_model)
562
80
  persistence.run
563
81
  end
564
82
 
565
- # @see Sideload#association_names
566
- def association_names
567
- sideloading.association_names
568
- end
569
-
570
- # The relevant proc for the given attribute and calculation.
571
- #
572
- # @example Custom Stats
573
- # # Given this configuration
574
- # allow_stat :rating do
575
- # average { |scope, attr| ... }
576
- # end
577
- #
578
- # # We'd call the method like
579
- # resource.stat(:rating, :average)
580
- # # Which would return the custom proc
581
- #
582
- # Raises +JsonapiCompliable::Errors::StatNotFound+ if not corresponding
583
- # stat has been configured.
584
- #
585
- # @see Errors::StatNotFound
586
- # @param [String, Symbol] attribute The attribute we're calculating.
587
- # @param [String, Symbol] calculation The calculation to run
588
- # @return [Proc] the corresponding callable
589
83
  def stat(attribute, calculation)
590
84
  stats_dsl = stats[attribute] || stats[attribute.to_sym]
591
85
  raise Errors::StatNotFound.new(attribute, calculation) unless stats_dsl
592
86
  stats_dsl.calculation(calculation)
593
87
  end
594
88
 
595
- # Interface to the sideloads for this Resource
596
- # @api private
597
- def sideloading
598
- self.class.sideloading
599
- end
600
-
601
- # @see .default_sort
602
- # @api private
603
- def default_sort
604
- self.class.config[:default_sort] || [{ id: :asc }]
605
- end
606
-
607
- # @see .default_page_number
608
- # @api private
609
- def default_page_number
610
- self.class.config[:default_page_number] || 1
611
- end
612
-
613
- # @see .default_page_size
614
- # @api private
615
- def default_page_size
616
- self.class.config[:default_page_size] || 20
617
- end
618
-
619
- # Returns :undefined_jsonapi_type when not configured.
620
- # @see .type
621
- # @api private
622
- def type
623
- self.class.config[:type] || :undefined_jsonapi_type
624
- end
625
-
626
- # @see .allow_filter
627
- # @api private
628
- def filters
629
- self.class.config[:filters]
630
- end
631
-
632
- # @see .sort
633
- # @api private
634
- def sorting
635
- self.class.config[:sorting]
636
- end
637
-
638
- # @see .allow_stat
639
- # @api private
640
- def stats
641
- self.class.config[:stats]
642
- end
643
-
644
- # @see .paginate
645
- # @api private
646
- def pagination
647
- self.class.config[:pagination]
648
- end
649
-
650
- # @see .extra_field
651
- # @api private
652
- def extra_fields
653
- self.class.config[:extra_fields]
654
- end
655
-
656
- # @see .default_filter
657
- # @api private
658
- def default_filters
659
- self.class.config[:default_filters]
660
- end
661
-
662
- # @see .model
663
- # @api private
664
- def model
665
- self.class.config[:model]
666
- end
667
-
668
- # @see .use_adapter
669
- # @api private
670
- def adapter
671
- self.class.config[:adapter]
672
- end
673
-
674
- # How do you want to resolve the scope?
675
- #
676
- # For ActiveRecord, when we want to actually fire SQL, it's
677
- # +#to_a+.
678
- #
679
- # @example Custom API Call
680
- # # Let's build a hash and pass it off to an HTTP client
681
- # class PostResource < ApplicationResource
682
- # type :posts
683
- # use_adapter JsonapiCompliable::Adapters::Null
684
- #
685
- # sort do |scope, attribute, direction|
686
- # scope.merge!(order: { attribute => direction }
687
- # end
688
- #
689
- # page do |scope, current_page, per_page|
690
- # scope.merge!(page: current_page, per_page: per_page)
691
- # end
692
- #
693
- # def resolve(scope)
694
- # MyHttpClient.get(scope)
695
- # end
696
- # end
697
- #
698
- # This method *must* return an array of resolved model objects.
699
- #
700
- # By default, delegates to the adapter. You likely want to alter your
701
- # adapter rather than override this directly.
702
- #
703
- # @see Adapters::ActiveRecord#resolve
704
- # @param scope The scope object we've built up
705
- # @return [Array] array of resolved model objects
706
89
  def resolve(scope)
707
90
  adapter.resolve(scope)
708
91
  end
709
92
 
710
- # How to run write requests within a transaction.
711
- #
712
- # @example
713
- # resource.transaction do
714
- # # ... save calls ...
715
- # end
716
- #
717
- # Should roll back the transaction, but avoid bubbling up the error,
718
- # if +JsonapiCompliable::Errors::ValidationError+ is raised within
719
- # the block.
720
- #
721
- # By default, delegates to the adapter. You likely want to alter your
722
- # adapter rather than override this directly.
723
- #
724
- # @see Adapters::ActiveRecord#transaction
725
- # @return the result of +yield+
93
+ def before_commit(model, method)
94
+ hook = self.class.config[:before_commit][method]
95
+ hook.call(model) if hook
96
+ end
97
+
726
98
  def transaction
727
99
  response = nil
728
100
  begin