jsonapi-resources 0.10.6 → 0.11.0.beta2

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