jsonapi-realizer 4.4.0 → 5.0.0
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 +4 -4
- data/README.md +13 -104
- data/lib/jsonapi/realizer.rb +26 -25
- data/lib/jsonapi/realizer/action.rb +2 -2
- data/lib/jsonapi/realizer/adapter.rb +21 -58
- data/lib/jsonapi/realizer/adapter/active_record.rb +38 -17
- data/lib/jsonapi/realizer/adapter_spec.rb +3 -2
- data/lib/jsonapi/realizer/configuration.rb +22 -0
- data/lib/jsonapi/realizer/context.rb +8 -0
- data/lib/jsonapi/realizer/controller.rb +55 -0
- data/lib/jsonapi/realizer/error.rb +10 -9
- data/lib/jsonapi/realizer/error/invalid_content_type_header.rb +5 -0
- data/lib/jsonapi/realizer/error/invalid_data_type_property.rb +13 -0
- data/lib/jsonapi/realizer/error/invalid_root_property.rb +13 -0
- data/lib/jsonapi/realizer/error/{duplicate_registration.rb → missing_data_type_property.rb} +1 -1
- data/lib/jsonapi/realizer/error/resource_attribute_not_found.rb +14 -0
- data/lib/jsonapi/realizer/error/resource_relationship_not_found.rb +14 -0
- data/lib/jsonapi/realizer/resource.rb +278 -73
- data/lib/jsonapi/realizer/resource/attribute.rb +23 -0
- data/lib/jsonapi/realizer/resource/configuration.rb +27 -0
- data/lib/jsonapi/realizer/resource/relation.rb +31 -0
- data/lib/jsonapi/realizer/resource_spec.rb +55 -8
- data/lib/jsonapi/realizer/version.rb +1 -1
- data/lib/jsonapi/realizer_spec.rb +22 -119
- metadata +70 -20
- data/lib/jsonapi/realizer/action/create.rb +0 -36
- data/lib/jsonapi/realizer/action/create_spec.rb +0 -165
- data/lib/jsonapi/realizer/action/destroy.rb +0 -27
- data/lib/jsonapi/realizer/action/destroy_spec.rb +0 -81
- data/lib/jsonapi/realizer/action/index.rb +0 -29
- data/lib/jsonapi/realizer/action/index_spec.rb +0 -75
- data/lib/jsonapi/realizer/action/show.rb +0 -35
- data/lib/jsonapi/realizer/action/show_spec.rb +0 -81
- data/lib/jsonapi/realizer/action/update.rb +0 -37
- data/lib/jsonapi/realizer/action/update_spec.rb +0 -170
- data/lib/jsonapi/realizer/action_spec.rb +0 -46
- data/lib/jsonapi/realizer/adapter/memory.rb +0 -31
- data/lib/jsonapi/realizer/error/invalid_accept_header.rb +0 -9
- data/lib/jsonapi/realizer/error/malformed_data_root_property.rb +0 -9
- data/lib/jsonapi/realizer/error/missing_accept_header.rb +0 -9
- data/lib/jsonapi/realizer/error/missing_type_resource_property.rb +0 -9
@@ -0,0 +1,55 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
module Realizer
|
3
|
+
module Controller
|
4
|
+
private def reject_missing_content_type_header
|
5
|
+
return if request.body.size.zero?
|
6
|
+
return if request.headers.property?("Content-Type")
|
7
|
+
|
8
|
+
raise(JSONAPI::Realizer.configuration.default_missing_content_type_exception)
|
9
|
+
end
|
10
|
+
|
11
|
+
private def reject_invalid_content_type_header
|
12
|
+
reject_missing_content_type_header
|
13
|
+
|
14
|
+
return if request.headers.fetch("Content-Type").include?(JSONAPI::MEDIA_TYPE)
|
15
|
+
|
16
|
+
raise(JSONAPI::Realizer.configuration.default_invalid_content_type_exception)
|
17
|
+
end
|
18
|
+
|
19
|
+
private def reject_missing_root_property
|
20
|
+
return if request.parameters.key?("body")
|
21
|
+
return if request.paremters.key?("errors")
|
22
|
+
return if request.paremters.key?("meta")
|
23
|
+
|
24
|
+
raise(Error::MissingRootProperty)
|
25
|
+
end
|
26
|
+
|
27
|
+
private def reject_invalid_root_property
|
28
|
+
reject_missing_root_property
|
29
|
+
|
30
|
+
return unless request.parameters.key?("data") && (request.parameters.fetch("data").is_a?(Hash) || request.parameters.fetch("data").is_a?(Array))
|
31
|
+
return unless request.parameters.key?("errors") && request.parameters.fetch("errors").is_a?(Array)
|
32
|
+
|
33
|
+
raise(Error::InvalidRootProperty)
|
34
|
+
end
|
35
|
+
|
36
|
+
private def reject_missing_type_property
|
37
|
+
reject_invalid_root_property
|
38
|
+
|
39
|
+
return if request.parameters.fetch("data").is_a?(Hash) && request.parameters.fetch("data").key?("type")
|
40
|
+
return if request.parameters.fetch("data").is_a?(Array) && request.parameters.fetch("data").all? { |data| data.key?("type") }
|
41
|
+
|
42
|
+
raise(Error::MissingDataTypeProperty)
|
43
|
+
end
|
44
|
+
|
45
|
+
private def reject_invalid_type_property
|
46
|
+
reject_missing_type_property
|
47
|
+
|
48
|
+
return if request.parameters.fetch("data").is_a?(Hash) && request.parameters.fetch("data").fetch("type").is_a?(String) && request.parameters.fetch("data").fetch("type").present?
|
49
|
+
return if request.parameters.fetch("data").is_a?(Array) && request.parameters.fetch("data").map {|data| data.fetch("type")}.all? {|type| type.is_a?(String) && type.present? }
|
50
|
+
|
51
|
+
raise(Error::InvalidDataTypeProperty)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,15 +1,16 @@
|
|
1
1
|
module JSONAPI
|
2
2
|
module Realizer
|
3
3
|
class Error < StandardError
|
4
|
-
|
5
|
-
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
10
|
-
require_relative
|
11
|
-
require_relative
|
12
|
-
require_relative
|
4
|
+
include(ActiveModel::Model)
|
5
|
+
|
6
|
+
require_relative("error/invalid_content_type_header")
|
7
|
+
require_relative("error/missing_content_type_header")
|
8
|
+
require_relative("error/invalid_root_property")
|
9
|
+
require_relative("error/missing_root_property")
|
10
|
+
require_relative("error/missing_data_type_property")
|
11
|
+
require_relative("error/include_without_data_property")
|
12
|
+
require_relative("error/resource_attribute_not_found")
|
13
|
+
require_relative("error/resource_relationship_not_found")
|
13
14
|
end
|
14
15
|
end
|
15
16
|
end
|
@@ -1,127 +1,332 @@
|
|
1
1
|
module JSONAPI
|
2
2
|
module Realizer
|
3
3
|
module Resource
|
4
|
-
|
4
|
+
require_relative("resource/configuration")
|
5
|
+
require_relative("resource/attribute")
|
6
|
+
require_relative("resource/relation")
|
5
7
|
|
6
|
-
|
8
|
+
extend(ActiveSupport::Concern)
|
9
|
+
include(ActiveModel::Model)
|
7
10
|
|
8
|
-
|
9
|
-
@
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
MIXIN_HOOK = ->(*) do
|
12
|
+
@attributes = {}
|
13
|
+
@relations = {}
|
14
|
+
|
15
|
+
unless const_defined?("Context")
|
16
|
+
self::Context = Class.new do
|
17
|
+
include(JSONAPI::Realizer::Context)
|
18
|
+
|
19
|
+
def initialize(**keyword_arguments)
|
20
|
+
keyword_arguments.keys.each(&singleton_class.method(:attr_accessor))
|
21
|
+
|
22
|
+
super(**keyword_arguments)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
validates_presence_of(:intent)
|
28
|
+
validates_presence_of(:parameters, :allow_empty => true)
|
29
|
+
validates_presence_of(:headers, :allow_empty => true)
|
30
|
+
|
31
|
+
identifier(JSONAPI::Realizer.configuration.default_identifier)
|
32
|
+
|
33
|
+
has(JSONAPI::Realizer.configuration.default_identifier)
|
19
34
|
end
|
35
|
+
private_constant :MIXIN_HOOK
|
36
|
+
|
37
|
+
attr_writer(:intent)
|
38
|
+
attr_accessor(:parameters)
|
39
|
+
attr_accessor(:headers)
|
40
|
+
attr_writer(:context)
|
41
|
+
attr_accessor(:scope)
|
42
|
+
|
43
|
+
def initialize(**keyword_arguments)
|
44
|
+
super(**keyword_arguments)
|
45
|
+
|
46
|
+
context.validate!
|
47
|
+
validate!
|
20
48
|
|
21
|
-
|
22
|
-
|
49
|
+
if filtering?
|
50
|
+
@scope = adapter.filtering(scope, filters)
|
51
|
+
end
|
52
|
+
|
53
|
+
if include?
|
54
|
+
@scope = adapter.include_relationships(scope, includes)
|
55
|
+
end
|
56
|
+
|
57
|
+
if sorting?
|
58
|
+
@scope = adapter.sorting(scope, sorts)
|
59
|
+
end
|
60
|
+
|
61
|
+
if paginate?
|
62
|
+
@scope = adapter.paginate(scope, *pagination)
|
63
|
+
end
|
64
|
+
|
65
|
+
if writing? && data?
|
66
|
+
adapter.write_attributes(object, attributes)
|
67
|
+
adapter.write_relationships(object, relationships)
|
68
|
+
end
|
23
69
|
end
|
24
70
|
|
25
|
-
def
|
26
|
-
@
|
71
|
+
def to_hash
|
72
|
+
@native ||= {
|
73
|
+
:pagination => if paginate? then pagination end,
|
74
|
+
:selects => if selects? then selects end,
|
75
|
+
:includes => if include? then includes end,
|
76
|
+
:object => object
|
77
|
+
}.compact
|
27
78
|
end
|
28
79
|
|
29
|
-
def
|
30
|
-
|
80
|
+
private def writing?
|
81
|
+
[:create, :update].include?(intent)
|
31
82
|
end
|
32
83
|
|
33
|
-
|
34
|
-
|
84
|
+
def paginate?
|
85
|
+
parameters.key?("page") && (parameters.fetch("page").key?("limit") || parameters.fetch("page").key?("offset"))
|
35
86
|
end
|
36
87
|
|
37
|
-
|
38
|
-
|
88
|
+
def pagination
|
89
|
+
[
|
90
|
+
parameters.fetch("page").fetch("limit", nil),
|
91
|
+
parameters.fetch("page").fetch("offset", nil)
|
92
|
+
]
|
39
93
|
end
|
40
94
|
|
41
|
-
|
42
|
-
|
95
|
+
def sorting?
|
96
|
+
parameters.key?("sort")
|
43
97
|
end
|
44
98
|
|
45
|
-
|
46
|
-
|
99
|
+
def sorts
|
100
|
+
@sorts ||= parameters.
|
101
|
+
# {sort: "name,-age,accounts.created_at,-accounts.updated_at"}
|
102
|
+
fetch("sort").
|
103
|
+
# "name,-age,accounts.created_at,-accounts.updated_at"
|
104
|
+
split(",").
|
105
|
+
# ["name", "-age", "accounts.created_at", "-accounts.updated_at"]
|
106
|
+
map do |token|
|
107
|
+
if token.start_with?("-") then [token.sub(/^-/, "").underscore, "-"] else [token.underscore, "+"] end
|
108
|
+
end.
|
109
|
+
# [["name", "+"], ["age", "-"], ["accounts.created_at", "+"], ["accounts.updated_at", "-"]]
|
110
|
+
map do |(path, direction)|
|
111
|
+
[if path.include?(".") then path.split(".") else [self.class.configuration.type, path] end, direction]
|
112
|
+
end
|
113
|
+
# [[["accounts", "name"], "+"], [["accounts", "age"], "-"], [["accounts", "created_at"], "+"], [["accounts", "updated_at"], "-"]]
|
47
114
|
end
|
48
115
|
|
49
|
-
|
50
|
-
|
116
|
+
def filtering?
|
117
|
+
parameters.key?("filter")
|
51
118
|
end
|
52
119
|
|
53
|
-
|
54
|
-
|
120
|
+
def filters
|
121
|
+
@filters ||= parameters.
|
122
|
+
# {"filter" => {"full-name" => "Abby Marquardt", "email" => "amado@goldner.com"}}
|
123
|
+
fetch("filter").
|
124
|
+
# {"full-name" => "Abby Marquardt", "email" => "amado@goldner.com"}
|
125
|
+
transform_keys(&:underscore)
|
126
|
+
# {"full_name" => "Abby Marquardt", "email" => "amado@goldner.com"}
|
55
127
|
end
|
56
128
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
129
|
+
def include?
|
130
|
+
parameters.key?("include")
|
131
|
+
end
|
61
132
|
|
62
|
-
|
63
|
-
|
64
|
-
|
133
|
+
def includes
|
134
|
+
@includes ||= parameters.
|
135
|
+
# {"include" => "active-photographer.photographs,comments,comments.author"}
|
136
|
+
fetch("include").
|
137
|
+
# "active-photographer.photographs,comments,comments.author"
|
138
|
+
split(/\s*,\s*/).
|
139
|
+
# ["active-photographer.photographs", "comments", "comments.author"]
|
140
|
+
map {|chain| chain.split(".")}.
|
141
|
+
# [["active-photographer", "photographs"], ["comments"], ["comments", "author"]]
|
142
|
+
map {|list| list.map(&:underscore)}.
|
143
|
+
# [["active_photographer", "photographs"], ["comments"], ["comments", "author"]]
|
144
|
+
map do |relationship_chain|
|
145
|
+
# This walks down the path of relationships and normalizes thenm to
|
146
|
+
# their defined "as", which lets us expose AccountRealizer#name, but that actually
|
147
|
+
# references Account#full_name.
|
148
|
+
relationship_chain.reduce([[], self.class]) do |(normalized_relationship_chain, realizer_class), relationship_link|
|
149
|
+
[
|
150
|
+
[
|
151
|
+
*normalized_relationship_chain,
|
152
|
+
realizer_class.relation(relationship_link).as
|
153
|
+
],
|
154
|
+
realizer_class.relation(relationship_link).realizer_class
|
155
|
+
]
|
156
|
+
end.first
|
157
|
+
end
|
158
|
+
# [["account", "photographs"], ["comments"], ["comments", "account"]]
|
159
|
+
end
|
65
160
|
|
66
|
-
|
67
|
-
|
68
|
-
|
161
|
+
def selects?
|
162
|
+
parameters.key?("fields")
|
163
|
+
end
|
69
164
|
|
70
|
-
|
71
|
-
|
72
|
-
|
165
|
+
def selects
|
166
|
+
@selects ||= parameters.
|
167
|
+
# {"fields" => {"articles" => "title,body,sub-text", "people" => "name"}}
|
168
|
+
fetch("fields").
|
169
|
+
# {"articles" => "title,body,sub-text", "people" => "name"}
|
170
|
+
transform_keys(&:underscore).
|
171
|
+
# {"articles" => "title,body,sub-text", "people" => "name"}
|
172
|
+
transform_values {|value| value.split(/\s*,\s*/)}.
|
173
|
+
# {"articles" => ["title", "body", "sub-text"], "people" => ["name"]}
|
174
|
+
transform_values {|value| value.map(&:underscore)}
|
175
|
+
# {"articles" => ["title", "body", "sub_text"], "people" => ["name"]}
|
176
|
+
end
|
177
|
+
|
178
|
+
private def data?
|
179
|
+
parameters.key?("data")
|
180
|
+
end
|
181
|
+
|
182
|
+
private def data
|
183
|
+
@data ||= parameters.fetch("data")
|
184
|
+
end
|
185
|
+
|
186
|
+
private def type
|
187
|
+
return unless data.key?("type")
|
188
|
+
|
189
|
+
@type ||= data.fetch("type")
|
190
|
+
end
|
191
|
+
|
192
|
+
def attributes
|
193
|
+
return unless data.key?("attributes")
|
194
|
+
|
195
|
+
@attributes ||= data.
|
196
|
+
fetch("attributes").
|
197
|
+
transform_keys(&:underscore).
|
198
|
+
transform_keys{|key| attribute(key).as}
|
199
|
+
end
|
200
|
+
|
201
|
+
def relationships
|
202
|
+
return unless data.key?("relationships")
|
73
203
|
|
74
|
-
|
75
|
-
|
204
|
+
@relationships ||= data.
|
205
|
+
fetch("relationships").
|
206
|
+
transform_keys(&:underscore).
|
207
|
+
map(&method(:as_relationship)).to_h.
|
208
|
+
transform_keys{|key| relation(key).as}
|
209
|
+
end
|
210
|
+
|
211
|
+
private def scope
|
212
|
+
@scope ||= adapter.find_many(@scope || model_class)
|
213
|
+
end
|
214
|
+
|
215
|
+
def object
|
216
|
+
@object ||= case intent
|
217
|
+
when :create
|
218
|
+
scope.new
|
219
|
+
when :show, :update, :destroy
|
220
|
+
adapter.find_one(scope, parameters.fetch("id"))
|
221
|
+
else
|
222
|
+
scope
|
76
223
|
end
|
224
|
+
end
|
77
225
|
|
78
|
-
|
79
|
-
|
226
|
+
def intent
|
227
|
+
@intent.to_sym
|
228
|
+
end
|
229
|
+
|
230
|
+
private def as_relationship(name, value)
|
231
|
+
data = value.fetch("data")
|
232
|
+
|
233
|
+
relation_configuration = relation(name).realizer_class.configuration
|
234
|
+
|
235
|
+
if data.is_a?(Array)
|
236
|
+
[name, relation_configuration.adapter.find_many(relation_configuration.model_class, {id: data.map {|value| value.fetch("id")}})]
|
237
|
+
else
|
238
|
+
[name, relation_configuration.adapter.find_one(relation_configuration.model_class, data.fetch("id"))]
|
80
239
|
end
|
240
|
+
end
|
81
241
|
|
82
|
-
|
83
|
-
|
242
|
+
private def attribute(name)
|
243
|
+
self.class.attribute(name)
|
244
|
+
end
|
245
|
+
|
246
|
+
private def relation(name)
|
247
|
+
self.class.relation(name)
|
248
|
+
end
|
249
|
+
|
250
|
+
private def adapter
|
251
|
+
self.class.configuration.adapter
|
252
|
+
end
|
253
|
+
|
254
|
+
private def model_class
|
255
|
+
self.class.configuration.model_class
|
256
|
+
end
|
257
|
+
|
258
|
+
def context
|
259
|
+
self.class.const_get("Context").new(**@context || {})
|
260
|
+
end
|
261
|
+
|
262
|
+
included do
|
263
|
+
class_eval(&MIXIN_HOOK) unless @abstract_class
|
264
|
+
end
|
265
|
+
|
266
|
+
class_methods do
|
267
|
+
def inherited(object)
|
268
|
+
object.class_eval(&MIXIN_HOOK) unless object.instance_variable_defined?(:@abstract_class)
|
84
269
|
end
|
85
270
|
|
86
|
-
def
|
87
|
-
|
271
|
+
def identifier(value)
|
272
|
+
@identifier ||= value.to_sym
|
88
273
|
end
|
89
274
|
|
90
|
-
def
|
91
|
-
|
275
|
+
def type(value, class_name:, adapter:)
|
276
|
+
@type ||= value.to_s
|
277
|
+
@model_class ||= class_name.constantize
|
278
|
+
@adapter ||= JSONAPI::Realizer::Adapter.new(interface: adapter)
|
92
279
|
end
|
93
280
|
|
94
|
-
def
|
95
|
-
|
281
|
+
def has(name, as: name)
|
282
|
+
@attributes[name] ||= Attribute.new(
|
283
|
+
:name => name,
|
284
|
+
:as => as,
|
285
|
+
:owner => self
|
286
|
+
)
|
96
287
|
end
|
97
288
|
|
98
|
-
def
|
99
|
-
|
289
|
+
def has_one(name, as: name, class_name:)
|
290
|
+
@relations[name] ||= Relation.new(
|
291
|
+
:owner => self,
|
292
|
+
:type => :one,
|
293
|
+
:name => name,
|
294
|
+
:as => as,
|
295
|
+
:realizer_class_name => class_name
|
296
|
+
)
|
100
297
|
end
|
101
298
|
|
102
|
-
def
|
103
|
-
|
299
|
+
def has_many(name, as: name, class_name:)
|
300
|
+
@relations[name] ||= Relation.new(
|
301
|
+
:owner => self,
|
302
|
+
:type => :many,
|
303
|
+
:name => name,
|
304
|
+
:as => as,
|
305
|
+
:realizer_class_name => class_name
|
306
|
+
)
|
104
307
|
end
|
105
308
|
|
106
|
-
def
|
107
|
-
|
309
|
+
def context
|
310
|
+
const_get("Context")
|
108
311
|
end
|
109
312
|
|
110
|
-
def
|
111
|
-
configuration.
|
313
|
+
def configuration
|
314
|
+
@configuration ||= Configuration.new({
|
315
|
+
:owner => self,
|
316
|
+
:type => @type,
|
317
|
+
:model_class => @model_class,
|
318
|
+
:adapter => @adapter,
|
319
|
+
:attributes => @attributes,
|
320
|
+
:relations => @relations
|
321
|
+
})
|
112
322
|
end
|
113
323
|
|
114
|
-
def
|
115
|
-
|
116
|
-
resource_class: self,
|
117
|
-
model_class: class_name.constantize,
|
118
|
-
adapter: JSONAPI::Realizer::Adapter.new(adapter),
|
119
|
-
type: type.to_s
|
120
|
-
)
|
324
|
+
def attribute(name)
|
325
|
+
configuration.attributes.fetch(name.to_sym){raise(Error::ResourceRelationshipNotFound, name: name, realizer: self)}
|
121
326
|
end
|
122
327
|
|
123
|
-
def
|
124
|
-
|
328
|
+
def relation(name)
|
329
|
+
configuration.relations.fetch(name.to_sym){raise(Error::ResourceRelationshipNotFound, name: name, realizer: self)}
|
125
330
|
end
|
126
331
|
end
|
127
332
|
end
|