jsonapi-resources 0.10.6 → 0.11.0.beta2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +39 -2
  4. data/lib/generators/jsonapi/controller_generator.rb +2 -0
  5. data/lib/generators/jsonapi/resource_generator.rb +2 -0
  6. data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +3 -2
  7. data/lib/jsonapi/active_relation/join_manager.rb +30 -18
  8. data/lib/jsonapi/active_relation/join_manager_v10.rb +305 -0
  9. data/lib/jsonapi/active_relation_retrieval.rb +885 -0
  10. data/lib/jsonapi/active_relation_retrieval_v09.rb +715 -0
  11. data/lib/jsonapi/{active_relation_resource.rb → active_relation_retrieval_v10.rb} +113 -135
  12. data/lib/jsonapi/acts_as_resource_controller.rb +49 -49
  13. data/lib/jsonapi/cached_response_fragment.rb +4 -2
  14. data/lib/jsonapi/callbacks.rb +2 -0
  15. data/lib/jsonapi/compiled_json.rb +2 -0
  16. data/lib/jsonapi/configuration.rb +35 -15
  17. data/lib/jsonapi/error.rb +2 -0
  18. data/lib/jsonapi/error_codes.rb +2 -0
  19. data/lib/jsonapi/exceptions.rb +2 -0
  20. data/lib/jsonapi/formatter.rb +2 -0
  21. data/lib/jsonapi/include_directives.rb +77 -19
  22. data/lib/jsonapi/link_builder.rb +2 -0
  23. data/lib/jsonapi/mime_types.rb +6 -10
  24. data/lib/jsonapi/naive_cache.rb +2 -0
  25. data/lib/jsonapi/operation.rb +2 -0
  26. data/lib/jsonapi/operation_result.rb +2 -0
  27. data/lib/jsonapi/paginator.rb +2 -0
  28. data/lib/jsonapi/path.rb +2 -0
  29. data/lib/jsonapi/path_segment.rb +4 -2
  30. data/lib/jsonapi/processor.rb +95 -140
  31. data/lib/jsonapi/relationship.rb +89 -35
  32. data/lib/jsonapi/{request_parser.rb → request.rb} +157 -164
  33. data/lib/jsonapi/resource.rb +7 -2
  34. data/lib/jsonapi/{basic_resource.rb → resource_common.rb} +187 -88
  35. data/lib/jsonapi/resource_controller.rb +2 -0
  36. data/lib/jsonapi/resource_controller_metal.rb +2 -0
  37. data/lib/jsonapi/resource_fragment.rb +17 -15
  38. data/lib/jsonapi/resource_identity.rb +6 -0
  39. data/lib/jsonapi/resource_serializer.rb +20 -4
  40. data/lib/jsonapi/resource_set.rb +36 -16
  41. data/lib/jsonapi/resource_tree.rb +191 -0
  42. data/lib/jsonapi/resources/railtie.rb +3 -1
  43. data/lib/jsonapi/resources/version.rb +3 -1
  44. data/lib/jsonapi/response_document.rb +4 -2
  45. data/lib/jsonapi/routing_ext.rb +4 -2
  46. data/lib/jsonapi/simple_resource.rb +13 -0
  47. data/lib/jsonapi-resources.rb +10 -4
  48. data/lib/tasks/check_upgrade.rake +3 -1
  49. metadata +47 -15
  50. data/lib/jsonapi/resource_id_tree.rb +0 -112
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'csv'
2
4
 
3
5
  module JSONAPI
@@ -13,7 +15,7 @@ module JSONAPI
13
15
  :transaction
14
16
  end
15
17
 
16
- attr_reader :response_document
18
+ attr_reader :response_document, :jsonapi_request
17
19
 
18
20
  def index
19
21
  process_request
@@ -76,48 +78,54 @@ module JSONAPI
76
78
  end
77
79
 
78
80
  def process_request
79
- @response_document = create_response_document
80
-
81
- unless verify_content_type_header && verify_accept_header
82
- render_response_document
83
- return
81
+ begin
82
+ setup_response_document
83
+ verify_content_type_header
84
+ verify_accept_header
85
+ parse_request
86
+ execute_request
87
+ rescue => e
88
+ handle_exceptions(e)
84
89
  end
