sanger-jsonapi-resources 0.1.1 → 0.2.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +35 -12
  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/active_relation/adapters/join_left_active_record_adapter.rb +26 -0
  7. data/lib/jsonapi/active_relation/join_manager.rb +297 -0
  8. data/lib/jsonapi/active_relation_resource.rb +898 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +130 -113
  10. data/lib/jsonapi/basic_resource.rb +1164 -0
  11. data/lib/jsonapi/cached_response_fragment.rb +129 -0
  12. data/lib/jsonapi/callbacks.rb +2 -0
  13. data/lib/jsonapi/compatibility_helper.rb +29 -0
  14. data/lib/jsonapi/compiled_json.rb +13 -1
  15. data/lib/jsonapi/configuration.rb +88 -21
  16. data/lib/jsonapi/error.rb +29 -0
  17. data/lib/jsonapi/error_codes.rb +4 -0
  18. data/lib/jsonapi/exceptions.rb +82 -50
  19. data/lib/jsonapi/formatter.rb +5 -3
  20. data/lib/jsonapi/include_directives.rb +22 -67
  21. data/lib/jsonapi/link_builder.rb +76 -80
  22. data/lib/jsonapi/mime_types.rb +6 -10
  23. data/lib/jsonapi/naive_cache.rb +2 -0
  24. data/lib/jsonapi/operation.rb +18 -5
  25. data/lib/jsonapi/operation_result.rb +76 -16
  26. data/lib/jsonapi/paginator.rb +2 -0
  27. data/lib/jsonapi/path.rb +45 -0
  28. data/lib/jsonapi/path_segment.rb +78 -0
  29. data/lib/jsonapi/processor.rb +193 -115
  30. data/lib/jsonapi/relationship.rb +145 -14
  31. data/lib/jsonapi/request.rb +734 -0
  32. data/lib/jsonapi/resource.rb +3 -1251
  33. data/lib/jsonapi/resource_controller.rb +2 -0
  34. data/lib/jsonapi/resource_controller_metal.rb +7 -1
  35. data/lib/jsonapi/resource_fragment.rb +56 -0
  36. data/lib/jsonapi/resource_identity.rb +44 -0
  37. data/lib/jsonapi/resource_serializer.rb +158 -284
  38. data/lib/jsonapi/resource_set.rb +196 -0
  39. data/lib/jsonapi/resource_tree.rb +236 -0
  40. data/lib/jsonapi/resources/railtie.rb +9 -0
  41. data/lib/jsonapi/resources/version.rb +1 -1
  42. data/lib/jsonapi/response_document.rb +107 -83
  43. data/lib/jsonapi/routing_ext.rb +50 -26
  44. data/lib/jsonapi-resources.rb +23 -5
  45. data/lib/tasks/check_upgrade.rake +52 -0
  46. metadata +43 -31
  47. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  48. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  49. data/lib/jsonapi/operation_results.rb +0 -35
  50. data/lib/jsonapi/relationship_builder.rb +0 -167
  51. data/lib/jsonapi/request_parser.rb +0 -678
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ class CachedResponseFragment
5
+
6
+ Lookup = Struct.new(:resource_klass, :serializer_config_key, :context, :context_key, :cache_ids) do
7
+
8
+ def type
9
+ resource_klass._type
10
+ end
11
+
12
+ def keys
13
+ cache_ids.map do |(id, cache_key)|
14
+ [type, id, cache_key, serializer_config_key, context_key]
15
+ end
16
+ end
17
+ end
18
+
19
+ Write = Struct.new(:resource_klass, :resource, :serializer, :serializer_config_key, :context, :context_key, :relationship_data) do
20
+ def to_key_value
21
+
22
+ (id, cache_key) = resource.cache_id
23
+
24
+ json = serializer.object_hash(resource, relationship_data)
25
+
26
+ cr = CachedResponseFragment.new(
27
+ resource_klass,
28
+ id,
29
+ json['type'],
30
+ context,
31
+ resource.fetchable_fields,
32
+ json['relationships'],
33
+ json['links'],
34
+ json['attributes'],
35
+ json['meta']
36
+ )
37
+
38
+ key = [resource_klass._type, id, cache_key, serializer_config_key, context_key]
39
+
40
+ [key, cr]
41
+ end
42
+ end
43
+
44
+ attr_reader :resource_klass, :id, :type, :context, :fetchable_fields, :relationships,
45
+ :links_json, :attributes_json, :meta_json
46
+
47
+ def initialize(resource_klass, id, type, context, fetchable_fields, relationships,
48
+ links_json, attributes_json, meta_json)
49
+ @resource_klass = resource_klass
50
+ @id = id
51
+ @type = type
52
+ @context = context
53
+ @fetchable_fields = Set.new(fetchable_fields)
54
+
55
+ # Relationships left uncompiled because we'll often want to insert included ids on retrieval
56
+ @relationships = relationships
57
+
58
+ @links_json = CompiledJson.of(links_json)
59
+ @attributes_json = CompiledJson.of(attributes_json)
60
+ @meta_json = CompiledJson.of(meta_json)
61
+ end
62
+
63
+ def to_cache_value
64
+ {
65
+ id: id,
66
+ type: type,
67
+ fetchable: fetchable_fields,
68
+ rels: relationships,
69
+ links: links_json.try(:to_s),
70
+ attrs: attributes_json.try(:to_s),
71
+ meta: meta_json.try(:to_s)
72
+ }
73
+ end
74
+
75
+ # @param [Lookup[]] lookups
76
+ # @return [Hash<Class<Resource>, Hash<ID, CachedResourceFragment>>]
77
+ def self.lookup(lookups, context)
78
+ type_to_klass = lookups.map {|l| [l.type, l.resource_klass]}.to_h
79
+
80
+ keys = lookups.map(&:keys).flatten(1)
81
+
82
+ hits = JSONAPI.configuration.resource_cache.read_multi(*keys).reject {|_, v| v.nil?}
83
+
84
+ return keys.inject({}) do |hash, key|
85
+ (type, id, _, _) = key
86
+ resource_klass = type_to_klass[type]
87
+ hash[resource_klass] ||= {}
88
+
89
+ if hits.has_key?(key)
90
+ hash[resource_klass][id] = self.from_cache_value(resource_klass, context, hits[key])
91
+ else
92
+ hash[resource_klass][id] = nil
93
+ end
94
+
95
+ hash
96
+ end
97
+ end
98
+
99
+ # @param [Write[]] lookups
100
+ def self.write(writes)
101
+ key_values = writes.map(&:to_key_value)
102
+
103
+ to_write = key_values.map {|(k, v)| [k, v.to_cache_value]}.to_h
104
+
105
+ if JSONAPI.configuration.resource_cache.respond_to? :write_multi
106
+ JSONAPI.configuration.resource_cache.write_multi(to_write)
107
+ else
108
+ to_write.each do |key, value|
109
+ JSONAPI.configuration.resource_cache.write(key, value)
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+ def self.from_cache_value(resource_klass, context, h)
116
+ new(
117
+ resource_klass,
118
+ h.fetch(:id),
119
+ h.fetch(:type),
120
+ context,
121
+ h.fetch(:fetchable),
122
+ h.fetch(:rels, nil),
123
+ h.fetch(:links, nil),
124
+ h.fetch(:attrs, nil),
125
+ h.fetch(:meta, nil)
126
+ )
127
+ end
128
+ end
129
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/callbacks'
2
4
 
