jsonapi_compliable 0.11.34 → 1.0.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
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