90
+ render_response_document
91
+ end
85
92
 
86
- request_parser = JSONAPI::RequestParser.new(
87
- params,
88
- context: context,
89
- key_formatter: key_formatter,
90
- server_error_callbacks: (self.class.server_error_callbacks || []))
91
-
92
- transactional = request_parser.transactional?
93
+ def setup_response_document
94
+ @response_document = create_response_document
95
+ end
93
96
 
94
- begin
95
- process_operations(transactional) do
96
- run_callbacks :process_operations do
97
- request_parser.each(response_document) do |op|
98
- op.options[:serializer] = resource_serializer_klass.new(
99
- op.resource_klass,
100
- include_directives: op.options[:include_directives],
101
- fields: op.options[:fields],
102
- base_url: base_url,
103
- key_formatter: key_formatter,
104
- route_formatter: route_formatter,
105
- serialization_options: serialization_options,
106
- controller: self
107
- )
108
- op.options[:cache_serializer_output] = !JSONAPI.configuration.resource_cache.nil?
109
-
110
- process_operation(op)
111
- end
112
- end
113
- if response_document.has_errors?
114
- raise ActiveRecord::Rollback
97
+ def parse_request
98
+ @jsonapi_request = JSONAPI::Request.new(
99
+ params,
100
+ context: context,
101
+ key_formatter: key_formatter,
102
+ server_error_callbacks: (self.class.server_error_callbacks || []))
103
+ fail JSONAPI::Exceptions::Errors.new(@jsonapi_request.errors) if @jsonapi_request.errors.any?
104
+ end
105
+
106
+ def execute_request
107
+ process_operations(jsonapi_request.transactional?) do
108
+ run_callbacks :process_operations do
109
+ jsonapi_request.operations.each do |op|
110
+ op.options[:serializer] = resource_serializer_klass.new(
111
+ op.resource_klass,
112
+ include_directives: op.options[:include_directives],
113
+ fields: op.options[:fields],
114
+ base_url: base_url,
115
+ key_formatter: key_formatter,
116
+ route_formatter: route_formatter,
117
+ serialization_options: serialization_options,
118
+ controller: self
119
+ )
120
+ op.options[:cache_serializer_output] = !JSONAPI.configuration.resource_cache.nil?
121
+
122
+ process_operation(op)
115
123
  end
116
124
  end
117
- rescue => e
118
- handle_exceptions(e)
125
+ if response_document.has_errors?
126
+ raise ActiveRecord::Rollback
127
+ end
119
128
  end
120
- render_response_document
121
129
  end
122
130
 
123
131
  def process_operations(transactional)
@@ -161,24 +169,16 @@ module JSONAPI
161
169
 
162
170
  def verify_content_type_header
163
171
  if ['create', 'create_relationship', 'update_relationship', 'update'].include?(params[:action])
164
- unless request.content_type == JSONAPI::MEDIA_TYPE
165
- fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
172
+ unless request.media_type == JSONAPI::MEDIA_TYPE
173
+ fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.media_type)
166
174
  end
167
175
  end
168
- true
169
- rescue => e
170
- handle_exceptions(e)
171
- false
172
176
  end
173
177
 
174
178
  def verify_accept_header
175
179
  unless valid_accept_media_type?
176
180
  fail JSONAPI::Exceptions::NotAcceptableError.new(request.accept)
177
181
  end
178
- true
179
- rescue => e
180
- handle_exceptions(e)
181
- false
182
182
  end
183
183
 
184
184
  def valid_accept_media_type?
@@ -273,7 +273,7 @@ module JSONAPI
273
273
  when ActionController::ParameterMissing
274
274
  errors = JSONAPI::Exceptions::ParameterMissing.new(e.param).errors
275
275
  else
276
- if JSONAPI.configuration.exception_class_whitelisted?(e)
276
+ if JSONAPI.configuration.exception_class_allowed?(e)
277
277
  raise e
278
278
  else
279
279
  if self.class.server_error_callbacks
@@ -308,7 +308,7 @@ module JSONAPI
308
308
  # caught that is not a JSONAPI::Exceptions::Error
309
309
  # Useful for additional logging or notification configuration that
310
310
  # would normally depend on rails catching and rendering an exception.
