jsonapi-resources 0.9.12 → 0.10.0.beta1

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.
Files changed (36) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1 -1
  3. data/README.md +34 -11
  4. data/lib/bug_report_templates/rails_5_latest.rb +125 -0
  5. data/lib/bug_report_templates/rails_5_master.rb +140 -0
  6. data/lib/jsonapi-resources.rb +8 -3
  7. data/lib/jsonapi/active_relation_resource_finder.rb +640 -0
  8. data/lib/jsonapi/active_relation_resource_finder/join_tree.rb +126 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +121 -106
  10. data/lib/jsonapi/{cached_resource_fragment.rb → cached_response_fragment.rb} +13 -30
  11. data/lib/jsonapi/compiled_json.rb +11 -1
  12. data/lib/jsonapi/configuration.rb +44 -18
  13. data/lib/jsonapi/error.rb +27 -0
  14. data/lib/jsonapi/exceptions.rb +43 -40
  15. data/lib/jsonapi/formatter.rb +3 -3
  16. data/lib/jsonapi/include_directives.rb +2 -45
  17. data/lib/jsonapi/link_builder.rb +87 -80
  18. data/lib/jsonapi/operation.rb +16 -5
  19. data/lib/jsonapi/operation_result.rb +74 -16
  20. data/lib/jsonapi/processor.rb +233 -112
  21. data/lib/jsonapi/relationship.rb +77 -53
  22. data/lib/jsonapi/request_parser.rb +378 -423
  23. data/lib/jsonapi/resource.rb +224 -524
  24. data/lib/jsonapi/resource_controller_metal.rb +2 -2
  25. data/lib/jsonapi/resource_fragment.rb +47 -0
  26. data/lib/jsonapi/resource_id_tree.rb +112 -0
  27. data/lib/jsonapi/resource_identity.rb +42 -0
  28. data/lib/jsonapi/resource_serializer.rb +133 -301
  29. data/lib/jsonapi/resource_set.rb +108 -0
  30. data/lib/jsonapi/resources/version.rb +1 -1
  31. data/lib/jsonapi/response_document.rb +100 -88
  32. data/lib/jsonapi/routing_ext.rb +21 -43
  33. metadata +29 -45
  34. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  35. data/lib/jsonapi/operation_results.rb +0 -35
  36. data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -0,0 +1,108 @@
1
+ module JSONAPI
2
+ # Contains a hash of resource types which contain a hash of resources, relationships and primary status keyed by
3
+ # resource id.
4
+ class ResourceSet
5
+
6
+ attr_reader :resource_klasses, :populated
7
+
8
+ def initialize(resource_id_tree)
9
+ @populated = false
10
+ @resource_klasses = flatten_resource_id_tree(resource_id_tree)
11
+ end
12
+
13
+ def populate!(serializer, context, find_options)
14
+ @resource_klasses.each_key do |resource_klass|
15
+ missed_ids = []
16
+
17
+ serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
18
+ context_json = resource_klass.attribute_caching_context(context).to_json
19
+ context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
20
+ context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
21
+
22
+ if resource_klass.caching?
23
+ cache_ids = []
24
+
25
+ @resource_klasses[resource_klass].each_pair do |k, v|
26
+ # Store the hashcode of the cache_field to avoid storing objects and to ensure precision isn't lost
27
+ # on timestamp types (i.e. string conversions dropping milliseconds)
28
+ cache_ids.push([k, resource_klass.hash_cache_field(v[:cache_id])])
29
+ end
30
+
31
+ found_resources = CachedResponseFragment.fetch_cached_fragments(
32
+ resource_klass,
33
+ serializer_config_key,
34
+ cache_ids,
35
+ context)
36
+
37
+ found_resources.each do |found_result|
38
+ resource = found_result[1]
39
+ if resource.nil?
40
+ missed_ids.push(found_result[0])
41
+ else
42
+ @resource_klasses[resource_klass][resource.id][:resource] = resource
43
+ end
44
+ end
45
+ else
46
+ missed_ids = @resource_klasses[resource_klass].keys
47
+ end
48
+
49
+ # fill in any missed resources
50
+ unless missed_ids.empty?
51
+ filters = {resource_klass._primary_key => missed_ids}
52
+ find_opts = {
53
+ context: context,
54
+ fields: find_options[:fields] }
55
+
56
+ found_resources = resource_klass.find(filters, find_opts)
57
+
58
+ found_resources.each do |resource|
59
+ relationship_data = @resource_klasses[resource_klass][resource.id][:relationships]
60
+
61
+ if resource_klass.caching?
62
+ (id, cr) = CachedResponseFragment.write(
63
+ resource_klass,
64
+ resource,
65
+ serializer,
66
+ serializer_config_key,
67
+ context,
68
+ context_key,
69
+ relationship_data)
70
+
71
+ @resource_klasses[resource_klass][id][:resource] = cr
72
+ else
73
+ @resource_klasses[resource_klass][resource.id][:resource] = resource
74
+ end
75
+ end
76
+ end
77
+ end
78
+ @populated = true
79
+ self
80
+ end
81
+
82
+ private
83
+ def flatten_resource_id_tree(resource_id_tree, flattened_tree = {})
84
+ resource_id_tree.fragments.each_pair do |resource_rid, fragment|
85
+
86
+ resource_klass = resource_rid.resource_klass
87
+ id = resource_rid.id
88
+
89
+ flattened_tree[resource_klass] ||= {}
90
+
91
+ flattened_tree[resource_klass][id] ||= { primary: fragment.primary, relationships: {} }
92
+ flattened_tree[resource_klass][id][:cache_id] ||= fragment.cache
93
+
94
+ fragment.related.try(:each_pair) do |relationship_name, related_rids|
95
+ flattened_tree[resource_klass][id][:relationships][relationship_name] ||= Set.new
96
+ flattened_tree[resource_klass][id][:relationships][relationship_name].merge(related_rids)
97
+ end
98
+ end
99
+
100
+ related_resource_id_trees = resource_id_tree.related_resource_id_trees
101
+ related_resource_id_trees.try(:each_value) do |related_resource_id_tree|
102
+ flatten_resource_id_tree(related_resource_id_tree, flattened_tree)
103
+ end
104
+
105
+ flattened_tree
106
+ end
107
+ end
108
+ end
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = '0.9.12'
3
+ VERSION = '0.10.0.beta1'
4
4
  end
