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.
- checksums.yaml +5 -5
- data/.ruby-version +1 -1
- data/.travis.yml +1 -2
- data/Rakefile +7 -3
- data/jsonapi_compliable.gemspec +7 -3
- data/lib/generators/jsonapi/resource_generator.rb +8 -79
- data/lib/generators/jsonapi/templates/application_resource.rb.erb +2 -1
- data/lib/generators/jsonapi/templates/controller.rb.erb +19 -64
- data/lib/generators/jsonapi/templates/resource.rb.erb +5 -47
- data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
- data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
- data/lib/jsonapi_compliable.rb +87 -18
- data/lib/jsonapi_compliable/adapters/abstract.rb +202 -45
- data/lib/jsonapi_compliable/adapters/active_record.rb +6 -130
- data/lib/jsonapi_compliable/adapters/active_record/base.rb +247 -0
- data/lib/jsonapi_compliable/adapters/active_record/belongs_to_sideload.rb +17 -0
- data/lib/jsonapi_compliable/adapters/active_record/has_many_sideload.rb +17 -0
- data/lib/jsonapi_compliable/adapters/active_record/has_one_sideload.rb +17 -0
- data/lib/jsonapi_compliable/adapters/active_record/inferrence.rb +12 -0
- data/lib/jsonapi_compliable/adapters/active_record/many_to_many_sideload.rb +30 -0
- data/lib/jsonapi_compliable/adapters/null.rb +177 -6
- data/lib/jsonapi_compliable/base.rb +33 -320
- data/lib/jsonapi_compliable/context.rb +16 -0
- data/lib/jsonapi_compliable/deserializer.rb +14 -39
- data/lib/jsonapi_compliable/errors.rb +227 -24
- data/lib/jsonapi_compliable/extensions/extra_attribute.rb +3 -1
- data/lib/jsonapi_compliable/filter_operators.rb +25 -0
- data/lib/jsonapi_compliable/hash_renderer.rb +57 -0
- data/lib/jsonapi_compliable/query.rb +190 -202
- data/lib/jsonapi_compliable/rails.rb +12 -6
- data/lib/jsonapi_compliable/railtie.rb +64 -0
- data/lib/jsonapi_compliable/renderer.rb +60 -0
- data/lib/jsonapi_compliable/resource.rb +35 -663
- data/lib/jsonapi_compliable/resource/configuration.rb +239 -0
- data/lib/jsonapi_compliable/resource/dsl.rb +138 -0
- data/lib/jsonapi_compliable/resource/interface.rb +32 -0
- data/lib/jsonapi_compliable/resource/polymorphism.rb +68 -0
- data/lib/jsonapi_compliable/resource/sideloading.rb +102 -0
- data/lib/jsonapi_compliable/resource_proxy.rb +127 -0
- data/lib/jsonapi_compliable/responders.rb +19 -0
- data/lib/jsonapi_compliable/runner.rb +25 -0
- data/lib/jsonapi_compliable/scope.rb +37 -79
- data/lib/jsonapi_compliable/scoping/extra_attributes.rb +29 -0
- data/lib/jsonapi_compliable/scoping/filter.rb +39 -58
- data/lib/jsonapi_compliable/scoping/filterable.rb +9 -14
- data/lib/jsonapi_compliable/scoping/paginate.rb +9 -3
- data/lib/jsonapi_compliable/scoping/sort.rb +16 -4
- data/lib/jsonapi_compliable/sideload.rb +221 -347
- data/lib/jsonapi_compliable/sideload/belongs_to.rb +34 -0
- data/lib/jsonapi_compliable/sideload/has_many.rb +16 -0
- data/lib/jsonapi_compliable/sideload/has_one.rb +9 -0
- data/lib/jsonapi_compliable/sideload/many_to_many.rb +24 -0
- data/lib/jsonapi_compliable/sideload/polymorphic_belongs_to.rb +108 -0
- data/lib/jsonapi_compliable/stats/payload.rb +4 -8
- data/lib/jsonapi_compliable/types.rb +172 -0
- data/lib/jsonapi_compliable/util/attribute_check.rb +88 -0
- data/lib/jsonapi_compliable/util/persistence.rb +29 -7
- data/lib/jsonapi_compliable/util/relationship_payload.rb +4 -4
- data/lib/jsonapi_compliable/util/render_options.rb +4 -32
- data/lib/jsonapi_compliable/util/serializer_attributes.rb +98 -0
- data/lib/jsonapi_compliable/util/validation_response.rb +15 -9
- data/lib/jsonapi_compliable/version.rb +1 -1
- metadata +105 -24
- data/lib/generators/jsonapi/field_generator.rb +0 -0
- data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +0 -29
- data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +0 -20
- data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +0 -22
- data/lib/generators/jsonapi/templates/payload.rb.erb +0 -39
- data/lib/generators/jsonapi/templates/serializer.rb.erb +0 -25
- data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +0 -19
- data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +0 -33
- data/lib/jsonapi_compliable/adapters/active_record_sideloading.rb +0 -152
- 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
|
-
|
49
|
-
|
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
|
-
|
58
|
-
|
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:
|
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
|
-
|
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
|
-
|
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
|
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],
|
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
|
130
|
+
def nested_include_hashes(memo, relationship_payloads)
|
156
131
|
{}.tap do |subs|
|
157
132
|
relationship_payloads.each do |rp|
|
158
|
-
|
159
|
-
deep_merge!(subs,
|
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
|
3
|
+
class Base < StandardError;end
|
4
4
|
|
5
|
-
class
|
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
|
14
|
-
def initialize(
|
15
|
-
@
|
16
|
-
@
|
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
|
-
|
111
|
+
Tried to add filter-only attribute #{@name.inspect}, but type was missing!
|
22
112
|
|
23
|
-
|
113
|
+
If you are adding a filter that does not have a corresponding attribute, you must pass a type:
|
24
114
|
|
25
|
-
|
115
|
+
filter :name, :string do <--- like this
|
116
|
+
# ... code ...
|
117
|
+
end
|
26
118
|
MSG
|
27
119
|
end
|
28
120
|
end
|
29
121
|
|
30
|
-
class
|
31
|
-
def initialize(
|
32
|
-
@
|
33
|
-
@
|
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
|
-
|
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
|
-
|
242
|
+
This is a limitation of most datastores; the same issue exists in ActiveRecord.
|
41
243
|
|
42
|
-
|
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 <
|
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 <
|
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 <
|
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 <
|
291
|
+
class RecordNotFound < Base
|
90
292
|
end
|
91
293
|
|
92
|
-
class RequiredFilter <
|
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
|
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
|