jpie 1.0.0 → 1.0.1

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/release.mdc +62 -0
  3. data/.gitignore +5 -0
  4. data/.rubocop.yml +82 -38
  5. data/Gemfile +12 -10
  6. data/Gemfile.lock +10 -1
  7. data/README.md +675 -1235
  8. data/jpie.gemspec +15 -15
  9. data/kiln/app/resources/user_message_resource.rb +2 -0
  10. data/lib/jpie.rb +0 -1
  11. data/lib/json_api/active_storage/deserialization.rb +32 -22
  12. data/lib/json_api/active_storage/detection.rb +36 -41
  13. data/lib/json_api/active_storage/serialization.rb +13 -11
  14. data/lib/json_api/configuration.rb +4 -5
  15. data/lib/json_api/controllers/base_controller.rb +3 -3
  16. data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
  17. data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
  18. data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
  19. data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
  20. data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
  21. data/lib/json_api/controllers/concerns/controller_helpers.rb +11 -215
  22. data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
  23. data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
  24. data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
  25. data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
  26. data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
  27. data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
  28. data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
  29. data/lib/json_api/controllers/concerns/relationships_controller/active_storage_removal.rb +67 -0
  30. data/lib/json_api/controllers/concerns/relationships_controller/events.rb +44 -0
  31. data/lib/json_api/controllers/concerns/relationships_controller/removal.rb +92 -0
  32. data/lib/json_api/controllers/concerns/relationships_controller/response_helpers.rb +55 -0
  33. data/lib/json_api/controllers/concerns/relationships_controller/serialization.rb +72 -0
  34. data/lib/json_api/controllers/concerns/relationships_controller/sorting.rb +114 -0
  35. data/lib/json_api/controllers/concerns/relationships_controller/updating.rb +73 -0
  36. data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
  37. data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
  38. data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
  39. data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
  40. data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
  41. data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
  42. data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
  43. data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
  44. data/lib/json_api/controllers/concerns/resource_actions.rb +51 -602
  45. data/lib/json_api/controllers/relationships_controller.rb +26 -422
  46. data/lib/json_api/errors/parameter_not_allowed.rb +1 -1
  47. data/lib/json_api/railtie.rb +46 -9
  48. data/lib/json_api/resources/active_storage_blob_resource.rb +9 -1
  49. data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
  50. data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
  51. data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
  52. data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
  53. data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
  54. data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
  55. data/lib/json_api/resources/resource.rb +13 -219
  56. data/lib/json_api/routing.rb +56 -47
  57. data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
  58. data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
  59. data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
  60. data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
  61. data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
  62. data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
  63. data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
  64. data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
  65. data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
  66. data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
  67. data/lib/json_api/serialization/deserializer.rb +10 -346
  68. data/lib/json_api/serialization/serializer.rb +17 -260
  69. data/lib/json_api/support/active_storage_support.rb +10 -13
  70. data/lib/json_api/support/collection_query.rb +14 -370
  71. data/lib/json_api/support/concerns/condition_building.rb +57 -0
  72. data/lib/json_api/support/concerns/nested_filters.rb +130 -0
  73. data/lib/json_api/support/concerns/pagination.rb +30 -0
  74. data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
  75. data/lib/json_api/support/concerns/regular_filters.rb +81 -0
  76. data/lib/json_api/support/concerns/sorting.rb +88 -0
  77. data/lib/json_api/support/instrumentation.rb +13 -12
  78. data/lib/json_api/support/param_helpers.rb +9 -6
  79. data/lib/json_api/support/relationship_helpers.rb +4 -2
  80. data/lib/json_api/support/resource_identifier.rb +29 -29
  81. data/lib/json_api/support/responders.rb +5 -5
  82. data/lib/json_api/version.rb +1 -1
  83. metadata +51 -1