5
5
  end
@@ -1,86 +1,133 @@
1
1
  module JSONAPI
2
2
  class ResponseDocument
3
- def initialize(operation_results, serializer, options = {})
4
- @operation_results = operation_results
5
- @serializer = serializer
3
+ attr_reader :serialized_results
4
+
5
+ def initialize(options = {})
6
+ @serialized_results = []
7
+ @result_codes = []
8
+ @error_results = []
9
+ @global_errors = []
10
+
6
11
  @options = options
7
12
 
13
+ @top_level_meta = @options.fetch(:base_meta, {})
14
+ @top_level_links = @options.fetch(:base_links, {})
15
+
8
16
  @key_formatter = @options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
9
17
  end
10
18
 
11
- def contents
12
- hash = results_to_hash
19
+ def has_errors?
20
+ @error_results.length > 0 || @global_errors.length > 0
21
+ end
13
22
 
14
- meta = top_level_meta
15
- hash.merge!(meta: meta) unless meta.empty?
23
+ def add_result(result, operation)
24
+ if result.is_a?(JSONAPI::ErrorsOperationResult)
25
+ # Clear any serialized results
26
+ @serialized_results = []
16
27
 
17
- links = top_level_links
18
- hash.merge!(links: links) unless links.empty?
28
+ # In JSONAPI v1 we only have one operation so all errors can be kept together
29
+ result.errors.each do |error|
30
+ add_global_error(error)
31
+ end
32
+ else
33
+ @serialized_results.push result.to_hash(operation.options[:serializer])
34
+ @result_codes.push result.code.to_i
35
+ update_links(operation.options[:serializer], result)
36
+ update_meta(result)
37
+ end
38
+ end
19
39
 
20
- hash
40
+ def add_global_error(error)
41
+ @global_errors.push error
21
42
  end
22
43
 
23
- def status
24
- if @operation_results.has_errors?
25
- @operation_results.all_errors[0].status
44
+ def contents
45
+ if has_errors?
46
+ return { 'errors' => @global_errors }
26
47
  else
27
- @operation_results.results[0].code
48
+ hash = @serialized_results[0]
49
+ meta = top_level_meta
50
+ hash.merge!('meta' => meta) unless meta.empty?
51
+
52
+ links = top_level_links
53
+ hash.merge!('links' => links) unless links.empty?
54
+
55
+ return hash
28
56
  end
29
57
  end
30
58
 