3
5
  module JSONAPI
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # JSONAPI::CompatibilityHelper
4
+ #
5
+ # This module provides a version-safe method for issuing deprecation warnings
6
+ # that works across multiple versions of Rails (7.x, 8.x, etc).
7
+ #
8
+ # Usage:
9
+ # JSONAPI::CompatibilityHelper.deprecation_warn("Your deprecation message")
10
+ #
11
+ # The method will use the public `warn` method if available, otherwise it will
12
+ # use `send(:warn, ...)` to maintain compatibility with Rails 8+ where `warn`
13
+ # is private.
14
+ #
15
+ # Example:
16
+ # JSONAPI::CompatibilityHelper.deprecation_warn("This feature is deprecated.")
17
+
18
+ module JSONAPI
19
+ module CompatibilityHelper
20
+ def deprecation_warn(message)
21
+ if ActiveSupport::Deprecation.respond_to?(:warn) && ActiveSupport::Deprecation.public_method_defined?(:warn)
22
+ ActiveSupport::Deprecation.warn(message)
23
+ else
24
+ ActiveSupport::Deprecation.send(:warn, message)
25
+ end
26
+ end
27
+ module_function :deprecation_warn
28
+ end
29
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  class CompiledJson
3
5
  def self.compile(h)
@@ -5,6 +7,7 @@ module JSONAPI
5
7
  end
6
8
 
7
9
  def self.of(obj)
10
+ # :nocov:
8
11
  case obj
9
12
  when NilClass then nil
