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
@@ -0,0 +1,16 @@
1
+ module JsonapiCompliable
2
+ module Context
3
+ extend ActiveSupport::Concern
4
+
5
+ module Overrides
6
+ def sideload_whitelist=(val)
7
+ super(JSONAPI::IncludeDirective.new(val).to_hash)
8
+ end
9
+ end
10
+
11
+ included do
12
+ class_attribute :sideload_whitelist
13
+ class << self;prepend Overrides;end
14
+ end
15
+ end
16
+ end
@@ -45,26 +45,18 @@
45
45
  #
46
46
  # { type: 'authors', method: :create, temp_id: 'abc123' }
47
47
  class JsonapiCompliable::Deserializer
48
- # @param payload [Hash] The incoming payload with symbolized keys
49
- # @param env [Hash] the Rack env (e.g. +request.env+).
50
- def initialize(payload, env)
51
- @payload = payload || {}
48
+ def initialize(payload)
49
+ @payload = payload
52
50
  @payload = @payload[:_jsonapi] if @payload.has_key?(:_jsonapi)
53
- @env = env
54
- validate_content_type
55
51
  end
56
52
 
57
- # checks Content-Type header and prints a warning if it doesn't seem correct
58
- def validate_content_type
59
- content_type = @env['CONTENT_TYPE'] || ""
60
- if !(content_type.include?("application/json") || content_type.include?("application/vnd.api+json"))
61
- print("WARNING - JSONAPI Compliable :: Content-Type header appears to be set to an invalid value: #{content_type}\n")
62
- end
53
+ def params
54
+ @payload
63
55
  end
64
56
 
65
57
  # @return [Hash] the raw :data value of the payload
66
58
  def data
67
- @payload[:data] || {}
59
+ @payload[:data]
68
60
  end
69
61
 
70
62
  # @return [String] the raw :id value of the payload
@@ -92,11 +84,11 @@ class JsonapiCompliable::Deserializer
92
84
  # +temp_id+: the +temp-id+, if specified
93
85
  #
94
86
  # @return [Hash]
95
- def meta
87
+ def meta(action: :update)
96
88
  {
97
89
  type: data[:type],
98
90
  temp_id: data[:'temp-id'],
99
- method: method
91
+ method: action
100
92
  }
101
93
  end
102
94
 
@@ -105,20 +97,11 @@ class JsonapiCompliable::Deserializer
105
97
  @relationships ||= process_relationships(raw_relationships)
106
98
  end
107
99
 
108
- # Parses the +relationships+ recursively and builds an all-hash
109
- # include directive like
110
- #
111
- # { posts: { comments: {} } }
112
- #
113
- # Relationships that have been marked for destruction will NOT
114
- # be part of the include directive.
115
- #
116
- # @return [Hash] the include directive
117
- def include_directive(memo = {}, relationship_node = nil)
100
+ def include_hash(memo = {}, relationship_node = nil)
118
101
  relationship_node ||= relationships
119
102
 
120
103
  relationship_node.each_pair do |name, relationship_payload|
121
- merge_include_directive(memo, name, relationship_payload)
104
+ merge_include_hash(memo, name, relationship_payload)
122
105
  end
123
106
 
124
107
  memo
@@ -126,12 +109,12 @@ class JsonapiCompliable::Deserializer
126
109
 
127
110
  private
128
111
 
129
- def merge_include_directive(memo, name, relationship_payload)
112
+ def merge_include_hash(memo, name, relationship_payload)
130
113
  arrayified = [relationship_payload].flatten
131
114
  return if arrayified.all? { |rp| removed?(rp) }
132
115
 
133
116
  memo[name] ||= {}
134
- deep_merge!(memo[name], sub_directives(memo[name], arrayified))
117
+ deep_merge!(memo[name], nested_include_hashes(memo[name], arrayified))
135
118
  memo
136
119
  end
137
120
 
@@ -139,24 +122,16 @@ class JsonapiCompliable::Deserializer
139
122
  @payload[:included] || []