31
- private
59
+ def status
60
+ status_codes = if has_errors?
61
+ @global_errors.collect do |error|
62
+ error.status.to_i
63
+ end
64
+ else
65
+ @result_codes
66
+ end
67
+
68
+ # Count the unique status codes
69
+ counts = status_codes.each_with_object(Hash.new(0)) { |code, counts| counts[code] += 1 }
70
+
71
+ # if there is only one status code we can return that
72
+ return counts.keys[0].to_i if counts.length == 1
73
+
74
+ # :nocov: not currently used
75
+
76
+ # if there are many we should return the highest general code, 200, 400, 500 etc.
77
+ max_status = 0
78
+ status_codes.each do |status|
79
+ code = status.to_i
80
+ max_status = code if max_status < code
81
+ end
82
+ return (max_status / 100).floor * 100
83
+ # :nocov:
84
+ end
32
85
 
33
- # Rolls up the top level meta data from the base_meta, the set of operations,
34
- # and the result of each operation. The keys are then formatted.
35
- def top_level_meta
36
- meta = @options.fetch(:base_meta, {})
86
+ private
37
87
 
38
- meta.merge!(@operation_results.meta)
88
+ def update_meta(result)
89
+ @top_level_meta.merge!(result.meta)
39
90
 
40
- @operation_results.results.each do |result|
41
- meta.merge!(result.meta)
91
+ if JSONAPI.configuration.top_level_meta_include_record_count && result.respond_to?(:record_count)
92
+ @top_level_meta[JSONAPI.configuration.top_level_meta_record_count_key] = result.record_count
93
+ end
42
94
 
43
- if JSONAPI.configuration.top_level_meta_include_record_count && result.respond_to?(:record_count)
44
- meta[JSONAPI.configuration.top_level_meta_record_count_key] = result.record_count
45
- end
95
+ if JSONAPI.configuration.top_level_meta_include_page_count && result.respond_to?(:page_count)
96
+ @top_level_meta[JSONAPI.configuration.top_level_meta_page_count_key] = result.page_count
97
+ end
46
98
 
47
- if JSONAPI.configuration.top_level_meta_include_page_count && result.respond_to?(:page_count)
48
- meta[JSONAPI.configuration.top_level_meta_page_count_key] = result.page_count
99
+ if result.warnings.any?
100
+ @top_level_meta[:warnings] = result.warnings.collect do |warning|
101
+ warning.to_hash
49
102
  end
50
103
  end
104
+ end
51
105
 
52
- meta.as_json.deep_transform_keys { |key| @key_formatter.format(key) }
106
+ def top_level_meta
107
+ @top_level_meta.as_json.deep_transform_keys { |key| @key_formatter.format(key) }
53
108
  end
54
109
 
55
- # Rolls up the top level links from the base_links, the set of operations,
56
- # and the result of each operation. The keys are then formatted.
57
- def top_level_links
58
- links = @options.fetch(:base_links, {})
59
-
60
- links.merge!(@operation_results.links)
61
-
62
- @operation_results.results.each do |result|
63
- links.merge!(result.links)
64
-
65
- # Build pagination links
66
- if result.is_a?(JSONAPI::ResourcesOperationResult) || result.is_a?(JSONAPI::RelatedResourcesOperationResult)
67
- result.pagination_params.each_pair do |link_name, params|
68
- if result.is_a?(JSONAPI::RelatedResourcesOperationResult)
69
- relationship = result.source_resource.class._relationships[result._type.to_sym]
70
- unless relationship.exclude_link?(link_name)
71
- link = @serializer.link_builder.relationships_related_link(result.source_resource, relationship, query_params(params))
72
- end
73
- else
74
- unless @serializer.link_builder.primary_resource_klass.exclude_link?(link_name)
75
- link = @serializer.link_builder.query_link(query_params(params))
76
- end
77
- end
78
- links[link_name] = link unless link.blank?
110
+ def update_links(serializer, result)
111
+ @top_level_links.merge!(result.links)
112
+
113
+ # Build pagination links
114
+ if result.is_a?(JSONAPI::ResourceSetOperationResult) ||
115
+ result.is_a?(JSONAPI::ResourcesSetOperationResult) ||
116
+ result.is_a?(JSONAPI::RelatedResourcesSetOperationResult)
117
+
118
+ result.pagination_params.each_pair do |link_name, params|
119
+ if result.is_a?(JSONAPI::RelatedResourcesSetOperationResult)
120
+ relationship = result.source_resource.class._relationships[result._type.to_sym]
121
+ @top_level_links[link_name] = serializer.link_builder.relationships_related_link(result.source_resource, relationship, query_params(params))
122
+ else
123
+ @top_level_links[link_name] = serializer.query_link(query_params(params))
79
124
  end
80
125
  end
81
126
  end
127
+ end
82
128
 