311
- # Ignores whitelist exceptions from config
311
+ # Ignores allowlist exceptions from config
312
312
 
313
313
  module ClassMethods
314
314
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class CachedResponseFragment
3
5
 
@@ -51,8 +53,8 @@ module JSONAPI
51
53
  @fetchable_fields = Set.new(fetchable_fields)
52
54
 
53
55
  # Relationships left uncompiled because we'll often want to insert included ids on retrieval
54
- # Remove the data since that should not be cached
55
- @relationships = relationships&.transform_values {|v| v.delete_if {|k, _v| k == 'data'} }
56
+ @relationships = relationships
57
+
56
58
  @links_json = CompiledJson.of(links_json)
57
59
  @attributes_json = CompiledJson.of(attributes_json)
58
60
  @meta_json = CompiledJson.of(meta_json)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/callbacks'
2
4
 
3
5
  module JSONAPI
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class CompiledJson
3
5
  def self.compile(h)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'jsonapi/formatter'
2
4
  require 'jsonapi/processor'
3
5
  require 'concurrent'
@@ -28,8 +30,8 @@ module JSONAPI
28
30
  :allow_transactions,
29
31
  :include_backtraces_in_errors,
30
32
  :include_application_backtraces_in_errors,
31
- :exception_class_whitelist,
32
- :whitelist_all_exceptions,
33
+ :exception_class_allowlist,
34
+ :allow_all_exceptions,
33
35
  :always_include_to_one_linkage_data,
34
36
  :always_include_to_many_linkage_data,
35
37
  :cache_formatters,
@@ -40,6 +42,7 @@ module JSONAPI
40
42
  :resource_cache_digest_function,
41
43
  :resource_cache_usage_report_function,
42
44
  :default_exclude_links,
45
+ :default_resource_retrieval_strategy,
43
46
  :use_related_resource_records_for_joins
44
47
 
45
48
  def initialize
@@ -96,12 +99,12 @@ module JSONAPI
96
99
  # raise a Pundit::NotAuthorizedError at some point during operations
97
100
  # processing. If you want to use Rails' `rescue_from` macro to
98
101
  # catch this error and render a 403 status code, you should add
99
- # the `Pundit::NotAuthorizedError` to the `exception_class_whitelist`.
100
- self.exception_class_whitelist = []
102
+ # the `Pundit::NotAuthorizedError` to the `exception_class_allowlist`.
103
+ self.exception_class_allowlist = []
101
104
 
102
- # If enabled, will override configuration option `exception_class_whitelist`
103
- # and whitelist all exceptions.
104
- self.whitelist_all_exceptions = false
105
+ # If enabled, will override configuration option `exception_class_allowlist`
106
+ # and allow all exceptions.
107
+ self.allow_all_exceptions = false
105
108
 
106
109
  # Resource Linkage
107
110
  # Controls the serialization of resource linkage for non compound documents
@@ -160,9 +163,24 @@ module JSONAPI
160
163
  # specific default links to exclude, which may be `:self` and `:related`.
161
164
  self.default_exclude_links = :none
162
165
 
163
- # Use a related resource's `records` when performing joins. This setting allows included resources to account for
164
- # permission scopes. It can be overridden explicitly per relationship. Furthermore, specifying a `relation_name`
165
- # on a relationship will cause this setting to be ignored.
166
+ # Global configuration for resource retrieval strategy used by the Resource class.
167
+ # Selecting a default_resource_retrieval_strategy will affect all resources that derive from
168
+ # Resource. The default value is 'JSONAPI::ActiveRelationRetrieval'.
169
+ #
170
+ # To use multiple retrieval strategies in an app set this to :none and set a custom retrieval strategy
171
+ # per resource (or base resource) using the class method `load_resource_retrieval_strategy`.
172
+ #
173
+ # Available strategies:
174
+ # 'JSONAPI::ActiveRelationRetrieval'
175
+ # 'JSONAPI::ActiveRelationRetrievalV09'
176
+ # 'JSONAPI::ActiveRelationRetrievalV10'
177
+ # :none
178
+ # :self
179
+ self.default_resource_retrieval_strategy = 'JSONAPI::ActiveRelationRetrieval'
180
+
181
+ # For 'JSONAPI::ActiveRelationRetrievalV10': use a related resource's `records` when performing joins.
182
+ # This setting allows included resources to account for permission scopes. It can be overridden explicitly per
183
+ # relationship. Furthermore, specifying a `relation_name` on a relationship will cause this setting to be ignored.
166
184
  self.use_related_resource_records_for_joins = true