10
13
  when CompiledJson then obj
@@ -12,6 +15,7 @@ module JSONAPI
12
15
  when Hash then CompiledJson.compile(obj)
13
16
  else raise "Can't figure out how to turn #{obj.inspect} into CompiledJson"
14
17
  end
18
+ # :nocov:
15
19
  end
16
20
 
17
21
  def initialize(json, h = nil)
@@ -19,7 +23,7 @@ module JSONAPI
19
23
  @h = h
20
24
  end
21
25
 
22
- def to_json(*args)
26
+ def to_json(*_args)
23
27
  @json
24
28
  end
25
29
 
@@ -27,9 +31,17 @@ module JSONAPI
27
31
  @json
28
32
  end
29
33
 
34
+ # :nocov:
30
35
  def to_h
31
36
  @h ||= JSON.parse(@json)
32
37
  end
38
+ # :nocov:
39
+
40
+ def [](key)
41
+ # :nocov:
42
+ to_h[key]
43
+ # :nocov:
44
+ end
33
45
 
34
46
  undef_method :as_json
35
47
  end
@@ -1,20 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'jsonapi/formatter'
2
4
  require 'jsonapi/processor'
3
5
  require 'concurrent'
4
-
6
+ require_relative 'compatibility_helper'
5
7
  module JSONAPI
6
8
  class Configuration
7
9
  attr_reader :json_key_format,
8
10
  :resource_key_type,
9
11
  :route_format,
10
12
  :raise_if_parameters_not_allowed,
11
- :allow_include,
13
+ :warn_on_route_setup_issues,
14
+ :warn_on_missing_routes,
15
+ :warn_on_performance_issues,
16
+ :default_allow_include_to_one,
17
+ :default_allow_include_to_many,
12
18
  :allow_sort,
13
19
  :allow_filter,
14
20
  :default_paginator,
15
21
  :default_page_size,
16
22
  :maximum_page_size,
17
- :default_processor_klass,
23
+ :default_processor_klass_name,
18
24
  :use_text_errors,
19
25
  :top_level_links_include_pagination,
20
26
  :top_level_meta_include_record_count,
@@ -23,16 +29,19 @@ module JSONAPI
23
29
  :top_level_meta_page_count_key,
24
30
  :allow_transactions,
25
31
  :include_backtraces_in_errors,
26
- :exception_class_whitelist,
27
- :whitelist_all_exceptions,
32
+ :include_application_backtraces_in_errors,
33
+ :exception_class_allowlist,
34
+ :allow_all_exceptions,
28
35
  :always_include_to_one_linkage_data,
29
36
  :always_include_to_many_linkage_data,
30
37
  :cache_formatters,
31
38
  :use_relationship_reflection,
32
39
  :resource_cache,
40
+ :default_caching,
33
41
  :default_resource_cache_field,
34
42
  :resource_cache_digest_function,
35
- :resource_cache_usage_report_function
43
+ :resource_cache_usage_report_function,
44
+ :default_exclude_links
36
45
 
37
46
  def initialize
38
47
  #:underscored_key, :camelized_key, :dasherized_key, or custom
@@ -45,12 +54,17 @@ module JSONAPI
45
54
  self.resource_key_type = :integer
46
55
 
47
56
  # optional request features
48
- self.allow_include = true
57
+ self.default_allow_include_to_one = true
58
+ self.default_allow_include_to_many = true
49
59
  self.allow_sort = true
50
60
  self.allow_filter = true
51
61
 
52
62
  self.raise_if_parameters_not_allowed = true
53
63
 
64
+ self.warn_on_route_setup_issues = true
65
+ self.warn_on_missing_routes = true
66
+ self.warn_on_performance_issues = true
67
+
54
68
  # :none, :offset, :paged, or a custom paginator name
55
69
  self.default_paginator = :none
56
70
 
@@ -71,20 +85,24 @@ module JSONAPI
71
85
  self.use_text_errors = false
72
86
 
73
87
  # Whether or not to include exception backtraces in JSONAPI error
74
- # responses. Defaults to `false` in production, and `true` otherwise.
75
- self.include_backtraces_in_errors = !Rails.env.production?
88
+ # responses. Defaults to `false` in anything other than development or test.
89
+ self.include_backtraces_in_errors = (Rails.env.development? || Rails.env.test?)
90
+
91
+ # Whether or not to include exception application backtraces in JSONAPI error
92
+ # responses. Defaults to `false` in anything other than development or test.
93
+ self.include_application_backtraces_in_errors = (Rails.env.development? || Rails.env.test?)
76
94
 