data/jpie.gemspec CHANGED
@@ -1,31 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('lib', __dir__)
3
+ lib = File.expand_path("lib", __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require 'json_api/version'
5
+ require "json_api/version"
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = 'jpie'
8
+ spec.name = "jpie"
9
9
  spec.version = JSONAPI::VERSION
10
- spec.authors = ['Emil Kampp']
11
- spec.email = ['emil@kampp.me']
10
+ spec.authors = ["Emil Kampp"]
11
+ spec.email = ["emil@kampp.me"]
12
12
 
13
- spec.summary = 'JSON:API compliant Rails gem for producing and consuming JSON:API resources'
14
- spec.description = 'A Rails 8+ gem that provides jsonapi_resources routing DSL and generic JSON:API controllers'
15
- spec.homepage = 'https://github.com/klaay/json_api'
16
- spec.license = 'MIT'
13
+ spec.summary = "JSON:API compliant Rails gem for producing and consuming JSON:API resources"
14
+ spec.description = "A Rails 8+ gem that provides jsonapi_resources routing DSL and generic JSON:API controllers"
15
+ spec.homepage = "https://github.com/klaay/json_api"
16
+ spec.license = "MIT"
17
17
 
18
18
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
19
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
20
  end
21
- spec.bindir = 'exe'
21
+ spec.bindir = "exe"
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
- spec.require_paths = ['lib']
23
+ spec.require_paths = ["lib"]
24
24
 
25
- spec.required_ruby_version = '>= 3.4.0'
25
+ spec.required_ruby_version = ">= 3.4.0"
26
26
 
27
- spec.add_dependency 'actionpack', '~> 8.0', '>= 8.0.0'
28
- spec.add_dependency 'rails', '~> 8.0', '>= 8.0.0'
27
+ spec.add_dependency "actionpack", "~> 8.0", ">= 8.0.0"
28
+ spec.add_dependency "rails", "~> 8.0", ">= 8.0.0"
29
29
 
30
- spec.metadata['rubygems_mfa_required'] = 'true'
30
+ spec.metadata["rubygems_mfa_required"] = "true"
31
31
  end
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class UserMessageResource < MessageResource
2
4
  end
data/lib/jpie.rb CHANGED
@@ -3,4 +3,3 @@
3
3
  # jpie gem entry point
4
4
  # Requires the json_api code which provides the JSONAPI namespace
5
5
  require "json_api"
6
-
@@ -19,18 +19,19 @@ module JSONAPI
19
19
  def attach_files(record, attachment_params, definition: nil)
20
20
  return if attachment_params.empty?
21
21
 
22
- attachment_params.each do |attachment_name, blob_or_blobs|
23
- attachment = record.public_send(attachment_name)
24
- is_has_many = blob_or_blobs.is_a?(Array)
25
- append_only = is_has_many && append_only_enabled?(attachment_name, definition)
26
-
27
- if is_has_many
28
- handle_has_many_attachment(attachment, blob_or_blobs, append_only:,
29
- attachment_name:, definition:)
30
- else
31
- handle_has_one_attachment(attachment, blob_or_blobs, attachment_name:,
32
- definition:)
33
- end
22
+ attachment_params.each do |name, blobs|
23
+ attach_single(record, name, blobs, definition)
24
+ end
25
+ end
26
+
27
+ def attach_single(record, name, blobs, definition)
28
+ attachment = record.public_send(name)
29
+ if blobs.is_a?(Array)
30
+ handle_has_many_attachment(attachment, blobs,
31
+ append_only: append_only_enabled?(name, definition),
32
+ attachment_name: name, definition:,)
33
+ else
34
+ handle_has_one_attachment(attachment, blobs, attachment_name: name, definition:)
34
35
  end
35
36
  end
36
37
 
@@ -80,17 +81,26 @@ module JSONAPI
80
81
 
81
82
  # Private helper methods
82
83
  def handle_has_many_attachment(attachment, blob_or_blobs, append_only:, attachment_name:, definition:)
83
- if blob_or_blobs.empty?
84
- return if append_only
84
+ return handle_empty_blobs(attachment, append_only, attachment_name, definition) if blob_or_blobs.empty?
85
+ return append_blobs(attachment, blob_or_blobs) if append_only
85
86
 
86
- attachment.purge if purge_on_nil_enabled?(attachment_name, definition) && attachment.attached?
87
- elsif append_only
88
- existing_blobs = attachment.attached? ? attachment.blobs.to_a : []
89
- attachment.attach(existing_blobs + blob_or_blobs)
90
- else
91
- attachment.purge if attachment.attached?
92
- attachment.attach(blob_or_blobs)
93
- end
87
+ replace_blobs(attachment, blob_or_blobs)
88
+ end
89
+
90
+ def handle_empty_blobs(attachment, append_only, attachment_name, definition)
91
+ return if append_only
92
+
93
+ attachment.purge if purge_on_nil_enabled?(attachment_name, definition) && attachment.attached?
94
+ end
95
+
96
+ def append_blobs(attachment, blob_or_blobs)
97
+ existing_blobs = attachment.attached? ? attachment.blobs.to_a : []
98
+ attachment.attach(existing_blobs + blob_or_blobs)
99
+ end
100
+
101
+ def replace_blobs(attachment, blob_or_blobs)
102
+ attachment.purge if attachment.attached?
103
+ attachment.attach(blob_or_blobs)
94
104
  end
95
105
 
96
106
  def handle_has_one_attachment(attachment, blob_or_blobs, attachment_name:, definition:)
@@ -21,53 +21,48 @@ module JSONAPI
21
21
  def filter_from_includes(includes_hash, current_model_class)
22
22
  return {} unless defined?(::ActiveStorage)
23
23
 
24
- filtered = {}
25
- includes_hash.each do |key, value|
26
- # Skip ActiveStorage attachments - they'll be loaded on-demand by the serializer
27
- next if current_model_class.reflect_on_attachment(key).present?
28
-
29
- # Check if this is a regular association
30
- association = current_model_class.reflect_on_association(key)
31
- next if association.nil?
32
-
33
- if value.is_a?(Hash) && value.any?
34
- # For polymorphic associations, we can't determine the class at compile time,
35
- # so we skip filtering and include the nested hash as-is
36
- if association.polymorphic?
37
- filtered[key] = value
38
- else
39
- # Recursively filter nested includes, using the associated class
40
- nested_class = association.klass
41
- nested_filtered = filter_from_includes(value, nested_class)
42
- filtered[key] = nested_filtered if nested_filtered.any?
43
- end
44
- else
45
- filtered[key] = value
46
- end
24
+ includes_hash.each_with_object({}) do |(key, value), filtered|
25
+ process_include_entry(key, value, current_model_class, filtered)
47
26
  end
48
- filtered
27
+ end
28
+
29
+ def process_include_entry(key, value, current_model_class, filtered)
30
+ return if current_model_class.reflect_on_attachment(key).present?
31
+
32
+ association = current_model_class.reflect_on_association(key)
33
+ return if association.nil?
34
+
35
+ result = filter_include_value(value, association)
36
+ filtered[key] = result unless result.nil?
37
+ end
38
+
39
+ def filter_include_value(value, association)
40
+ return value unless value.is_a?(Hash) && value.any?
41
+ return value if association.polymorphic?
42
+
43
+ nested_filtered = filter_from_includes(value, association.klass)
44
+ nested_filtered.any? ? nested_filtered : nil
49
45
  end
50
46
 
51
47
  def filter_polymorphic_from_includes(includes_hash, current_model_class)
52
- filtered = {}
53
-
54
- includes_hash.each do |key, value|
55
- association = current_model_class.reflect_on_association(key)
56
- next unless association
57
-
58
- # Skip polymorphic associations entirely for preloading
59
- next if association.polymorphic?
60
-
61
- if value.is_a?(Hash) && value.any?
62
- nested_class = association.klass
63
- nested_filtered = filter_polymorphic_from_includes(value, nested_class)
64
- filtered[key] = nested_filtered if nested_filtered.any?
65
- else
66
- filtered[key] = value
67
- end
48
+ includes_hash.each_with_object({}) do |(key, value), filtered|
49
+ process_polymorphic_include(key, value, current_model_class, filtered)
68
50
  end
51
+ end
52
+
53
+ def process_polymorphic_include(key, value, model_class, filtered)
54
+ association = model_class.reflect_on_association(key)
55
+ return unless association && !association.polymorphic?
56
+
57
+ result = resolve_polymorphic_value(value, association)
58
+ filtered[key] = result if result
59
+ end
60
+
61
+ def resolve_polymorphic_value(value, association)
62
+ return value unless value.is_a?(Hash) && value.any?
69
63
 
70
- filtered
64
+ nested = filter_polymorphic_from_includes(value, association.klass)
65
+ nested.any? ? nested : nil
71
66
  end
72
67
  end
73
68
  end
@@ -9,19 +9,21 @@ module JSONAPI
9
9
  return nil unless defined?(::ActiveStorage)
10
10
 
11
11
  attachment = record.public_send(attachment_name)
12
+ return nil unless attachment.respond_to?(:attached?)
12
13
 
13
- if attachment.respond_to?(:attached?) && attachment.attached?
14
- # has_many_attached returns an array-like object
15
- if attachment.is_a?(::ActiveStorage::Attached::Many)
16
- attachment.blobs.map { |blob| serialize_blob_identifier(blob) }
17
- else
18
- # has_one_attached
19
- serialize_blob_identifier(attachment.blob)
20
- end
21
- elsif attachment.respond_to?(:attached?) && !attachment.attached?
22
- # Not attached - return nil for has_one, empty array for has_many
23
- attachment.is_a?(::ActiveStorage::Attached::Many) ? [] : nil
14
+ attachment.attached? ? serialize_attached(attachment) : empty_attachment_value(attachment)
15
+ end
16
+
17
+ def serialize_attached(attachment)
18
+ if attachment.is_a?(::ActiveStorage::Attached::Many)
19
+ return attachment.blobs.map { |blob| serialize_blob_identifier(blob) }
24
20
  end
21
+
22
+ serialize_blob_identifier(attachment.blob)
23
+ end
24
+
25
+ def empty_attachment_value(attachment)
26
+ attachment.is_a?(::ActiveStorage::Attached::Many) ? [] : nil
25
27
  end
26
28
 
27
29
  def serialize_blob_identifier(blob)
@@ -2,17 +2,16 @@
2
2
 
3
3
  module JSONAPI
4
4
  class Configuration
5
- attr_accessor :default_page_size, :max_page_size, :jsonapi_version, :jsonapi_meta, :authorization_handler,
5
+ attr_accessor :default_page_size, :max_page_size, :jsonapi_meta, :authorization_handler,
6
6
  :authorization_scope, :document_meta_resolver
7
7
 
8
8
  def initialize
9
9
  @default_page_size = 25
10
10
  @max_page_size = 100
11
- @jsonapi_version = "1.1"
12
11
  @jsonapi_meta = nil
13
12
  @authorization_handler = nil
14
13
  @authorization_scope = nil
15
- @document_meta_resolver = ->(controller:) { {} }
14
+ @document_meta_resolver = ->(controller:) { {} } # rubocop:disable Lint/UnusedBlockArgument
16
15
  @base_controller_class = "ActionController::API"
17
16
  end
18
17
 
@@ -20,7 +19,7 @@ module JSONAPI
20
19
  if value.is_a?(Class)
21
20
  @base_controller_class = value.name
22
21
  elsif value.is_a?(String)
23
- raise ArgumentError, "base_controller_class cannot be blank" if value.blank?
22
+ raise ArgumentError, "base_controller_class cannot be blank" if value.nil? || value.strip.empty?
24
23
 
25
24
  @base_controller_class = value
26
25
  else
@@ -38,7 +37,7 @@ module JSONAPI
38
37
 
39
38
  def resolved_base_controller_class
40
39
  class_name = @base_controller_class
41
- raise ArgumentError, "base_controller_class cannot be blank" if class_name.blank?
40
+ raise ArgumentError, "base_controller_class cannot be blank" if class_name.nil? || class_name.empty?
42
41
 
43
42
  class_name.constantize
44
43
  end
@@ -17,9 +17,9 @@ module JSONAPI
17
17
  {
18
18
  status: "403",
19
19
  title: "Forbidden",
20
- detail:
21
- }
22
- ]
20
+ detail:,
21
+ },
22
+ ],
23
23
  }, status: :forbidden