167
185
  end
168
186
 
@@ -225,9 +243,9 @@ module JSONAPI
225
243
  return formatter
226
244
  end
227
245
 
228
- def exception_class_whitelisted?(e)
229
- @whitelist_all_exceptions ||
230
- @exception_class_whitelist.flatten.any? { |k| e.class.ancestors.map(&:to_s).include?(k.to_s) }
246
+ def exception_class_allowed?(e)
247
+ @allow_all_exceptions ||
248
+ @exception_class_allowlist.flatten.any? { |k| e.class.ancestors.map(&:to_s).include?(k.to_s) }
231
249
  end
232
250
 
233
251
  def default_processor_klass=(default_processor_klass)
@@ -276,9 +294,9 @@ module JSONAPI
276
294
 
277
295
  attr_writer :include_application_backtraces_in_errors
278
296
 
279
- attr_writer :exception_class_whitelist
297
+ attr_writer :exception_class_allowlist
280
298
 
281
- attr_writer :whitelist_all_exceptions
299
+ attr_writer :allow_all_exceptions
282
300
 
283
301
  attr_writer :always_include_to_one_linkage_data
284
302
 
@@ -306,6 +324,8 @@ module JSONAPI
306
324
 
307
325
  attr_writer :default_exclude_links
308
326
 
327
+ attr_writer :default_resource_retrieval_strategy
328
+
309
329
  attr_writer :use_related_resource_records_for_joins
310
330
  end
311
331
 
data/lib/jsonapi/error.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class Error
3
5
  attr_accessor :title, :detail, :id, :href, :code, :source, :links, :status, :meta
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  VALIDATION_ERROR = '100'
3
5
  INVALID_RESOURCE = '101'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Exceptions
3
5
  class Error < RuntimeError
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class Formatter
3
5
  class << self
@@ -1,24 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class IncludeDirectives
3
5
  # Construct an IncludeDirectives Hash from an array of dot separated include strings.
4
6
  # For example ['posts.comments.tags']
5
7
  # will transform into =>
6
8
  # {
7
- # posts: {
8
- # include_related: {
9
- # comments:{
10
- # include_related: {
11
- # tags: {
12
- # include_related: {}
13
- # }
9
+ # include_related: {
10
+ # posts: {
11
+ # include: true,
12
+ # include_related: {
13
+ # comments: {
14
+ # include: true,
15
+ # include_related: {
16
+ # tags: {
17
+ # include: true,
18
+ # include_related: {},
19
+ # include_in_join: true
20
+ # }
21
+ # },
22
+ # include_in_join: true
14
23
  # }
15
- # }
24
+ # },
25
+ # include_in_join: true
16
26
  # }
17
27
  # }
18
28
  # }
19
29
 
20
- def initialize(resource_klass, includes_array)
30
+ def initialize(resource_klass, includes_array, force_eager_load: false)
21
31
  @resource_klass = resource_klass
32
+ @force_eager_load = force_eager_load
22
33
  @include_directives_hash = { include_related: {} }
23
34
  includes_array.each do |include|
24
35
  parse_include(include)
@@ -29,21 +40,68 @@ module JSONAPI
29
40
  @include_directives_hash
30
41
  end
31
42
 
32
- private
43
+ def [](name)
44
+ @include_directives_hash[name]
45
+ end
33
46
 
34
- def parse_include(include)
35
- path = JSONAPI::Path.new(resource_klass: @resource_klass,
36
- path_string: include,
37
- ensure_default_field: false,
38
- parse_fields: false)
47
+ def model_includes
48
+ get_includes(@include_directives_hash)
49
+ end
39
50
 
51
+ private
52
+
53
+ def get_related(current_path)
40
54
  current = @include_directives_hash