140
123
  end
141
124
 
142
- def method
143
- case @env['REQUEST_METHOD']
144
- when 'POST' then :create
145
- when 'PUT', 'PATCH' then :update
146
- when 'DELETE' then :destroy
147
- end
148
- end
149
-
150
125
  def removed?(relationship_payload)
151
126
  method = relationship_payload[:meta][:method]
152
127
  [:disassociate, :destroy].include?(method)
153
128
  end
154
129
 
155
- def sub_directives(memo, relationship_payloads)
130
+ def nested_include_hashes(memo, relationship_payloads)
156
131
  {}.tap do |subs|
157
132
  relationship_payloads.each do |rp|
158
- sub_directive = include_directive(memo, rp[:relationships])
159
- deep_merge!(subs, sub_directive)
133
+ nested = include_hash(memo, rp[:relationships])
134
+ deep_merge!(subs, nested)
160
135
  end
161
136
  end
162
137
  end
@@ -1,8 +1,98 @@
1
1
  module JsonapiCompliable
2
2
  module Errors
3
- class BadFilter < StandardError; end
3
+ class Base < StandardError;end
4
4
 
5
- class ValidationError < StandardError
5
+ class AdapterNotImplemented < Base
6
+ def initialize(adapter, attribute, method)
7
+ @adapter = adapter
8
+ @attribute = attribute
9
+ @method = method
10
+ end
11
+
12
+ def message
13
+ <<-MSG
14
+ The adapter #{@adapter.class} does not implement method '#{@method}', which was requested for attribute '#{@attribute}'. Add this method to your adapter to support this filter operator.
15
+ MSG
16
+ end
17
+ end
18
+
19
+ class AttributeError < Base
20
+ attr_reader :resource,
21
+ :name,
22
+ :flag,
23
+ :exists,
24
+ :request,
25
+ :guard
26
+
27
+ def initialize(resource, name, flag, **opts)
28
+ @resource = resource
29
+ @name = name
30
+ @flag = flag
31
+ @exists = opts[:exists] || false
32
+ @request = opts[:request] || false
33
+ @guard = opts[:guard]
34
+ end
35
+
36
+ def action
37
+ if @request
38
+ {
39
+ sortable: 'sort on',
40
+ filterable: 'filter on',
41
+ readable: 'read',
42
+ writable: 'write'
43
+ }[@flag]
44
+ else
45
+ {
46
+ sortable: 'add sort',
47
+ filterable: 'add filter',
48
+ readable: 'read',
49
+ writable: 'write'
50
+ }[@flag]
51
+ end
52
+ end
53
+
54
+ def resource_name
55
+ name = if @resource.is_a?(JsonapiCompliable::Resource)
56
+ @resource.class.name
57
+ else
58
+ @resource.name
59
+ end
60
+ name || 'AnonymousResourceClass'
61
+ end
62
+
63
+ def message
64
+ msg = "#{resource_name}: Tried to #{action} attribute #{@name.inspect}"
65
+ if @exists
66
+ if @guard
67
+ msg << ", but the guard #{@guard.inspect} did not pass."
68
+ else
69
+ msg << ", but the attribute was marked #{@flag.inspect} => false."
70
+ end
71
+ else
72
+ msg << ", but could not find an attribute with that name."
73
+ end
74
+ msg
75
+ end
76
+ end
77
+
78
+ class PolymorphicChildNotFound < Base
79
+ def initialize(resource_class, model)
80
+ @resource_class = resource_class
81
+ @model = model
82
+ end
83
+
84
+ def message
85
+ <<-MSG
86
+ #{@resource_class}: Tried to find subclass with model #{@model.class}, but nothing found!
87
+
88
+ Make sure all your child classes are assigned and associated to the right models:
89
+
90
+ self.polymorphic = ['Subclass1Resource', 'Subclass2Resource']
91
+ MSG
92
+ end
93
+ end
94
+
95
+ class ValidationError < Base
6
96
  attr_reader :validation_response
7
97
 
