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.
- 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
|