55
+ current_resource_klass = @resource_klass
56
+ current_path.split('.').each do |fragment|
57
+ fragment = fragment.to_sym
58
+
59
+ if current_resource_klass
60
+ current_relationship = current_resource_klass._relationship(fragment)
61
+ current_resource_klass = current_relationship.try(:resource_klass)
62
+ else
63
+ raise JSONAPI::Exceptions::InvalidInclude.new(current_resource_klass, current_path)
64
+ end
65
+
66
+ include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include
67
+
68
+ current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
69
+ current = current[:include_related][fragment]
70
+ end
71
+ current
72
+ end
73
+
74
+ def get_includes(directive, only_joined_includes = true)
75
+ ir = directive[:include_related]
76
+ ir = ir.select { |_k,v| v[:include_in_join] } if only_joined_includes
77
+
78
+ ir.map do |name, sub_directive|
79
+ sub = get_includes(sub_directive, only_joined_includes)
80
+ sub.any? ? { name => sub } : name
81
+ end
82
+ end
83
+
84
+ def parse_include(include)
85
+ parts = include.split('.')
86
+ local_path = ''
41
87
 
42
- path.segments.each do |segment|
43
- relationship_name = segment.relationship.name.to_sym
88
+ parts.each do |name|
89
+ local_path += local_path.length > 0 ? ".#{name}" : name
90
+ related = get_related(local_path)
91
+ related[:include] = true
92
+ end
93
+ end
44
94
 
45
- current[:include_related][relationship_name] ||= { include_related: {} }
46
- current = current[:include_related][relationship_name]
95
+ def delve_paths(obj)
96
+ case obj
97
+ when Array
98
+ obj.map{|elem| delve_paths(elem)}.flatten(1)
99
+ when Hash
100
+ obj.map{|k,v| [[k]] + delve_paths(v).map{|path| [k] + path } }.flatten(1)
101
+ when Symbol, String
102
+ [[obj]]
103
+ else
104
+ raise "delve_paths cannot descend into #{obj.class.name}"
47
105
  end
48
106
 
49
107
  rescue JSONAPI::Exceptions::InvalidRelationship => _e
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class LinkBuilder
3
5
  attr_reader :base_url,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
 
3
5
  module JSONAPI
@@ -7,16 +9,10 @@ module JSONAPI
7
9
  def self.install
8
10
  Mime::Type.register JSONAPI::MEDIA_TYPE, :api_json
9
11
 
10
- # :nocov:
11
- if Rails::VERSION::MAJOR >= 5
12
- parsers = ActionDispatch::Request.parameter_parsers.merge(
13
- Mime::Type.lookup(JSONAPI::MEDIA_TYPE).symbol => parser
14
- )
15
- ActionDispatch::Request.parameter_parsers = parsers
16
- else
17
- ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime::Type.lookup(JSONAPI::MEDIA_TYPE)] = parser
18
- end
19
- # :nocov:
12
+ parsers = ActionDispatch::Request.parameter_parsers.merge(
13
+ Mime::Type.lookup(JSONAPI::MEDIA_TYPE).symbol => parser
14
+ )
15
+ ActionDispatch::Request.parameter_parsers = parsers
20
16
  end
21
17
 
22
18
  def self.parser
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
 
3
5
  # Cache which memoizes the given block.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class Operation
3
5
  attr_reader :resource_klass, :operation_type, :options
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class OperationResult
3
5
  attr_accessor :code
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class Paginator
3
5
  def initialize(_params)
data/lib/jsonapi/path.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class Path
3
5
  attr_reader :segments, :resource_klass
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class PathSegment
3
5
  def self.parse(source_resource_klass:, segment_string:, parse_fields: true)
@@ -30,7 +32,7 @@ module JSONAPI
30
32
  end
31
33
 
32
34
  def eql?(other)
33
- other.is_a?(JSONAPI::PathSegment::Relationship) && relationship == other.relationship && resource_klass == other.resource_klass
35
+ other.is_a?(self.class) && relationship == other.relationship && resource_klass == other.resource_klass
34
36
  end
35
37
 
36
38
  def hash
@@ -59,7 +61,7 @@ module JSONAPI
59
61
  end
60
62
 
61
63
  def eql?(other)
62
- other.is_a?(JSONAPI::PathSegment::Field) && field_name == other.field_name && resource_klass == other.resource_klass
64
+ other.is_a?(self.class) && field_name == other.field_name && resource_klass == other.resource_klass
63
65
  end
64
66
 
65
67
  def delegated_field_name