jsonapionify 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +29 -0
  3. data/.csslintrc +2 -0
  4. data/.gitignore +11 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +1171 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +10 -0
  9. data/CODE_OF_CONDUCT.md +13 -0
  10. data/Gemfile +4 -0
  11. data/Guardfile +14 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +43 -0
  14. data/Rakefile +34 -0
  15. data/TODO +13 -0
  16. data/bin/console +14 -0
  17. data/bin/setup +7 -0
  18. data/config.ru +15 -0
  19. data/fixtures/documentation.json +364 -0
  20. data/jsonapionify.gemspec +50 -0
  21. data/lib/core_ext/boolean.rb +3 -0
  22. data/lib/jsonapionify/api/action.rb +211 -0
  23. data/lib/jsonapionify/api/attribute.rb +67 -0
  24. data/lib/jsonapionify/api/base/app_builder.rb +33 -0
  25. data/lib/jsonapionify/api/base/class_methods.rb +73 -0
  26. data/lib/jsonapionify/api/base/delegation.rb +15 -0
  27. data/lib/jsonapionify/api/base/doc_helper.rb +47 -0
  28. data/lib/jsonapionify/api/base/reloader.rb +10 -0
  29. data/lib/jsonapionify/api/base/resource_definitions.rb +39 -0
  30. data/lib/jsonapionify/api/base.rb +25 -0
  31. data/lib/jsonapionify/api/context.rb +14 -0
  32. data/lib/jsonapionify/api/context_delegate.rb +42 -0
  33. data/lib/jsonapionify/api/errors.rb +6 -0
  34. data/lib/jsonapionify/api/errors_object.rb +66 -0
  35. data/lib/jsonapionify/api/header_options.rb +13 -0
  36. data/lib/jsonapionify/api/param_options.rb +46 -0
  37. data/lib/jsonapionify/api/relationship/blocks.rb +41 -0
  38. data/lib/jsonapionify/api/relationship/many.rb +61 -0
  39. data/lib/jsonapionify/api/relationship/one.rb +36 -0
  40. data/lib/jsonapionify/api/relationship.rb +89 -0
  41. data/lib/jsonapionify/api/resource/builders.rb +81 -0
  42. data/lib/jsonapionify/api/resource/class_methods.rb +82 -0
  43. data/lib/jsonapionify/api/resource/defaults/actions.rb +11 -0
  44. data/lib/jsonapionify/api/resource/defaults/errors.rb +99 -0
  45. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +96 -0
  46. data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +31 -0
  47. data/lib/jsonapionify/api/resource/defaults.rb +10 -0
  48. data/lib/jsonapionify/api/resource/definitions/actions.rb +196 -0
  49. data/lib/jsonapionify/api/resource/definitions/attributes.rb +51 -0
  50. data/lib/jsonapionify/api/resource/definitions/contexts.rb +16 -0
  51. data/lib/jsonapionify/api/resource/definitions/helpers.rb +9 -0
  52. data/lib/jsonapionify/api/resource/definitions/pagination.rb +79 -0
  53. data/lib/jsonapionify/api/resource/definitions/params.rb +49 -0
  54. data/lib/jsonapionify/api/resource/definitions/relationships.rb +42 -0
  55. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +103 -0
  56. data/lib/jsonapionify/api/resource/definitions/response_headers.rb +22 -0
  57. data/lib/jsonapionify/api/resource/definitions/scopes.rb +50 -0
  58. data/lib/jsonapionify/api/resource/definitions/sorting.rb +85 -0
  59. data/lib/jsonapionify/api/resource/definitions.rb +14 -0
  60. data/lib/jsonapionify/api/resource/error_handling.rb +108 -0
  61. data/lib/jsonapionify/api/resource/http.rb +11 -0
  62. data/lib/jsonapionify/api/resource/includer.rb +4 -0
  63. data/lib/jsonapionify/api/resource.rb +35 -0
  64. data/lib/jsonapionify/api/response.rb +47 -0
  65. data/lib/jsonapionify/api/server/mock_response.rb +37 -0
  66. data/lib/jsonapionify/api/server/request.rb +78 -0
  67. data/lib/jsonapionify/api/server.rb +50 -0
  68. data/lib/jsonapionify/api/test_helper.rb +52 -0
  69. data/lib/jsonapionify/api.rb +9 -0
  70. data/lib/jsonapionify/autoload.rb +52 -0
  71. data/lib/jsonapionify/callbacks.rb +49 -0
  72. data/lib/jsonapionify/character_range.rb +41 -0
  73. data/lib/jsonapionify/continuation.rb +26 -0
  74. data/lib/jsonapionify/documentation/template.erb +487 -0
  75. data/lib/jsonapionify/documentation.rb +40 -0
  76. data/lib/jsonapionify/enumerable_observer.rb +91 -0
  77. data/lib/jsonapionify/indented_string.rb +27 -0
  78. data/lib/jsonapionify/inherited_attributes.rb +125 -0
  79. data/lib/jsonapionify/structure/collections/base.rb +104 -0
  80. data/lib/jsonapionify/structure/collections/errors.rb +7 -0
  81. data/lib/jsonapionify/structure/collections/included_resources.rb +39 -0
  82. data/lib/jsonapionify/structure/collections/resource_identifiers.rb +7 -0
  83. data/lib/jsonapionify/structure/collections/resources.rb +7 -0
  84. data/lib/jsonapionify/structure/helpers/errors.rb +71 -0
  85. data/lib/jsonapionify/structure/helpers/inherits_origin.rb +17 -0
  86. data/lib/jsonapionify/structure/helpers/member_names.rb +37 -0
  87. data/lib/jsonapionify/structure/helpers/meta_delegate.rb +16 -0
  88. data/lib/jsonapionify/structure/helpers/object_defaults.rb +123 -0
  89. data/lib/jsonapionify/structure/helpers/object_setters.rb +21 -0
  90. data/lib/jsonapionify/structure/helpers/pagination_links.rb +10 -0
  91. data/lib/jsonapionify/structure/helpers/validations.rb +296 -0
  92. data/lib/jsonapionify/structure/maps/base.rb +25 -0
  93. data/lib/jsonapionify/structure/maps/error_links.rb +7 -0
  94. data/lib/jsonapionify/structure/maps/links.rb +21 -0
  95. data/lib/jsonapionify/structure/maps/relationship_links.rb +11 -0
  96. data/lib/jsonapionify/structure/maps/relationships.rb +23 -0
  97. data/lib/jsonapionify/structure/maps/resource_links.rb +7 -0
  98. data/lib/jsonapionify/structure/maps/top_level_links.rb +10 -0
  99. data/lib/jsonapionify/structure/objects/attributes.rb +29 -0
  100. data/lib/jsonapionify/structure/objects/base.rb +166 -0
  101. data/lib/jsonapionify/structure/objects/error.rb +16 -0
  102. data/lib/jsonapionify/structure/objects/included_resource.rb +14 -0
  103. data/lib/jsonapionify/structure/objects/jsonapi.rb +7 -0
  104. data/lib/jsonapionify/structure/objects/link.rb +18 -0
  105. data/lib/jsonapionify/structure/objects/meta.rb +7 -0
  106. data/lib/jsonapionify/structure/objects/relationship.rb +20 -0
  107. data/lib/jsonapionify/structure/objects/resource.rb +45 -0
  108. data/lib/jsonapionify/structure/objects/resource_identifier.rb +40 -0
  109. data/lib/jsonapionify/structure/objects/source.rb +10 -0
  110. data/lib/jsonapionify/structure/objects/top_level.rb +105 -0
  111. data/lib/jsonapionify/structure.rb +27 -0
  112. data/lib/jsonapionify/types/array_type.rb +32 -0
  113. data/lib/jsonapionify/types/boolean_type.rb +22 -0
  114. data/lib/jsonapionify/types/date_string_type.rb +28 -0
  115. data/lib/jsonapionify/types/float_type.rb +8 -0
  116. data/lib/jsonapionify/types/integer_type.rb +9 -0
  117. data/lib/jsonapionify/types/object_type.rb +22 -0
  118. data/lib/jsonapionify/types/string_type.rb +66 -0
  119. data/lib/jsonapionify/types/time_string_type.rb +28 -0
  120. data/lib/jsonapionify/types.rb +49 -0
  121. data/lib/jsonapionify/unstrict_proc.rb +28 -0
  122. data/lib/jsonapionify/version.rb +3 -0
  123. data/lib/jsonapionify.rb +37 -0
  124. 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,7 @@
1
+ module JSONAPIonify::Structure
2
+ module Maps
3
+ class ErrorLinks < Links
4
+ may_contain! :about
5
+ end
6
+ end
7
+ 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,7 @@
1
+ module JSONAPIonify::Structure
2
+ module Maps
3
+ class ResourceLinks < Links
4
+ may_contain! :self
5
+ end
6
+ end
7
+ 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,7 @@
1
+ module JSONAPIonify::Structure
2
+ module Objects
3
+ class Jsonapi < Base
4
+ may_contain! :version, :meta
5
+ end
6
+ end
7
+ 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,7 @@
1
+ module JSONAPIonify::Structure
2
+ module Objects
3
+ class Meta < Base
4
+
5
+ end
6
+ end
7
+ 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