77
95
  # List of classes that should not be rescued by the operations processor.
78
96
  # For example, if you use Pundit for authorization, you might
79
97
  # raise a Pundit::NotAuthorizedError at some point during operations
80
98
  # processing. If you want to use Rails' `rescue_from` macro to
81
99
  # catch this error and render a 403 status code, you should add
82
- # the `Pundit::NotAuthorizedError` to the `exception_class_whitelist`.
83
- self.exception_class_whitelist = []
100
+ # the `Pundit::NotAuthorizedError` to the `exception_class_allowlist`.
101
+ self.exception_class_allowlist = []
84
102
 
85
- # If enabled, will override configuration option `exception_class_whitelist`
86
- # and whitelist all exceptions.
87
- self.whitelist_all_exceptions = false
103
+ # If enabled, will override configuration option `exception_class_allowlist`
104
+ # and allow all exceptions.
105
+ self.allow_all_exceptions = false
88
106
 
89
107
  # Resource Linkage
90
108
  # Controls the serialization of resource linkage for non compound documents
@@ -94,7 +112,7 @@ module JSONAPI
94
112
 
95
113
  # The default Operation Processor to use if one is not defined specifically
96
114
  # for a Resource.
97
- self.default_processor_klass = JSONAPI::Processor
115
+ self.default_processor_klass_name = 'JSONAPI::Processor'
98
116
 
99
117
  # Allows transactions for creating and updating records
100
118
  # Set this to false if your backend does not support transactions (e.g. Mongodb)
@@ -117,6 +135,11 @@ module JSONAPI
117
135
  # Rails cache store.
118
136
  self.resource_cache = nil
119
137
 
138
+ # Cache resources by default
139
+ # Cache resources by default. Individual resources can be excluded from caching by calling:
140
+ # `caching false`
141
+ self.default_caching = false
142
+
120
143
  # Default resource cache field
121
144
  # On Resources with caching enabled, this field will be used to check for out-of-date
122
145
  # cache entries, unless overridden on a specific Resource. Defaults to "updated_at".
@@ -131,6 +154,12 @@ module JSONAPI
131
154
  # Optionally provide a callable which JSONAPI will call with information about cache
132
155
  # performance. Should accept three arguments: resource name, hits count, misses count.
133
156
  self.resource_cache_usage_report_function = nil
157
+
158
+ # Global configuration for links exclusion
159
+ # Controls whether to generate links like `self`, `related` with all the resources
160
+ # and relationships. Accepts either `:default`, `:none`, or array containing the
161
+ # specific default links to exclude, which may be `:self` and `:related`.
162
+ self.default_exclude_links = :none
134
163
  end
135
164
 
136
165
  def cache_formatters=(bool)
@@ -192,16 +221,42 @@ module JSONAPI
192
221
  return formatter
193
222
  end
194
223
 
195
- def exception_class_whitelisted?(e)
196
- @whitelist_all_exceptions ||
197
- @exception_class_whitelist.flatten.any? { |k| e.class.ancestors.map(&:to_s).include?(k.to_s) }
224
+ def exception_class_allowed?(e)
225
+ @allow_all_exceptions ||
226
+ @exception_class_allowlist.flatten.any? { |k| e.class.ancestors.map(&:to_s).include?(k.to_s) }
198
227
  end
199
228
 
200
229
  def default_processor_klass=(default_processor_klass)
230
+ JSONAPI::CompatibilityHelper.deprecation_warn('`default_processor_klass` has been replaced by `default_processor_klass_name`.')
201
231
  @default_processor_klass = default_processor_klass
202
232
  end
203
233
 
204
- attr_writer :allow_include, :allow_sort, :allow_filter
234
+ def default_processor_klass
235
+ @default_processor_klass ||= default_processor_klass_name.safe_constantize
236
+ end
237
+
238
+ def default_processor_klass_name=(default_processor_klass_name)
239
+ @default_processor_klass = nil
240
+ @default_processor_klass_name = default_processor_klass_name
241
+ end
242
+
243
+ def allow_include=(allow_include)
244
+ JSONAPI::CompatibilityHelper.deprecation_warn('`allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options.')
245
+ @default_allow_include_to_one = allow_include
246
+ @default_allow_include_to_many = allow_include
247
+ end
248
+
249
+ def whitelist_all_exceptions=(allow_all_exceptions)
250
+ JSONAPI::CompatibilityHelper.deprecation_warn('`whitelist_all_exceptions` has been replaced by `allow_all_exceptions`')
251
+ @allow_all_exceptions = allow_all_exceptions
252
+ end
253
+
254
+ def exception_class_whitelist=(exception_class_allowlist)
255
+ JSONAPI::CompatibilityHelper.deprecation_warn('`exception_class_whitelist` has been replaced by `exception_class_allowlist`')
256
+ @exception_class_allowlist = exception_class_allowlist
257
+ end
258
+
259
+ attr_writer :allow_sort, :allow_filter, :default_allow_include_to_one, :default_allow_include_to_many
205
260
 