24
24
  end
25
25
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module ControllerHelpers
5
+ module Authorization
6
+ extend ActiveSupport::Concern
7
+
8
+ protected
9
+
10
+ def apply_authorization_scope(scope, action:)
11
+ handler = JSONAPI.configuration.authorization_scope
12
+ return scope unless handler
13
+
14
+ handler.call(controller: self, scope:, action:, model_class:)
15
+ end
16
+
17
+ def authorize_resource_action!(record, action:, context: nil)
18
+ handler = JSONAPI.configuration.authorization_handler
19
+ return unless handler
20
+
21
+ handler.call(controller: self, record:, action:, context:)
22
+ end
23
+
24
+ def current_user
25
+ nil
26
+ end
27
+ public :current_user
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module ControllerHelpers
5
+ module DocumentMeta
6
+ extend ActiveSupport::Concern
7
+
8
+ protected
9
+
10
+ def jsonapi_document_meta(extra_meta = {})
11
+ resolver = JSONAPI.configuration.document_meta_resolver
12
+ base_meta = resolver ? resolver.call(controller: self) : {}
13
+ meta = base_meta.merge(extra_meta || {})
14
+ return {} if meta.empty?
15
+
16
+ meta
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module ControllerHelpers
5
+ module ErrorRendering
6
+ extend ActiveSupport::Concern
7
+
8
+ protected
9
+
10
+ def render_resource_not_found_error(message)
11
+ render_jsonapi_error(
12
+ status: 404,
13
+ title: "Resource Not Found",
14
+ detail: message,
15
+ )
16
+ end
17
+
18
+ def render_model_not_found_error(error)
19
+ render_jsonapi_error(
20
+ status: 404,
21
+ title: "Resource Not Found",
22
+ detail: "Model class for '#{@resource_name}' not found: #{error.message}",
23
+ )
24
+ end
25
+
26
+ def render_invalid_relationship_error(error)
27
+ render_jsonapi_error(
28
+ status: 400,
29
+ title: "Invalid Relationship",
30
+ detail: error.message,
31
+ )
32
+ end
33
+
34
+ def render_validation_errors(resource)
35
+ errors = resource.errors.map do |error|
36
+ {
37
+ status: "422",
38
+ title: "Validation Error",
39
+ detail: error.full_message,
40
+ source: { pointer: "/data/attributes/#{error.attribute}" },
41
+ }
42
+ end
43
+
44
+ render json: { errors: }, status: :unprocessable_entity
45
+ end
46
+
47
+ def render_parameter_not_allowed_error(error)
48
+ error_params = error.respond_to?(:params) ? error.params : []
49
+ detail = error_params.present? ? error_params.join(", ") : "Relationship or attribute is read-only"
50
+
51
+ render json: { errors: [parameter_not_allowed_error(detail)] }, status: :bad_request
52
+ end
53
+
54
+ def parameter_not_allowed_error(detail)
55
+ {
56
+ status: "400",
57
+ code: "parameter_not_allowed",
58
+ title: "Parameter Not Allowed",
59
+ detail:,
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module ControllerHelpers
5
+ module Parsing
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_action :parse_jsonapi_body, if: -> { modifying_request? }
10
+ end
11
+
12
+ protected
13
+
14
+ def parse_jsonapi_body
15
+ return unless jsonapi_content_type?
16
+ return if params[:data].present?
17
+
18
+ parse_and_apply_json_body
19
+ rescue JSON::ParserError
20
+ # Invalid JSON - will be handled by validation
21
+ end
22
+
23
+ def modifying_request?
24
+ request.post? || request.patch? || request.put? || request.delete?
25
+ end
26
+
27
+ def jsonapi_content_type?
28
+ request.content_type&.include?("application/vnd.api+json")
29
+ end
30
+
31
+ def parse_and_apply_json_body
32
+ body = request.body.read
33
+ request.body.rewind
34
+ return if body.blank?
35
+
36
+ parsed = JSON.parse(body)
37
+ parsed.deep_transform_keys!(&:to_sym)
38
+ request.env["action_dispatch.request.request_parameters"] = parsed
39
+ end
40
+
41
+ def jsonapi_params
42
+ data = params.require(:data)
43
+ return data if data.is_a?(Array)
44
+
45
+ permitted = data.permit(:type, :id, attributes: {})
46
+ permitted[:relationships] = permit_relationships(data) if data[:relationships].present?
47
+ permitted
48
+ end
49
+
50
+ def permit_relationships(data)
51
+ result = {}
52
+ data[:relationships].each do |key, value|
53
+ result[key] = value.permit(data: %i[type id]) if value.is_a?(ActionController::Parameters)
54
+ end
55
+ result
56
+ end
57
+
58
+ def jsonapi_attributes
59
+ (jsonapi_params[:attributes] || {}).to_h
60
+ end
61
+
62
+ def jsonapi_relationships
63
+ jsonapi_params[:relationships] || {}
64
+ end
65
+
66
+ def jsonapi_type
67
+ data = jsonapi_params
68
+ return data.first[:type] if data.is_a?(Array)
69
+
70
+ data[:type]
71
+ end
72
+
73
+ def jsonapi_id
74
+ data = jsonapi_params
75
+ return data.first[:id].to_s.presence if data.is_a?(Array)
76
+
77
+ data[:id].to_s.presence
78
+ end
79
+
80
+ def parse_include_param
81
+ return [] unless params[:include]
82
+
83
+ params[:include].to_s.split(",").map(&:strip)
84
+ end
85
+
86
+ def parse_fields_param
87
+ return {} unless params[:fields]
88
+
89
+ params[:fields].permit!.to_h.each_with_object({}) do |(type, fields), hash|
90
+ hash[type.to_sym] = fields.to_s.split(",").map(&:strip)
91
+ end
92
+ end
93
+
94
+ def parse_filter_param
95
+ return {} unless params[:filter]
96
+
97
+ raw_filters = params[:filter].permit!.to_h
98
+ JSONAPI::ParamHelpers.flatten_filter_hash(raw_filters)
99
+ end
100
+
101
+ def parse_sort_param
102
+ return [] unless params[:sort]
103
+
104
+ params[:sort].to_s.split(",").map(&:strip)
105
+ end
106
+
107
+ def parse_page_param
108
+ return {} unless params[:page]
109
+
110
+ params[:page].permit(:number, :size).to_h
111
+ end
112
+
113
+ def invalid_sort_fields_for_columns(sorts, available_columns)
114
+ sorts.filter_map do |sort_field|
115
+ field = JSONAPI::RelationshipHelpers.extract_sort_field_name(sort_field)
116
+ field unless available_columns.include?(field.to_s)
117
+ end
118
+ end
119
+
120
+ def valid_sort_fields_for_resource(resource_class, model_class)
121
+ model_columns = model_class.column_names.map(&:to_sym)
122
+ resource_sortable_fields = resource_class.permitted_sortable_fields.map(&:to_sym)
123
+ (model_columns + resource_sortable_fields).uniq.map(&:to_s)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module ControllerHelpers
5
+ module ResourceSetup
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_reader :resource_class, :model_class
10
+ end
11
+
12
+ protected
13
+
14
+ def set_resource_name
15
+ @resource_name = params[:resource_type].to_s.singularize
16
+ end
17
+
18
+ def set_resource_class
19
+ @resource_class = JSONAPI::ResourceLoader.find(params[:resource_type])
20
+ @model_class = @resource_class.model_class
21
+ rescue JSONAPI::ResourceLoader::MissingResourceClass => e
22
+ render_resource_not_found_error(e.message)
23
+ rescue NameError => e
24
+ render_model_not_found_error(e)
25
+ end
26
+
27
+ def set_resource
28
+ @resource = @preloaded_resource || model_class.find(params[:id])
29
+ rescue ActiveRecord::RecordNotFound
30
+ render_jsonapi_error(
31
+ status: 404,
32
+ title: "Record Not Found",
33
+ detail: "Could not find #{@resource_name} with id '#{params[:id]}'",
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end