jsonapionify 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +29 -0
- data/.csslintrc +2 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +1171 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +34 -0
- data/TODO +13 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/config.ru +15 -0
- data/fixtures/documentation.json +364 -0
- data/jsonapionify.gemspec +50 -0
- data/lib/core_ext/boolean.rb +3 -0
- data/lib/jsonapionify/api/action.rb +211 -0
- data/lib/jsonapionify/api/attribute.rb +67 -0
- data/lib/jsonapionify/api/base/app_builder.rb +33 -0
- data/lib/jsonapionify/api/base/class_methods.rb +73 -0
- data/lib/jsonapionify/api/base/delegation.rb +15 -0
- data/lib/jsonapionify/api/base/doc_helper.rb +47 -0
- data/lib/jsonapionify/api/base/reloader.rb +10 -0
- data/lib/jsonapionify/api/base/resource_definitions.rb +39 -0
- data/lib/jsonapionify/api/base.rb +25 -0
- data/lib/jsonapionify/api/context.rb +14 -0
- data/lib/jsonapionify/api/context_delegate.rb +42 -0
- data/lib/jsonapionify/api/errors.rb +6 -0
- data/lib/jsonapionify/api/errors_object.rb +66 -0
- data/lib/jsonapionify/api/header_options.rb +13 -0
- data/lib/jsonapionify/api/param_options.rb +46 -0
- data/lib/jsonapionify/api/relationship/blocks.rb +41 -0
- data/lib/jsonapionify/api/relationship/many.rb +61 -0
- data/lib/jsonapionify/api/relationship/one.rb +36 -0
- data/lib/jsonapionify/api/relationship.rb +89 -0
- data/lib/jsonapionify/api/resource/builders.rb +81 -0
- data/lib/jsonapionify/api/resource/class_methods.rb +82 -0
- data/lib/jsonapionify/api/resource/defaults/actions.rb +11 -0
- data/lib/jsonapionify/api/resource/defaults/errors.rb +99 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +96 -0
- data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +31 -0
- data/lib/jsonapionify/api/resource/defaults.rb +10 -0
- data/lib/jsonapionify/api/resource/definitions/actions.rb +196 -0
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +51 -0
- data/lib/jsonapionify/api/resource/definitions/contexts.rb +16 -0
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +9 -0
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +79 -0
- data/lib/jsonapionify/api/resource/definitions/params.rb +49 -0
- data/lib/jsonapionify/api/resource/definitions/relationships.rb +42 -0
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +103 -0
- data/lib/jsonapionify/api/resource/definitions/response_headers.rb +22 -0
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +50 -0
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +85 -0
- data/lib/jsonapionify/api/resource/definitions.rb +14 -0
- data/lib/jsonapionify/api/resource/error_handling.rb +108 -0
- data/lib/jsonapionify/api/resource/http.rb +11 -0
- data/lib/jsonapionify/api/resource/includer.rb +4 -0
- data/lib/jsonapionify/api/resource.rb +35 -0
- data/lib/jsonapionify/api/response.rb +47 -0
- data/lib/jsonapionify/api/server/mock_response.rb +37 -0
- data/lib/jsonapionify/api/server/request.rb +78 -0
- data/lib/jsonapionify/api/server.rb +50 -0
- data/lib/jsonapionify/api/test_helper.rb +52 -0
- data/lib/jsonapionify/api.rb +9 -0
- data/lib/jsonapionify/autoload.rb +52 -0
- data/lib/jsonapionify/callbacks.rb +49 -0
- data/lib/jsonapionify/character_range.rb +41 -0
- data/lib/jsonapionify/continuation.rb +26 -0
- data/lib/jsonapionify/documentation/template.erb +487 -0
- data/lib/jsonapionify/documentation.rb +40 -0
- data/lib/jsonapionify/enumerable_observer.rb +91 -0
- data/lib/jsonapionify/indented_string.rb +27 -0
- data/lib/jsonapionify/inherited_attributes.rb +125 -0
- data/lib/jsonapionify/structure/collections/base.rb +104 -0
- data/lib/jsonapionify/structure/collections/errors.rb +7 -0
- data/lib/jsonapionify/structure/collections/included_resources.rb +39 -0
- data/lib/jsonapionify/structure/collections/resource_identifiers.rb +7 -0
- data/lib/jsonapionify/structure/collections/resources.rb +7 -0
- data/lib/jsonapionify/structure/helpers/errors.rb +71 -0
- data/lib/jsonapionify/structure/helpers/inherits_origin.rb +17 -0
- data/lib/jsonapionify/structure/helpers/member_names.rb +37 -0
- data/lib/jsonapionify/structure/helpers/meta_delegate.rb +16 -0
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +123 -0
- data/lib/jsonapionify/structure/helpers/object_setters.rb +21 -0
- data/lib/jsonapionify/structure/helpers/pagination_links.rb +10 -0
- data/lib/jsonapionify/structure/helpers/validations.rb +296 -0
- data/lib/jsonapionify/structure/maps/base.rb +25 -0
- data/lib/jsonapionify/structure/maps/error_links.rb +7 -0
- data/lib/jsonapionify/structure/maps/links.rb +21 -0
- data/lib/jsonapionify/structure/maps/relationship_links.rb +11 -0
- data/lib/jsonapionify/structure/maps/relationships.rb +23 -0
- data/lib/jsonapionify/structure/maps/resource_links.rb +7 -0
- data/lib/jsonapionify/structure/maps/top_level_links.rb +10 -0
- data/lib/jsonapionify/structure/objects/attributes.rb +29 -0
- data/lib/jsonapionify/structure/objects/base.rb +166 -0
- data/lib/jsonapionify/structure/objects/error.rb +16 -0
- data/lib/jsonapionify/structure/objects/included_resource.rb +14 -0
- data/lib/jsonapionify/structure/objects/jsonapi.rb +7 -0
- data/lib/jsonapionify/structure/objects/link.rb +18 -0
- data/lib/jsonapionify/structure/objects/meta.rb +7 -0
- data/lib/jsonapionify/structure/objects/relationship.rb +20 -0
- data/lib/jsonapionify/structure/objects/resource.rb +45 -0
- data/lib/jsonapionify/structure/objects/resource_identifier.rb +40 -0
- data/lib/jsonapionify/structure/objects/source.rb +10 -0
- data/lib/jsonapionify/structure/objects/top_level.rb +105 -0
- data/lib/jsonapionify/structure.rb +27 -0
- data/lib/jsonapionify/types/array_type.rb +32 -0
- data/lib/jsonapionify/types/boolean_type.rb +22 -0
- data/lib/jsonapionify/types/date_string_type.rb +28 -0
- data/lib/jsonapionify/types/float_type.rb +8 -0
- data/lib/jsonapionify/types/integer_type.rb +9 -0
- data/lib/jsonapionify/types/object_type.rb +22 -0
- data/lib/jsonapionify/types/string_type.rb +66 -0
- data/lib/jsonapionify/types/time_string_type.rb +28 -0
- data/lib/jsonapionify/types.rb +49 -0
- data/lib/jsonapionify/unstrict_proc.rb +28 -0
- data/lib/jsonapionify/version.rb +3 -0
- data/lib/jsonapionify.rb +37 -0
- metadata +530 -0
@@ -0,0 +1,296 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
|
3
|
+
module JSONAPIonify::Structure
|
4
|
+
module Helpers
|
5
|
+
module Validations
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
using JSONAPIonify::UnstrictProc
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# Raise the validation errors
|
12
|
+
def validation_error(message)
|
13
|
+
message = "#{name}: #{message}"
|
14
|
+
ValidationError.new(message)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Validations
|
18
|
+
|
19
|
+
# Fails if this key doesn't exist and the `given` does.
|
20
|
+
def may_not_exist!(key, without: nil, **options)
|
21
|
+
return may_not_exist_without! key, without if without
|
22
|
+
before_validation do
|
23
|
+
JSONAPIonify::Continuation.new(**options).check(self) do
|
24
|
+
invalid_keys = self.keys.map(&:to_sym) & keys.map(&:to_sym)
|
25
|
+
if invalid_keys.present?
|
26
|
+
invalid_keys.each do |k|
|
27
|
+
errors.add(k, 'is not permitted')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def may_not_exist_without!(key, without, **options)
|
35
|
+
before_validation do
|
36
|
+
JSONAPIonify::Continuation.new(**options).check(self) do
|
37
|
+
if has_key?(key) && !has_key?(without)
|
38
|
+
errors.add(key, "may not exist without: `#{without}`")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Warn but do not fail if keys present
|
45
|
+
def should_not_contain!(*keys, **options, &block)
|
46
|
+
before_validation do
|
47
|
+
JSONAPIonify::Continuation.new(**options).check(self) do
|
48
|
+
keys += self.keys.select(&block) if block_given?
|
49
|
+
invalid_keys = self.keys.map(&:to_sym) & keys.map(&:to_sym)
|
50
|
+
if invalid_keys.present?
|
51
|
+
invalid_keys.each do |key|
|
52
|
+
warnings.add(key, 'is not permitted')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Fails if these keys exist
|
60
|
+
def must_not_contain!(*keys, deep: false, **options, &block)
|
61
|
+
return must_not_contain_deep!(*keys, **options) if deep === true
|
62
|
+
before_validation do
|
63
|
+
JSONAPIonify::Continuation.new(**options).check(self) do
|
64
|
+
keys += self.keys.select(&block) if block_given?
|
65
|
+
invalid_keys = self.keys.map(&:to_sym) & keys.map(&:to_sym)
|
66
|
+
if invalid_keys.present?
|
67
|
+
invalid_keys.each do |key|
|
68
|
+
errors.add(key, 'is not permitted')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Fails if these keys exist deeply
|
76
|
+
def must_not_contain_deep!(*keys, **options, &block)
|
77
|
+
before_validation do
|
78
|
+
JSONAPIonify::Continuation.new(**options).check(self) do
|
79
|
+
all_invalid_keys = []
|
80
|
+
keys += self.keys.select(&block) if block_given?
|
81
|
+
is_invalid = proc do |hash|
|
82
|
+
invalid_keys = hash.keys.map(&:to_sym) & keys.map(&:to_sym)
|
83
|
+
all_invalid_keys += invalid_keys
|
84
|
+
children = hash.values.select { |v| v.respond_to?(:to_hash) }
|
85
|
+
invalid_keys.present? | children.map { |c| is_invalid.call c }.reduce(:|)
|
86
|
+
end
|
87
|
+
if is_invalid.call(self)
|
88
|
+
errors.add('*', "cannot contain keys #{keys_to_sentence(*all_invalid_keys.uniq)}")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Fails if one of these keys does not exist
|
95
|
+
def must_contain_one_of!(*keys, **options, &block)
|
96
|
+
self.permitted_keys = [*permitted_keys, *keys].uniq
|
97
|
+
before_validation do
|
98
|
+
JSONAPIonify::Continuation.new(**options).check(self) do
|
99
|
+
keys += self.keys.select(&block) if block_given?
|
100
|
+
valid_keys = keys.map(&:to_sym) & self.keys.map(&:to_sym)
|
101
|
+
unless valid_keys.present?
|
102
|
+
errors.add('*', "must contain one of: #{keys_to_sentence(*valid_keys)}")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Fails if these keys dont exist
|
109
|
+
def must_contain!(*keys, **options)
|
110
|
+
self.permitted_keys = [*self.permitted_keys, *keys]
|
111
|
+
keys.each do |key|
|
112
|
+
required_keys[key] = options
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Fails if keys other than these exist
|
117
|
+
def may_contain!(*keys)
|
118
|
+
self.allow_only_permitted = true
|
119
|
+
self.permitted_keys = [*permitted_keys, *keys].uniq
|
120
|
+
end
|
121
|
+
|
122
|
+
# Fails is more than one of the keys exists.
|
123
|
+
def must_not_coexist!(*keys, **options)
|
124
|
+
before_validation do
|
125
|
+
JSONAPIonify::Continuation.new(**options).check(self) do
|
126
|
+
keys = expand_keys(*keys)
|
127
|
+
conflicting_keys = keys & self.keys
|
128
|
+
if conflicting_keys.length > 1
|
129
|
+
conflicting_keys.each do |key|
|
130
|
+
conflicts_with = conflicting_keys - [key]
|
131
|
+
errors.add key, "conflicts with #{keys_to_sentence(*conflicts_with)}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Validates key using a provided method or block
|
139
|
+
def validate!(key, with: nil, message: 'is not valid', **options, &block)
|
140
|
+
before_validation do
|
141
|
+
if has_key? key
|
142
|
+
JSONAPIonify::Continuation.new(**options).check(self, key, self[key]) do
|
143
|
+
real_block = get_block_from_options(with, &block)
|
144
|
+
errors.add key, message unless real_block.call(self, key, self[key])
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def validate_object!(with: nil, message: 'is not valid', **options, &block)
|
151
|
+
before_validation do
|
152
|
+
JSONAPIonify::Continuation.new(**options).check(self) do
|
153
|
+
real_block = get_block_from_options(with, &block)
|
154
|
+
errors.add '*', message unless real_block.call(self)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Validates the object using a provided method or block
|
160
|
+
def validate_each!(with: nil, message: 'not valid', **options, &block)
|
161
|
+
before_validation do
|
162
|
+
real_block = get_block_from_options(with, &block)
|
163
|
+
keys.each do |key|
|
164
|
+
JSONAPIonify::Continuation.new(**options).check(self, key, self[key]) do
|
165
|
+
errors.add key, message unless real_block.call(self, key, self[key])
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Validates key is type
|
172
|
+
def type_of!(key, must_be:, allow_nil: false, **options)
|
173
|
+
allowed_type_map[key] ||= {}
|
174
|
+
types = Array.wrap(must_be)
|
175
|
+
types << NilClass if allow_nil
|
176
|
+
allowed_type_map[key][options] ||= []
|
177
|
+
allowed_type_map[key][options] += types
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Included Stuff
|
182
|
+
|
183
|
+
included do
|
184
|
+
extend JSONAPIonify::InheritedAttributes
|
185
|
+
class_attribute :allow_only_permitted, instance_writer: false
|
186
|
+
delegate :validation_error, to: :class
|
187
|
+
inherited_hash_attribute :allowed_type_map, :required_keys, instance_accessor: false
|
188
|
+
inherited_array_attribute :permitted_keys, instance_accessor: false
|
189
|
+
self.allow_only_permitted = false
|
190
|
+
self.permitted_keys = []
|
191
|
+
self.allowed_type_map = {}
|
192
|
+
self.required_keys = {}
|
193
|
+
|
194
|
+
# Check Permitted Keys
|
195
|
+
before_validation do
|
196
|
+
self.keys.each do |key|
|
197
|
+
errors.add(key, "is not permitted") unless permitted_key? key
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Check Permitted Types
|
202
|
+
before_validation do
|
203
|
+
keys.each do |key|
|
204
|
+
next unless permitted_key?(key)
|
205
|
+
unless permitted_type_for?(key)
|
206
|
+
types = permitted_types_for(key).map(&:name)
|
207
|
+
message = "must be a #{keys_to_sentence(*types)}."
|
208
|
+
errors.add(key, message)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Check Required Keys
|
214
|
+
before_validation do
|
215
|
+
required_keys.each do |key|
|
216
|
+
unless has_key? key
|
217
|
+
errors.add key, 'must be provided'
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
validate_each!(message: 'is not a valid member name') do |_, key, _|
|
223
|
+
Helpers::MemberNames.valid? key
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
# Instance Methods
|
229
|
+
|
230
|
+
def permitted_key?(key)
|
231
|
+
!allow_only_permitted || permitted_keys.map(&:to_sym).include?(key.to_sym)
|
232
|
+
end
|
233
|
+
|
234
|
+
def required_key?(key)
|
235
|
+
required_keys.include? key
|
236
|
+
end
|
237
|
+
|
238
|
+
def required_keys
|
239
|
+
self.class.required_keys.select do |_, options|
|
240
|
+
JSONAPIonify::Continuation.new(**options).check(self) { true }
|
241
|
+
end.keys
|
242
|
+
end
|
243
|
+
|
244
|
+
def permitted_type_for?(key)
|
245
|
+
types = permitted_types_for(key)
|
246
|
+
types.empty? || permitted_types_for(key).any? { |type| self[key].is_a? type }
|
247
|
+
end
|
248
|
+
|
249
|
+
def permitted_types_for(key)
|
250
|
+
options_and_types = allowed_type_map[key] || {}
|
251
|
+
options_and_types.each_with_object([]) do |(options, types), permitted_types|
|
252
|
+
JSONAPIonify::Continuation.new(**options).check(self) do
|
253
|
+
permitted_types.concat types
|
254
|
+
end
|
255
|
+
end.uniq
|
256
|
+
end
|
257
|
+
|
258
|
+
def permitted_keys
|
259
|
+
expand_keys(*self.class.permitted_keys)
|
260
|
+
end
|
261
|
+
|
262
|
+
def allowed_type_map
|
263
|
+
self.class.allowed_type_map.dup.tap do |hash|
|
264
|
+
wildcard_types = hash.delete('*')
|
265
|
+
keys.each do |k|
|
266
|
+
hash[k] ||= {}
|
267
|
+
hash[k].deep_merge! wildcard_types
|
268
|
+
end if wildcard_types.present?
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
private
|
273
|
+
|
274
|
+
def expand_keys(*keys)
|
275
|
+
keys.flat_map do |key|
|
276
|
+
key.is_a?(Proc) ? keys.select(&key) : key
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def get_block_from_options(symbol, &block)
|
281
|
+
raise ArgumentError, 'cannot pass symbol and block' if symbol && block
|
282
|
+
raise ArgumentError, 'must pass symbol or block' unless symbol || block
|
283
|
+
(block || method(symbol).to_proc).unstrict
|
284
|
+
end
|
285
|
+
|
286
|
+
def keys_to_sentence(*keys, connector: "or")
|
287
|
+
keys = keys.map { |key| backtick_key key }
|
288
|
+
keys.to_sentence(last_word_connector: ", #{connector} ", two_words_connector: " #{connector} ")
|
289
|
+
end
|
290
|
+
|
291
|
+
def backtick_key(key)
|
292
|
+
"`#{key}`"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Maps
|
3
|
+
class Base < Objects::Base
|
4
|
+
|
5
|
+
def self.value_is(type_class, strict: false)
|
6
|
+
define_method(:type_class) do
|
7
|
+
type_class
|
8
|
+
end
|
9
|
+
type! must_be: type_class if strict
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.type!(**opts)
|
13
|
+
type_of! '*', **opts
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def coerce_value(_, v)
|
19
|
+
return v unless v.is_a? Hash
|
20
|
+
type_class.new v
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Maps
|
3
|
+
class Links < Base
|
4
|
+
|
5
|
+
value_is Objects::Link
|
6
|
+
|
7
|
+
validate_each! message: 'must be url string or valid link object' do |obj, key, value|
|
8
|
+
case value
|
9
|
+
when String
|
10
|
+
uri = URI.parse(value)
|
11
|
+
uri.scheme.present?
|
12
|
+
when Objects::Link
|
13
|
+
true
|
14
|
+
else
|
15
|
+
false
|
16
|
+
end || !obj.permitted_key?(key)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Maps
|
3
|
+
class RelationshipLinks < Links
|
4
|
+
# The RelationshipLinksObject **MAY** contain the following members:
|
5
|
+
must_contain_one_of! :self, # the link that generated the current response document.
|
6
|
+
:related # a related resource link when the primary data represents a resource relationship.
|
7
|
+
|
8
|
+
may_contain! *Helpers::PaginationLinks
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Maps
|
3
|
+
class Relationships < Base
|
4
|
+
|
5
|
+
# The value of the relationships key MUST be an object (a "relationships object"). Members of the relationships object ("relationships") represent references from the resource object in which it's defined to other resource objects.
|
6
|
+
# Relationships may be to-one or to-many.
|
7
|
+
|
8
|
+
# A relationship object that represents a to-many relationship MAY also contain pagination links under the links member, as described below.
|
9
|
+
# A resource object's AttributesObject and its RelationshipsObject are collectively called its fields.
|
10
|
+
# Fields for a ResourceObject **MUST** share a common namespace with each
|
11
|
+
# other and with `type` and `id`. In other words, a resource can not have an
|
12
|
+
# attribute and relationship with the same name, nor can it have an attribute
|
13
|
+
# or relationship named `type` or `id`.
|
14
|
+
must_not_contain! :type, :id
|
15
|
+
validate_each! message: 'conflicts with a resource_key' do |obj, key, _|
|
16
|
+
!obj.parent || !obj.parent.attribute_keys.include?(key)
|
17
|
+
end
|
18
|
+
|
19
|
+
value_is Objects::Relationship, strict: true
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Maps
|
3
|
+
class TopLevelLinks < Links
|
4
|
+
# The TopLevelLinksObject **MAY** contain the following members:
|
5
|
+
may_contain! :self, # the link that generated the current response document.
|
6
|
+
:related, # a related resource link when the primary data represents a resource relationship.
|
7
|
+
*Helpers::PaginationLinks # pagination links for the primary data.
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Objects
|
3
|
+
class Attributes < Base
|
4
|
+
|
5
|
+
# Attributes may contain any valid JSON value.
|
6
|
+
# Complex data structures involving JSON objects and arrays are allowed as
|
7
|
+
# attribute values. However, any object that constitutes or is contained in an
|
8
|
+
# attribute **MUST NOT** contain a `relationships` or `links` member, as those
|
9
|
+
# members are reserved by this specification for future use.
|
10
|
+
must_not_contain! :relationships, :links, deep: true
|
11
|
+
|
12
|
+
# A resource object's AttributesObject and its RelationshipsObject are collectively called its fields.
|
13
|
+
# Fields for a ResourceObject **MUST** share a common namespace with each
|
14
|
+
# other and with `type` and `id`. In other words, a resource can not have an
|
15
|
+
# attribute and relationship with the same name, nor can it have an attribute
|
16
|
+
# or relationship named `type` or `id`.
|
17
|
+
must_not_contain! :type, :id
|
18
|
+
validate_each! message: 'conflicts with a relationship key' do |obj, key, _|
|
19
|
+
!obj.parent || !obj.parent.relationship_keys.include?(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Although has-one foreign keys (e.g. `author_id`) are often stored internally
|
23
|
+
# alongside other information to be represented in a resource object, these keys
|
24
|
+
# **SHOULD NOT** appear as attributes.
|
25
|
+
should_not_contain! { |key| key.to_s.end_with? '_id' }
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'oj'
|
2
|
+
require 'active_support/core_ext/module/delegation'
|
3
|
+
require 'active_support/core_ext/array/wrap'
|
4
|
+
require 'active_support/core_ext/hash'
|
5
|
+
require 'active_support/core_ext/array/conversions'
|
6
|
+
require 'active_support/core_ext/hash/keys'
|
7
|
+
|
8
|
+
module JSONAPIonify::Structure
|
9
|
+
module Objects
|
10
|
+
class Base
|
11
|
+
|
12
|
+
include JSONAPIonify::Callbacks
|
13
|
+
include Enumerable
|
14
|
+
include JSONAPIonify::EnumerableObserver
|
15
|
+
include Helpers::InheritsOrigin
|
16
|
+
|
17
|
+
define_callbacks :initialize, :validation
|
18
|
+
|
19
|
+
include Helpers::ObjectSetters
|
20
|
+
include Helpers::Validations
|
21
|
+
include Helpers::ObjectDefaults
|
22
|
+
|
23
|
+
# Attributes
|
24
|
+
attr_reader :object, :parent
|
25
|
+
|
26
|
+
delegate :fetch, :select, :has_key?, :keys, :values, :each, :present?, :blank?, :empty?, to: :object
|
27
|
+
delegate :cache_store, to: JSONAPIonify
|
28
|
+
|
29
|
+
before_initialize do
|
30
|
+
@object = {}
|
31
|
+
observe(@object).added do |items|
|
32
|
+
items.each do |_, value|
|
33
|
+
value.instance_variable_set(:@parent, self) unless value.frozen?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.from_hash(hash)
|
39
|
+
new hash.deep_symbolize_keys
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.from_json(json)
|
43
|
+
from_hash Oj.load json
|
44
|
+
end
|
45
|
+
|
46
|
+
# Initialize the object
|
47
|
+
def initialize(**attributes)
|
48
|
+
run_callbacks :initialize do
|
49
|
+
attributes.each do |k, v|
|
50
|
+
self[k] = v
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def copy
|
56
|
+
self.class.from_hash to_hash
|
57
|
+
end
|
58
|
+
|
59
|
+
def ==(other)
|
60
|
+
return unless other.respond_to? :[]
|
61
|
+
object.all? do |k, v|
|
62
|
+
other[k] == v
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def ===(other)
|
67
|
+
other.class == self.class && self == other
|
68
|
+
end
|
69
|
+
|
70
|
+
# Compile as json
|
71
|
+
attr_reader :errors, :warnings
|
72
|
+
|
73
|
+
def compile
|
74
|
+
validate
|
75
|
+
to_hash
|
76
|
+
end
|
77
|
+
|
78
|
+
def as_json
|
79
|
+
compile.deep_stringify_keys
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_json
|
83
|
+
Oj.dump(as_json)
|
84
|
+
end
|
85
|
+
|
86
|
+
def signature
|
87
|
+
"#{self.class.name}:#{Digest::SHA2.hexdigest to_hash.to_s}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_hash
|
91
|
+
object.reduce({}) do |hash, (k, v)|
|
92
|
+
hash[k] =
|
93
|
+
case v
|
94
|
+
when Objects::Base
|
95
|
+
v.to_hash
|
96
|
+
when Hash
|
97
|
+
v.deep_stringify_keys
|
98
|
+
when Collections::Base
|
99
|
+
v.collect_hashes
|
100
|
+
else
|
101
|
+
v
|
102
|
+
end
|
103
|
+
hash
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def compile!(*args)
|
108
|
+
compile(*args).tap do
|
109
|
+
if (wrns = warnings).present?
|
110
|
+
warn validation_error wrns.all_messages.to_sentence + '.'
|
111
|
+
end
|
112
|
+
if (errs = errors).present?
|
113
|
+
raise validation_error errs.all_messages.to_sentence + '.'
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def validate
|
119
|
+
object.values.each { |val| val.validate if val.respond_to? :validate }
|
120
|
+
[errors, warnings].each(&:clear)
|
121
|
+
@errors, @warnings =
|
122
|
+
cache_store.fetch(signature) do
|
123
|
+
run_callbacks :validation do
|
124
|
+
collect_child_errors
|
125
|
+
collect_child_warnings
|
126
|
+
end
|
127
|
+
[errors, warnings]
|
128
|
+
end
|
129
|
+
errors.blank?
|
130
|
+
end
|
131
|
+
|
132
|
+
def errors
|
133
|
+
@errors ||= Helpers::Errors.new
|
134
|
+
end
|
135
|
+
|
136
|
+
def warnings
|
137
|
+
@warnings ||= Helpers::Errors.new
|
138
|
+
end
|
139
|
+
|
140
|
+
def pretty_json
|
141
|
+
JSON.pretty_generate as_json
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def collect_child_errors
|
147
|
+
object.each do |key, value|
|
148
|
+
next unless value.respond_to? :errors
|
149
|
+
value.errors.each do |error_key, messages|
|
150
|
+
errors.replace [key, error_key].join('/'), messages
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def collect_child_warnings
|
156
|
+
object.each do |key, value|
|
157
|
+
next unless value.respond_to? :warnings
|
158
|
+
value.warnings.each do |warning_key, messages|
|
159
|
+
warnings.replace [key, warning_key].join('/'), messages
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Objects
|
3
|
+
class Error < Base
|
4
|
+
may_contain! :id, :links, :status, :code, :title, :detail, :source, :meta
|
5
|
+
|
6
|
+
implements :links, as: Maps::ErrorLinks
|
7
|
+
implements :source, as: Source
|
8
|
+
implements :meta, as: Meta
|
9
|
+
|
10
|
+
type_of! :id, must_be: String
|
11
|
+
type_of! :status, must_be: String
|
12
|
+
type_of! :code, must_be: String
|
13
|
+
type_of! :title, must_be: String
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Objects
|
3
|
+
# ResourceObjects appear in a JSON API document to represent resources.
|
4
|
+
class IncludedResource < Resource
|
5
|
+
validate_object! with: :referenced?, message: "included resource is not referenced"
|
6
|
+
|
7
|
+
def referenced?
|
8
|
+
return true unless parent
|
9
|
+
parent.referenced.include? self
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Objects
|
3
|
+
class Link < Base
|
4
|
+
|
5
|
+
may_contain! :href, :meta
|
6
|
+
|
7
|
+
validate! :href, message: 'must be a valid URL' do |*, value|
|
8
|
+
if value.is_a?(String)
|
9
|
+
uri = URI.parse(value)
|
10
|
+
uri.scheme.present?
|
11
|
+
else
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module JSONAPIonify::Structure
|
2
|
+
module Objects
|
3
|
+
class Relationship < Base
|
4
|
+
# A "relationship object" MUST contain at least one of the following:
|
5
|
+
must_contain_one_of! :links, # A links object.
|
6
|
+
:data, # Resource linkage.
|
7
|
+
:meta # A meta object that contains non-standard meta-information about the relationship.
|
8
|
+
|
9
|
+
implements :links, as: Maps::Links
|
10
|
+
implements :meta, as: Meta
|
11
|
+
|
12
|
+
collects_or_implements(
|
13
|
+
:data,
|
14
|
+
collects: Collections::ResourceIdentifiers,
|
15
|
+
implements: ResourceIdentifier,
|
16
|
+
allow_nil: true
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|