83
- links.deep_transform_keys { |key| @key_formatter.format(key) }
129
+ def top_level_links
130
+ @top_level_links.deep_transform_keys { |key| @key_formatter.format(key) }
84
131
  end
85
132
 
86
133
  def query_params(params)
@@ -101,40 +148,5 @@ module JSONAPI
101
148
 
102
149
  query_params
103
150
  end
104
-
105
- def results_to_hash
106
- if @operation_results.has_errors?
107
- { errors: @operation_results.all_errors }
108
- else
109
- if @operation_results.results.length == 1
110
- result = @operation_results.results[0]
111
-
112
- case result
113
- when JSONAPI::ResourceOperationResult
114
- @serializer.serialize_to_hash(result.resource)
115
- when JSONAPI::ResourcesOperationResult
116
- @serializer.serialize_to_hash(result.resources)
117
- when JSONAPI::RelationshipOperationResult
118
- @serializer.serialize_to_relationship_hash(result.parent_resource,
119
- result.relationship)
120
- when JSONAPI::OperationResult
121
- {}
122
- end
123
-
124
- elsif @operation_results.results.length > 1
125
- resources = []
126
- @operation_results.results.each do |result|
127
- case result
128
- when JSONAPI::ResourceOperationResult
129
- resources.push(result.resource)
130
- when JSONAPI::ResourcesOperationResult
131
- resources.concat(result.resources)
132
- end
133
- end
134
-
135
- @serializer.serialize_to_hash(resources)
136
- end
137
- end
138
- end
139
151
  end
140
152
  end
@@ -18,13 +18,7 @@ module ActionDispatch
18
18
 
19
19
  def jsonapi_resource(*resources, &_block)
20
20
  @resource_type = resources.first
21
- res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(@resource_type))
22
-
23
- res._routed = true
24
-
25
- unless res.singleton?
26
- warn "Singleton routes created for non singleton resource #{res}. Links may not be generated correctly."
27
- end
21
+ res = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(@resource_type))
28
22
 
29
23
  options = resources.extract_options!.dup
30
24
  options[:controller] ||= @resource_type
@@ -39,8 +33,8 @@ module ActionDispatch
39
33
  end
40
34
 
41
35
  if res._immutable
42
- options[:except] << :create unless options[:except].include?(:create) || options[:except].include?('create')
43
- options[:except] << :update unless options[:except].include?(:update) || options[:except].include?('update')
36
+ options[:except] << :create unless options[:except].include?(:create) || options[:except].include?('create')
37
+ options[:except] << :update unless options[:except].include?(:update) || options[:except].include?('update')
44
38
  options[:except] << :destroy unless options[:except].include?(:destroy) || options[:except].include?('destroy')
45
39
  end
46
40
 
@@ -70,7 +64,7 @@ module ActionDispatch
70
64
  end
71
65
 
72
66
  def jsonapi_relationships(options = {})
73
- res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(@resource_type))
67
+ res = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(@resource_type))
74
68
  res._relationships.each do |relationship_name, relationship|
75
69
  if relationship.is_a?(JSONAPI::Relationship::ToMany)
76
70
  jsonapi_links(relationship_name, options)
@@ -84,13 +78,7 @@ module ActionDispatch
84
78
 
85
79
  def jsonapi_resources(*resources, &_block)
86
80
  @resource_type = resources.first
87
- res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(@resource_type))
88
-
89
- res._routed = true
90
-
91
- if res.singleton?
92
- warn "Singleton resource #{res} should use `jsonapi_resource` instead."
93
- end
81
+ res = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(@resource_type))
94
82
 
95
83
  options = resources.extract_options!.dup
96
84
  options[:controller] ||= @resource_type
@@ -114,8 +102,8 @@ module ActionDispatch
114
102
  end
115
103
 
116
104
  if res._immutable
117
- options[:except] << :create unless options[:except].include?(:create) || options[:except].include?('create')
118
- options[:except] << :update unless options[:except].include?(:update) || options[:except].include?('update')
105
+ options[:except] << :create unless options[:except].include?(:create) || options[:except].include?('create')
106
+ options[:except] << :update unless options[:except].include?(:update) || options[:except].include?('update')
119
107
  options[:except] << :destroy unless options[:except].include?(:destroy) || options[:except].include?('destroy')
120
108
  end
121
109
 
@@ -159,28 +147,24 @@ module ActionDispatch
159
147
  formatted_relationship_name = format_route(link_type)
160
148
  options = links.extract_options!.dup
161
149
 
162
- res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix)
150
+ res = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix)
163
151
  options[:controller] ||= res._type.to_s
164
152
 
165
153
  methods = links_methods(options)
166
154
 
