jsonapi-resources 0.9.12 → 0.10.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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)