8
98
  def initialize(validation_response)
@@ -10,41 +100,153 @@ module JsonapiCompliable
10
100
  end
11
101
  end
12
102
 
13
- class MissingSerializer < StandardError
14
- def initialize(class_name, serializer_name)
15
- @class_name = class_name
16
- @serializer_name = serializer_name
103
+ class ImplicitFilterTypeMissing < Base
104
+ def initialize(resource_class, name)
105
+ @resource_class = resource_class
106
+ @name = name
17
107
  end
18
108
 
19
109
  def message
20
110
  <<-MSG
21
- Could not find serializer for class '#{@class_name}'!
111
+ Tried to add filter-only attribute #{@name.inspect}, but type was missing!
22
112
 
23
- Looked for '#{@serializer_name}' but doesn't appear to exist.
113
+ If you are adding a filter that does not have a corresponding attribute, you must pass a type:
24
114
 
25
- Use a custom Inferrer if you'd like different lookup logic.
115
+ filter :name, :string do <--- like this
116
+ # ... code ...
117
+ end
26
118
  MSG
27
119
  end
28
120
  end
29
121
 
30
- class MissingSerializer < StandardError
31
- def initialize(class_name, serializer_name)
32
- @class_name = class_name
33
- @serializer_name = serializer_name
122
+ class ImplicitSortTypeMissing < Base
123
+ def initialize(resource_class, name)
124
+ @resource_class = resource_class
125
+ @name = name
34
126
  end
35
127
 
36
128
  def message
37
129
  <<-MSG