167
155
  if methods.include?(:show)
168
- match "relationships/#{formatted_relationship_name}",
169
- controller: options[:controller],
170
- action: 'show_relationship', relationship: link_type.to_s, via: [:get],
171
- as: "relationships/#{link_type}"
156
+ match "relationships/#{formatted_relationship_name}", controller: options[:controller],
157
+ action: 'show_relationship', relationship: link_type.to_s, via: [:get]
172
158
  end
173
159
 
174
160
  if res.mutable?
175
161
  if methods.include?(:update)
176
- match "relationships/#{formatted_relationship_name}",
177
- controller: options[:controller],
162
+ match "relationships/#{formatted_relationship_name}", controller: options[:controller],
178
163
  action: 'update_relationship', relationship: link_type.to_s, via: [:put, :patch]
179
164
  end
180
165
 
181
166
  if methods.include?(:destroy)
182
- match "relationships/#{formatted_relationship_name}",
183
- controller: options[:controller],
167
+ match "relationships/#{formatted_relationship_name}", controller: options[:controller],
184
168
  action: 'destroy_relationship', relationship: link_type.to_s, via: [:delete]
185
169
  end
186
170
  end
@@ -191,15 +175,14 @@ module ActionDispatch
191
175
  formatted_relationship_name = format_route(link_type)
192
176
  options = links.extract_options!.dup
193
177
 
194
- res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix)
178
+ res = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix)
195
179
  options[:controller] ||= res._type.to_s
196
180
 
197
181
  methods = links_methods(options)
198
182
 
199
183
  if methods.include?(:show)
200
184
  match "relationships/#{formatted_relationship_name}", controller: options[:controller],
201
- action: 'show_relationship', relationship: link_type.to_s, via: [:get],
202
- as: "relationships/#{link_type}"
185
+ action: 'show_relationship', relationship: link_type.to_s, via: [:get]
203
186
  end
204
187
 
205
188
  if res.mutable?
@@ -221,47 +204,41 @@ module ActionDispatch
221
204
  end
222
205
 
223
206
  def jsonapi_related_resource(*relationship)
224
- source = JSONAPI::Resource.resource_for(resource_type_with_module_prefix)
207
+ source = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix)
225
208
  options = relationship.extract_options!.dup
226
209
 
227
210
  relationship_name = relationship.first
228
211
  relationship = source._relationships[relationship_name]
229
212
 
230
- relationship._routed = true
231
-
232
213
  formatted_relationship_name = format_route(relationship.name)
233
214
 
234
215
  if relationship.polymorphic?
235
216
  options[:controller] ||= relationship.class_name.underscore.pluralize
236
217
  else
237
- related_resource = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(relationship.class_name.underscore.pluralize))
218
+ related_resource = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(relationship.class_name.underscore.pluralize))
238
219
  options[:controller] ||= related_resource._type.to_s
239
220
  end
240
221
 
241
222
  match formatted_relationship_name, controller: options[:controller],
242
223
  relationship: relationship.name, source: resource_type_with_module_prefix(source._type),
243
- action: 'get_related_resource', via: [:get],
244
- as: "related/#{relationship_name}"
224
+ action: 'show_related_resource', via: [:get]
245
225
  end
246
226
 
247
227
  def jsonapi_related_resources(*relationship)
248
- source = JSONAPI::Resource.resource_for(resource_type_with_module_prefix)
228
+ source = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix)
249
229
  options = relationship.extract_options!.dup
250
230
 
251
231
  relationship_name = relationship.first
252
232
  relationship = source._relationships[relationship_name]
253
233
 
254
- relationship._routed = true
255
-
256
234
  formatted_relationship_name = format_route(relationship.name)
257
- related_resource = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(relationship.class_name.underscore))
235
+ related_resource = JSONAPI::Resource.resource_klass_for(resource_type_with_module_prefix(relationship.class_name.underscore))
258
236
  options[:controller] ||= related_resource._type.to_s
259
237
 
260
238
  match formatted_relationship_name,
261
239
  controller: options[:controller],
262
240
  relationship: relationship.name, source: resource_type_with_module_prefix(source._type),
263
- action: 'get_related_resources', via: [:get],
264
- as: "related/#{relationship_name}"
241
+ action: 'index_related_resources', via: [:get]
265
242
  end
266
243
 
267
244
  protected
@@ -274,6 +251,7 @@ module ActionDispatch
274
251
  @scope = @scope.parent
275
252
  end
276
253
  # :nocov:
254
+
277
255
  private
278
256
 
279
257
  def resource_type_with_module_prefix(resource = nil)