206
261
  attr_writer :default_paginator
207
262
 
@@ -225,9 +280,11 @@ module JSONAPI
225
280
 
226
281
  attr_writer :include_backtraces_in_errors
227
282
 
228
- attr_writer :exception_class_whitelist
283
+ attr_writer :include_application_backtraces_in_errors
284
+
285
+ attr_writer :exception_class_allowlist
229
286
 
230
- attr_writer :whitelist_all_exceptions
287
+ attr_writer :allow_all_exceptions
231
288
 
232
289
  attr_writer :always_include_to_one_linkage_data
233
290
 
@@ -235,15 +292,25 @@ module JSONAPI
235
292
 
236
293
  attr_writer :raise_if_parameters_not_allowed
237
294
 
295
+ attr_writer :warn_on_route_setup_issues
296
+
297
+ attr_writer :warn_on_missing_routes
298
+
299
+ attr_writer :warn_on_performance_issues
300
+
238
301
  attr_writer :use_relationship_reflection
239
302
 
240
303
  attr_writer :resource_cache
241
304
 
305
+ attr_writer :default_caching
306
+
242
307
  attr_writer :default_resource_cache_field
243
308
 
244
309
  attr_writer :resource_cache_digest_function
245
310
 
246
311
  attr_writer :resource_cache_usage_report_function
312
+
313
+ attr_writer :default_exclude_links
247
314
  end
248
315
 
249
316
  class << self
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
@@ -24,6 +26,33 @@ module JSONAPI
24
26
  instance_variables.each {|var| hash[var.to_s.delete('@')] = instance_variable_get(var) unless instance_variable_get(var).nil? }
25
27
  hash
26
28
  end
29
+
30
+ def update_with_overrides(error_object_overrides)
31
+ @title = error_object_overrides[:title] || @title
32
+ @detail = error_object_overrides[:detail] || @detail
33
+ @id = error_object_overrides[:id] || @id
34
+ @href = error_object_overrides[:href] || href
35
+
36
+ if error_object_overrides[:code]
37
+ # :nocov:
38
+ @code = if JSONAPI.configuration.use_text_errors
39
+ TEXT_ERRORS[error_object_overrides[:code]]
40
+ else
41
+ error_object_overrides[:code]
42
+ end
43
+ # :nocov:
44
+ end
45
+
46
+ @source = error_object_overrides[:source] || @source
47
+ @links = error_object_overrides[:links] || @links
48
+
49
+ if error_object_overrides[:status]
50
+ # :nocov:
51
+ @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[error_object_overrides[:status]].to_s
52
+ # :nocov:
53
+ end
54
+ @meta = error_object_overrides[:meta] || @meta
55
+ end
27
56
  end
28
57
 
29
58
  class Warning
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  VALIDATION_ERROR = '100'
3
5
  INVALID_RESOURCE = '101'
@@ -20,6 +22,7 @@ module JSONAPI
20
22
  INVALID_FILTERS_SYNTAX = '120'
21
23
  SAVE_FAILED = '121'
22
24
  INVALID_DATA_FORMAT = '122'
25
+ INVALID_RELATIONSHIP = '123'
23
26
  BAD_REQUEST = '400'
24
27
  FORBIDDEN = '403'
25
28
  RECORD_NOT_FOUND = '404'
@@ -50,6 +53,7 @@ module JSONAPI
50
53
  INVALID_FILTERS_SYNTAX => 'INVALID_FILTERS_SYNTAX',
51
54
  SAVE_FAILED => 'SAVE_FAILED',
52
55
  INVALID_DATA_FORMAT => 'INVALID_DATA_FORMAT',
56
+ INVALID_RELATIONSHIP => 'INVALID_RELATIONSHIP',
53
57
  FORBIDDEN => 'FORBIDDEN',
54
58
  RECORD_NOT_FOUND => 'RECORD_NOT_FOUND',
55
59
  NOT_ACCEPTABLE => 'NOT_ACCEPTABLE',