38
- Could not find serializer for class '#{@class_name}'!
130
+ Tried to add sort-only attribute #{@name.inspect}, but type was missing!
131
+
132
+ If you are adding a sort that does not have a corresponding attribute, you must pass a type:
133
+
134
+ sort :name, :string do <--- like this
135
+ # ... code ...
136
+ end
137
+ MSG
138
+ end
139
+ end
140
+
141
+ class TypecastFailed < Base
142
+ def initialize(resource, name, value, error)
143
+ @resource = resource
144
+ @name = name
145
+ @value = value
146
+ @error = error
147
+ end
148
+
149
+ def message
150
+ <<-MSG
151
+ #{@resource.class}: Failed typecasting #{@name.inspect}! Given #{@value.inspect} but the following error was raised:
152
+
153
+ #{@error.message}
154
+
155
+ #{@error.backtrace.join("\n")}
156
+ MSG
157
+ end
158
+ end
159
+
160
+ class ModelNotFound < Base
161
+ def initialize(resource_class)
162
+ @resource_class = resource_class
163
+ end
164
+
165
+ def message
166
+ <<-MSG
167
+ Could not find model for Resource '#{@resource_class}'
168
+
169
+ Manually set model (self.model = MyModel) if it does not match name of the Resource.
170
+ MSG
171
+ end
172
+ end
173
+
174
+ class TypeNotFound < Base
175
+ def initialize(resource, attribute, type)
176
+ @resource = resource
177
+ @attribute = attribute
178
+ @type = type
179
+ end
180
+
181
+ def message
182
+ <<-MSG
183
+ Could not find type #{@type.inspect}! This was specified on attribute #{@attribute.inspect} within resource #{@resource.name}
184
+
185
+ Valid types are: #{JsonapiCompliable::Types.map.keys.inspect}
186
+ MSG
187
+ end
188
+ end
189
+
190
+ class PolymorphicChildNotFound < Base
191
+ def initialize(sideload, name)
192
+ @sideload = sideload
193
+ @name = name
194
+ end
195
+
196
+ def message
197
+ <<-MSG
198
+ #{@sideload.parent_resource}: Found record with #{@sideload.grouper.column_name.inspect} == #{@name.inspect}, which is not registered!
199
+
200
+ Register the behavior of different types like so:
201
+
202
+ polymorphic_belongs_to #{@sideload.name.inspect} do
203
+ group_by(#{@sideload.grouper.column_name.inspect}) do
204
+ on(#{@name.to_sym.inspect}) <---- this is what's missing
205
+ on(:foo).belongs_to :foo, resource: FooResource (long-hand example)
206
+ end
207
+ end
208
+ MSG
209
+ end
210
+ end
211
+
212
+ class ResourceNotFound < Base
213
+ def initialize(resource, sideload_name)
214
+ @resource = resource
215
+ @sideload_name = sideload_name
216
+ end
217
+
218
+ def message
219
+ <<-MSG
220
+ Could not find resource class for sideload '#{@sideload_name}' on Resource '#{@resource.class.name}'!
221
+
222
+ If this follows a non-standard naming convention, use the :resource option to pass it directly:
223
+
224
+ has_many :comments, resource: SpecialCommentResource
225
+ MSG
226
+ end
227
+ end
228
+
229
+ class UnsupportedPagination < Base
230
+ def message
231
+ <<-MSG
232
+ It looks like you are requesting pagination of a sideload, but there are > 1 parents.
233
+
234
+ This is not supported. In other words, you can do
235
+
236
+ /employees/1?include=positions&page[positions][size]=2
237
+
238
+ But not
239
+
240
+ /employees?include=positions&page[positions][size]=2
39
241
 
40
- Looked for '#{@serializer_name}' but doesn't appear to exist.
242
+ This is a limitation of most datastores; the same issue exists in ActiveRecord.
41
243
 
42
- Use a custom Inferrer if you'd like different lookup logic.
244
+ Consider using a named relationship instead, e.g. 'has_one :top_comment'
43
245
  MSG
44
246
  end
45
247
  end
46
248
 
47
- class UnsupportedPageSize < StandardError
249
+ class UnsupportedPageSize < Base
48
250
  def initialize(size, max)
49
251
  @size, @max = size, max
50
252
  end
@@ -54,7 +256,7 @@ Use a custom Inferrer if you'd like different lookup logic.
54
256
  end
55
257
  end
56
258
 
57
- class InvalidInclude < StandardError
259
+ class InvalidInclude < Base
58
260
  def initialize(relationship, parent_resource)
59
261
  @relationship = relationship
60
262
  @parent_resource = parent_resource
@@ -65,7 +267,7 @@ Use a custom Inferrer if you'd like different lookup logic.
65
267
  end
66
268
  end
67
269
 
68
- class StatNotFound < StandardError
270
+ class StatNotFound < Base
69
271
  def initialize(attribute, calculation)
70
272
  @attribute = attribute
71
273
  @calculation = calculation
@@ -86,19 +288,20 @@ Use a custom Inferrer if you'd like different lookup logic.
86
288
  end
87
289
  end
88
290
 
89
- class RecordNotFound < StandardError
291
+ class RecordNotFound < Base
90
292
  end
91
293
 
92
- class RequiredFilter < StandardError
93
- def initialize(attributes)
294
+ class RequiredFilter < Base
295
+ def initialize(resource, attributes)
296
+ @resource = resource
94
297
  @attributes = Array(attributes)
95
298
  end
96
299
 
97
300
  def message
98
301
  if @attributes.length > 1
99
- "The required filters \"#{@attributes.join(', ')}\" were not provided"
302
+ "The required filters \"#{@attributes.join(', ')}\" on resource #{@resource.class} were not provided"
100
303
  else
101
- "The required filter \"#{@attributes[0]}\" was not provided"
304
+ "The required filter \"#{@attributes[0]}\" on resource #{@resource.class} was not provided"
102
305
  end
103
306
  end
104
307
  end
@@ -46,7 +46,9 @@ module JsonapiCompliable
46
46
  next false unless instance_eval(&options[:if])
47
47
  end
48
48
 
49
- @extra_fields[@_type] && @extra_fields[@_type].include?(name)
49
+ @extra_fields &&
50
+ @extra_fields[@_type] &&
51
+ @extra_fields[@_type].include?(name)
50
52
  }
51
53
 
52
54
  attribute name, if: allow_field, &blk