jpie 0.4.5 → 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.
- checksums.yaml +4 -4
- data/.cursor/rules/release.mdc +62 -0
- data/.gitignore +26 -0
- data/.rspec +3 -0
- data/.rubocop.yml +76 -107
- data/.travis.yml +7 -0
- data/Gemfile +23 -0
- data/Gemfile.lock +321 -0
- data/README.md +1508 -136
- data/Rakefile +3 -14
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/jpie.gemspec +21 -38
- data/kiln/app/resources/user_message_resource.rb +4 -0
- data/lib/jpie.rb +3 -25
- data/lib/json_api/active_storage/deserialization.rb +116 -0
- data/lib/json_api/active_storage/detection.rb +69 -0
- data/lib/json_api/active_storage/serialization.rb +34 -0
- data/lib/json_api/configuration.rb +57 -0
- data/lib/json_api/controllers/base_controller.rb +26 -0
- data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
- data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
- data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
- data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
- data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
- data/lib/json_api/controllers/concerns/controller_helpers.rb +19 -0
- data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
- data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
- data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
- data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
- data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
- data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
- data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
- data/lib/json_api/controllers/concerns/relationships_controller/active_storage_removal.rb +67 -0
- data/lib/json_api/controllers/concerns/relationships_controller/events.rb +44 -0
- data/lib/json_api/controllers/concerns/relationships_controller/removal.rb +92 -0
- data/lib/json_api/controllers/concerns/relationships_controller/response_helpers.rb +55 -0
- data/lib/json_api/controllers/concerns/relationships_controller/serialization.rb +72 -0
- data/lib/json_api/controllers/concerns/relationships_controller/sorting.rb +114 -0
- data/lib/json_api/controllers/concerns/relationships_controller/updating.rb +73 -0
- data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
- data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
- data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
- data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
- data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
- data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
- data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
- data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
- data/lib/json_api/controllers/concerns/resource_actions.rb +106 -0
- data/lib/json_api/controllers/relationships_controller.rb +108 -0
- data/lib/json_api/controllers/resources_controller.rb +6 -0
- data/lib/json_api/errors/parameter_not_allowed.rb +19 -0
- data/lib/json_api/railtie.rb +112 -0
- data/lib/json_api/resources/active_storage_blob_resource.rb +19 -0
- data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
- data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
- data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
- data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
- data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
- data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
- data/lib/json_api/resources/resource.rb +32 -0
- data/lib/json_api/resources/resource_loader.rb +35 -0
- data/lib/json_api/routing.rb +81 -0
- data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
- data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
- data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
- data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
- data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
- data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
- data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
- data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
- data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
- data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
- data/lib/json_api/serialization/deserializer.rb +26 -0
- data/lib/json_api/serialization/serializer.rb +77 -0
- data/lib/json_api/support/active_storage_support.rb +82 -0
- data/lib/json_api/support/collection_query.rb +50 -0
- data/lib/json_api/support/concerns/condition_building.rb +57 -0
- data/lib/json_api/support/concerns/nested_filters.rb +130 -0
- data/lib/json_api/support/concerns/pagination.rb +30 -0
- data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
- data/lib/json_api/support/concerns/regular_filters.rb +81 -0
- data/lib/json_api/support/concerns/sorting.rb +88 -0
- data/lib/json_api/support/instrumentation.rb +43 -0
- data/lib/json_api/support/param_helpers.rb +54 -0
- data/lib/json_api/support/relationship_guard.rb +16 -0
- data/lib/json_api/support/relationship_helpers.rb +76 -0
- data/lib/json_api/support/resource_identifier.rb +87 -0
- data/lib/json_api/support/responders.rb +100 -0
- data/lib/json_api/support/response_helpers.rb +10 -0
- data/lib/json_api/support/sort_parsing.rb +21 -0
- data/lib/json_api/support/type_conversion.rb +21 -0
- data/lib/json_api/testing/test_helper.rb +76 -0
- data/lib/json_api/testing.rb +3 -0
- data/lib/{jpie → json_api}/version.rb +2 -2
- data/lib/json_api.rb +50 -0
- data/lib/rubocop/cop/custom/hash_value_omission.rb +53 -0
- metadata +100 -169
- data/.cursor/rules/dependencies.mdc +0 -19
- data/.cursor/rules/examples.mdc +0 -16
- data/.cursor/rules/git.mdc +0 -14
- data/.cursor/rules/project_structure.mdc +0 -30
- data/.cursor/rules/publish_gem.mdc +0 -73
- data/.cursor/rules/security.mdc +0 -14
- data/.cursor/rules/style.mdc +0 -15
- data/.cursor/rules/testing.mdc +0 -16
- data/.overcommit.yml +0 -35
- data/CHANGELOG.md +0 -164
- data/LICENSE.txt +0 -21
- data/PUBLISHING.md +0 -111
- data/examples/basic_example.md +0 -146
- data/examples/including_related_resources.md +0 -491
- data/examples/pagination.md +0 -303
- data/examples/relationships.md +0 -114
- data/examples/resource_attribute_configuration.md +0 -147
- data/examples/resource_meta_configuration.md +0 -244
- data/examples/rspec_testing.md +0 -130
- data/examples/single_table_inheritance.md +0 -160
- data/lib/jpie/configuration.rb +0 -12
- data/lib/jpie/controller/crud_actions.rb +0 -141
- data/lib/jpie/controller/error_handling/handler_setup.rb +0 -124
- data/lib/jpie/controller/error_handling/handlers.rb +0 -109
- data/lib/jpie/controller/error_handling.rb +0 -23
- data/lib/jpie/controller/json_api_validation.rb +0 -193
- data/lib/jpie/controller/parameter_parsing.rb +0 -78
- data/lib/jpie/controller/related_actions.rb +0 -45
- data/lib/jpie/controller/relationship_actions.rb +0 -291
- data/lib/jpie/controller/relationship_validation.rb +0 -117
- data/lib/jpie/controller/rendering.rb +0 -154
- data/lib/jpie/controller.rb +0 -45
- data/lib/jpie/deserializer.rb +0 -110
- data/lib/jpie/errors.rb +0 -117
- data/lib/jpie/generators/resource_generator.rb +0 -116
- data/lib/jpie/generators/templates/resource.rb.erb +0 -31
- data/lib/jpie/railtie.rb +0 -42
- data/lib/jpie/resource/attributable.rb +0 -112
- data/lib/jpie/resource/inferrable.rb +0 -43
- data/lib/jpie/resource/sortable.rb +0 -93
- data/lib/jpie/resource.rb +0 -147
- data/lib/jpie/routing.rb +0 -59
- data/lib/jpie/serializer.rb +0 -205
data/Rakefile
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require 'rubocop/rake_task'
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
6
5
|
|
|
7
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
8
|
-
RuboCop::RakeTask.new
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
task brakeman: :environment do
|
|
12
|
-
require 'brakeman'
|
|
13
|
-
Brakeman.run app_path: '.', print_report: true, exit_on_warn: true
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
desc 'Run all checks (RSpec, RuboCop, Brakeman)'
|
|
17
|
-
task check: %i[spec rubocop brakeman]
|
|
18
|
-
|
|
19
|
-
task default: :check
|
|
8
|
+
task default: :spec
|
data/bin/console
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "json_api"
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
+
# require "pry"
|
|
12
|
+
# Pry.start
|
|
13
|
+
|
|
14
|
+
require "irb"
|
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/jpie.gemspec
CHANGED
|
@@ -1,48 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require "json_api/version"
|
|
4
6
|
|
|
5
7
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name
|
|
7
|
-
spec.version
|
|
8
|
-
spec.authors
|
|
9
|
-
spec.email
|
|
8
|
+
spec.name = "jpie"
|
|
9
|
+
spec.version = JSONAPI::VERSION
|
|
10
|
+
spec.authors = ["Emil Kampp"]
|
|
11
|
+
spec.email = ["emil@kampp.me"]
|
|
10
12
|
|
|
11
|
-
spec.summary
|
|
12
|
-
spec.description
|
|
13
|
-
|
|
14
|
-
spec.
|
|
15
|
-
spec.license = 'MIT'
|
|
16
|
-
spec.required_ruby_version = '>= 3.4.0'
|
|
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
|
-
spec.
|
|
19
|
-
|
|
20
|
-
spec.metadata['source_code_uri'] = "#{spec.homepage}.git"
|
|
21
|
-
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
22
|
-
|
|
23
|
-
# Specify which files should be added to the gem when it is released.
|
|
24
|
-
spec.files = Dir.chdir(__dir__) do
|
|
25
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
|
26
|
-
(File.expand_path(f) == __FILE__) ||
|
|
27
|
-
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
28
|
-
end
|
|
18
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
29
20
|
end
|
|
30
|
-
spec.bindir
|
|
31
|
-
spec.executables
|
|
32
|
-
spec.require_paths = [
|
|
21
|
+
spec.bindir = "exe"
|
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
23
|
+
spec.require_paths = ["lib"]
|
|
24
|
+
|
|
25
|
+
spec.required_ruby_version = ">= 3.4.0"
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
spec.add_dependency
|
|
36
|
-
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"
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
spec.add_development_dependency 'brakeman', '~> 6.0'
|
|
40
|
-
spec.add_development_dependency 'rake', '~> 13.0'
|
|
41
|
-
spec.add_development_dependency 'rspec', '~> 3.12'
|
|
42
|
-
spec.add_development_dependency 'rspec-rails', '~> 7.0'
|
|
43
|
-
spec.add_development_dependency 'rubocop', '~> 1.50'
|
|
44
|
-
spec.add_development_dependency 'rubocop-performance', '~> 1.16'
|
|
45
|
-
spec.add_development_dependency 'rubocop-rails', '~> 2.18'
|
|
46
|
-
spec.add_development_dependency 'rubocop-rspec', '~> 2.20'
|
|
47
|
-
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
30
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
48
31
|
end
|
data/lib/jpie.rb
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require
|
|
6
|
-
require_relative 'jpie/resource'
|
|
7
|
-
require_relative 'jpie/errors'
|
|
8
|
-
|
|
9
|
-
module JPie
|
|
10
|
-
autoload :Serializer, 'jpie/serializer'
|
|
11
|
-
autoload :Deserializer, 'jpie/deserializer'
|
|
12
|
-
autoload :Controller, 'jpie/controller'
|
|
13
|
-
autoload :Configuration, 'jpie/configuration'
|
|
14
|
-
autoload :Routing, 'jpie/routing'
|
|
15
|
-
|
|
16
|
-
class << self
|
|
17
|
-
def configuration
|
|
18
|
-
@configuration ||= Configuration.new
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def configure
|
|
22
|
-
yield(configuration)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
require 'jpie/railtie' if defined?(Rails)
|
|
3
|
+
# jpie gem entry point
|
|
4
|
+
# Requires the json_api code which provides the JSONAPI namespace
|
|
5
|
+
require "json_api"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module ActiveStorage
|
|
5
|
+
module Deserialization
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def extract_params_from_hash(params_hash, model_class)
|
|
9
|
+
return {} unless defined?(::ActiveStorage)
|
|
10
|
+
|
|
11
|
+
attachment_params = {}
|
|
12
|
+
model_class.attachment_reflections.each_key do |attachment_name|
|
|
13
|
+
attachment_name_str = attachment_name.to_s
|
|
14
|
+
attachment_params[attachment_name] = params_hash[attachment_name_str] if params_hash.key?(attachment_name_str)
|
|
15
|
+
end
|
|
16
|
+
attachment_params
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def attach_files(record, attachment_params, definition: nil)
|
|
20
|
+
return if attachment_params.empty?
|
|
21
|
+
|
|
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:)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def process_attachment(attrs, association_name, id_or_ids, singular:)
|
|
39
|
+
if singular
|
|
40
|
+
blob = find_blob_by_signed_id(id_or_ids)
|
|
41
|
+
attrs[association_name.to_s] = blob
|
|
42
|
+
else
|
|
43
|
+
blobs = Array(id_or_ids).map { |id| find_blob_by_signed_id(id) }
|
|
44
|
+
attrs[association_name.to_s] = blobs
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def find_blob_by_signed_id(signed_id)
|
|
49
|
+
return nil if signed_id.blank?
|
|
50
|
+
|
|
51
|
+
::ActiveStorage::Blob.find_signed!(signed_id)
|
|
52
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
53
|
+
raise
|
|
54
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
55
|
+
raise ArgumentError, "Blob not found for signed ID: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def purge_on_nil_enabled?(attachment_name, definition)
|
|
59
|
+
return true unless definition
|
|
60
|
+
|
|
61
|
+
relationship_def = find_relationship_definition(attachment_name, definition)
|
|
62
|
+
return true unless relationship_def
|
|
63
|
+
|
|
64
|
+
relationship_def[:options].fetch(:purge_on_nil, true)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def append_only_enabled?(attachment_name, definition)
|
|
68
|
+
return false unless definition
|
|
69
|
+
|
|
70
|
+
relationship_def = find_relationship_definition(attachment_name, definition)
|
|
71
|
+
return false unless relationship_def
|
|
72
|
+
|
|
73
|
+
relationship_def[:options].fetch(:append_only, false)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def find_relationship_definition(attachment_name, definition)
|
|
77
|
+
return nil unless definition.respond_to?(:relationship_definitions)
|
|
78
|
+
|
|
79
|
+
definition.relationship_definitions.find { |r| r[:name].to_s == attachment_name.to_s }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Private helper methods
|
|
83
|
+
def handle_has_many_attachment(attachment, blob_or_blobs, append_only:, attachment_name:, definition:)
|
|
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
|
|
86
|
+
|
|
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)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def handle_has_one_attachment(attachment, blob_or_blobs, attachment_name:, definition:)
|
|
107
|
+
if blob_or_blobs.present?
|
|
108
|
+
attachment.purge if attachment.attached?
|
|
109
|
+
attachment.attach(blob_or_blobs)
|
|
110
|
+
elsif blob_or_blobs.nil?
|
|
111
|
+
attachment.purge if purge_on_nil_enabled?(attachment_name, definition) && attachment.attached?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module ActiveStorage
|
|
5
|
+
module Detection
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def attachment?(association_name, model_class)
|
|
9
|
+
return false unless defined?(::ActiveStorage)
|
|
10
|
+
|
|
11
|
+
model_class.respond_to?(:reflect_on_attachment) &&
|
|
12
|
+
model_class.reflect_on_attachment(association_name.to_sym).present?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def blob_type?(type)
|
|
16
|
+
return false unless defined?(::ActiveStorage)
|
|
17
|
+
|
|
18
|
+
type.to_s == "active_storage_blobs"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def filter_from_includes(includes_hash, current_model_class)
|
|
22
|
+
return {} unless defined?(::ActiveStorage)
|
|
23
|
+
|
|
24
|
+
includes_hash.each_with_object({}) do |(key, value), filtered|
|
|
25
|
+
process_include_entry(key, value, current_model_class, filtered)
|
|
26
|
+
end
|
|
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
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def filter_polymorphic_from_includes(includes_hash, current_model_class)
|
|
48
|
+
includes_hash.each_with_object({}) do |(key, value), filtered|
|
|
49
|
+
process_polymorphic_include(key, value, current_model_class, filtered)
|
|
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?
|
|
63
|
+
|
|
64
|
+
nested = filter_polymorphic_from_includes(value, association.klass)
|
|
65
|
+
nested.any? ? nested : nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module ActiveStorage
|
|
5
|
+
module Serialization
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def serialize_relationship(attachment_name, record)
|
|
9
|
+
return nil unless defined?(::ActiveStorage)
|
|
10
|
+
|
|
11
|
+
attachment = record.public_send(attachment_name)
|
|
12
|
+
return nil unless attachment.respond_to?(:attached?)
|
|
13
|
+
|
|
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) }
|
|
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
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def serialize_blob_identifier(blob)
|
|
30
|
+
{ type: "active_storage_blobs", id: blob.id.to_s }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :default_page_size, :max_page_size, :jsonapi_meta, :authorization_handler,
|
|
6
|
+
:authorization_scope, :document_meta_resolver
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@default_page_size = 25
|
|
10
|
+
@max_page_size = 100
|
|
11
|
+
@jsonapi_meta = nil
|
|
12
|
+
@authorization_handler = nil
|
|
13
|
+
@authorization_scope = nil
|
|
14
|
+
@document_meta_resolver = ->(controller:) { {} } # rubocop:disable Lint/UnusedBlockArgument
|
|
15
|
+
@base_controller_class = "ActionController::API"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def base_controller_class=(value)
|
|
19
|
+
if value.is_a?(Class)
|
|
20
|
+
@base_controller_class = value.name
|
|
21
|
+
elsif value.is_a?(String)
|
|
22
|
+
raise ArgumentError, "base_controller_class cannot be blank" if value.nil? || value.strip.empty?
|
|
23
|
+
|
|
24
|
+
@base_controller_class = value
|
|
25
|
+
else
|
|
26
|
+
raise ArgumentError, "base_controller_class must be a string or class"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
return unless Rails.application&.initialized?
|
|
30
|
+
|
|
31
|
+
JSONAPI.rebuild_base_controllers!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def base_controller_class
|
|
35
|
+
@base_controller_class.constantize
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resolved_base_controller_class
|
|
39
|
+
class_name = @base_controller_class
|
|
40
|
+
raise ArgumentError, "base_controller_class cannot be blank" if class_name.nil? || class_name.empty?
|
|
41
|
+
|
|
42
|
+
class_name.constantize
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def base_controller_overridden?
|
|
46
|
+
@base_controller_class != "ActionController::API"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.configuration
|
|
51
|
+
@configuration ||= Configuration.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.configure
|
|
55
|
+
yield configuration if block_given?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
class BaseController < JSONAPI.configuration.resolved_base_controller_class
|
|
5
|
+
include ControllerHelpers
|
|
6
|
+
include ResourceActions
|
|
7
|
+
|
|
8
|
+
rescue_from JSONAPI::AuthorizationError, with: :render_jsonapi_authorization_error
|
|
9
|
+
rescue_from Pundit::NotAuthorizedError, with: :render_jsonapi_authorization_error if defined?(Pundit)
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def render_jsonapi_authorization_error(error)
|
|
14
|
+
detail = error&.message.presence || "You are not authorized to perform this action"
|
|
15
|
+
render json: {
|
|
16
|
+
errors: [
|
|
17
|
+
{
|
|
18
|
+
status: "403",
|
|
19
|
+
title: "Forbidden",
|
|
20
|
+
detail:,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}, status: :forbidden
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
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
|