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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json_api/support/relationship_guard"
|
|
4
|
+
require_relative "concerns/relationships/serialization"
|
|
5
|
+
require_relative "concerns/relationships/updating"
|
|
6
|
+
require_relative "concerns/relationships/removal"
|
|
7
|
+
require_relative "concerns/relationships/sorting"
|
|
8
|
+
require_relative "concerns/relationships/events"
|
|
9
|
+
require_relative "concerns/relationships/response_helpers"
|
|
10
|
+
|
|
11
|
+
module JSONAPI
|
|
12
|
+
class RelationshipsController < BaseController
|
|
13
|
+
include Relationships::Serialization
|
|
14
|
+
include Relationships::Updating
|
|
15
|
+
include Relationships::Removal
|
|
16
|
+
include Relationships::Sorting
|
|
17
|
+
include Relationships::Events
|
|
18
|
+
include Relationships::ResponseHelpers
|
|
19
|
+
|
|
20
|
+
skip_before_action :validate_resource_type!, only: %i[update destroy]
|
|
21
|
+
skip_before_action :validate_resource_id!, only: %i[update destroy]
|
|
22
|
+
|
|
23
|
+
before_action :set_resource_name
|
|
24
|
+
before_action :set_resource_class
|
|
25
|
+
before_action :set_resource
|
|
26
|
+
before_action :set_relationship_name
|
|
27
|
+
before_action :validate_relationship_exists
|
|
28
|
+
before_action :validate_sort_param, only: [:show]
|
|
29
|
+
skip_before_action :validate_resource_type!, :validate_resource_id!
|
|
30
|
+
|
|
31
|
+
def show
|
|
32
|
+
authorize_resource_action!(@resource, action: :show, context: { relationship: @relationship_name })
|
|
33
|
+
render json: build_show_response, status: :ok
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def update
|
|
37
|
+
authorize_resource_action!(@resource, action: :update, context: { relationship: @relationship_name })
|
|
38
|
+
relationship_data = parse_relationship_data
|
|
39
|
+
update_relationship(relationship_data)
|
|
40
|
+
save_and_render_relationship(relationship_data)
|
|
41
|
+
rescue ArgumentError => e
|
|
42
|
+
render_invalid_relationship_error(e)
|
|
43
|
+
rescue JSONAPI::Exceptions::ParameterNotAllowed => e
|
|
44
|
+
render_parameter_not_allowed_error(e)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def destroy
|
|
48
|
+
authorize_resource_action!(@resource, action: :update, context: { relationship: @relationship_name })
|
|
49
|
+
relationship_data = parse_relationship_data
|
|
50
|
+
return head :no_content if relationship_data.nil?
|
|
51
|
+
|
|
52
|
+
remove_relationship(relationship_data)
|
|
53
|
+
finalize_relationship_removal(relationship_data)
|
|
54
|
+
rescue ArgumentError => e
|
|
55
|
+
render_invalid_relationship_error(e)
|
|
56
|
+
rescue JSONAPI::Exceptions::ParameterNotAllowed => e
|
|
57
|
+
render_parameter_not_allowed_error(e)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def set_relationship_name
|
|
63
|
+
@relationship_name = params[:relationship_name].to_sym
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_relationship_exists
|
|
67
|
+
return if find_relationship_definition
|
|
68
|
+
|
|
69
|
+
render_jsonapi_error(
|
|
70
|
+
status: 404,
|
|
71
|
+
title: "Relationship Not Found",
|
|
72
|
+
detail: "Relationship '#{@relationship_name}' not found on #{@resource_name}",
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def find_relationship_definition
|
|
77
|
+
RelationshipHelpers.find_relationship_definition(@resource_class, @relationship_name)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def parse_relationship_data
|
|
81
|
+
raw_data = params[:data]
|
|
82
|
+
return nil if raw_data.nil?
|
|
83
|
+
return [] if raw_data.is_a?(Array) && raw_data.empty?
|
|
84
|
+
return raw_data.map { |item| ParamHelpers.deep_symbolize_params(item) } if raw_data.is_a?(Array)
|
|
85
|
+
|
|
86
|
+
ParamHelpers.deep_symbolize_params(raw_data)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def polymorphic_association?
|
|
90
|
+
RelationshipHelpers.polymorphic_association?(@resource_class, @relationship_name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ensure_relationship_writable!(association)
|
|
94
|
+
return if active_storage_writable?(association)
|
|
95
|
+
|
|
96
|
+
relationship_def = find_relationship_definition
|
|
97
|
+
readonly = relationship_def && (relationship_def[:options] || {})[:readonly] == true
|
|
98
|
+
|
|
99
|
+
JSONAPI::RelationshipGuard.ensure_writable!(association, @relationship_name, readonly:)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def active_storage_writable?(association)
|
|
103
|
+
self.class.respond_to?(:active_storage_attachment?) &&
|
|
104
|
+
active_storage_attachment?(@relationship_name, @resource.class) &&
|
|
105
|
+
association.nil?
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Errors
|
|
5
|
+
class ParameterNotAllowed < JSONAPI::Error
|
|
6
|
+
attr_reader :params
|
|
7
|
+
|
|
8
|
+
def initialize(params = [])
|
|
9
|
+
@params = params
|
|
10
|
+
super("Parameter not allowed: #{Array(params).join(", ")}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Backward compatibility alias
|
|
16
|
+
module Exceptions
|
|
17
|
+
ParameterNotAllowed = Errors::ParameterNotAllowed
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
|
|
5
|
+
module JSONAPI
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
config.before_initialize do
|
|
8
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
9
|
+
inflect.acronym "JSON"
|
|
10
|
+
inflect.acronym "API"
|
|
11
|
+
# Ensure json_api converts to JSONAPI
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "json_api.mime_type" do |_app|
|
|
16
|
+
Mime::Type.register "application/vnd.api+json", :jsonapi
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
initializer "json_api.routes" do |_app|
|
|
20
|
+
require "json_api/routing"
|
|
21
|
+
ActionDispatch::Routing::Mapper.include JSONAPI::Routing
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Removed eager_load_namespaces registration - JSONAPI module doesn't implement eager_load!
|
|
25
|
+
# Controllers and resources are autoloaded via Zeitwerk
|
|
26
|
+
|
|
27
|
+
initializer "json_api.parameter_parsing", after: "action_dispatch.configure" do |_app|
|
|
28
|
+
ActionDispatch::Request.parameter_parsers[:jsonapi] = lambda do |raw_post|
|
|
29
|
+
ActiveSupport::JSON.decode(raw_post)
|
|
30
|
+
rescue JSON::ParserError => e
|
|
31
|
+
raise ActionDispatch::Http::Parameters::ParseError, e.message
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
initializer "json_api.controllers.include_base", before: :add_routing_paths do |_app|
|
|
36
|
+
mixin = lambda do |base|
|
|
37
|
+
next unless JSONAPI.configuration.base_controller_overridden?
|
|
38
|
+
|
|
39
|
+
# Use class name comparison instead of object equality to avoid timing issues
|
|
40
|
+
# when ApplicationController loads before JSONAPIController
|
|
41
|
+
expected_class_name = JSONAPI.configuration.instance_variable_get(:@base_controller_class)
|
|
42
|
+
next unless base.name == expected_class_name
|
|
43
|
+
|
|
44
|
+
base.include(JSONAPI::ControllerHelpers)
|
|
45
|
+
base.include(JSONAPI::ResourceActions)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
ActiveSupport.on_load(:action_controller_base, &mixin)
|
|
49
|
+
ActiveSupport.on_load(:action_controller_api, &mixin)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Inject JSON:API concerns into the configured base controller class only when it's been overridden
|
|
53
|
+
# This allows empty controllers inheriting from ApplicationController to work automatically
|
|
54
|
+
# We don't mix into ActionController::API by default to avoid Rails filtering action methods
|
|
55
|
+
# (Rails treats public methods on abstract base classes as internal methods)
|
|
56
|
+
#
|
|
57
|
+
# We use after_initialize because:
|
|
58
|
+
# 1. App controllers (e.g., JSONAPIController) must be autoloaded first
|
|
59
|
+
# 2. Rails 8 freezes autoload paths during initialization, and config.to_prepare runs
|
|
60
|
+
# before app controllers are available, causing FrozenError or NameError
|
|
61
|
+
# 3. We register with reloader.to_prepare for code reloading in development
|
|
62
|
+
config.after_initialize do |app|
|
|
63
|
+
# Register for code reloading in development
|
|
64
|
+
app.reloader.to_prepare do
|
|
65
|
+
Railtie.setup_base_controllers
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Run once immediately after initialization
|
|
69
|
+
# In eager_load environments (production, test with config.eager_load = true),
|
|
70
|
+
# controllers are already loaded. In development, this triggers autoloading
|
|
71
|
+
# which is now safe since initialization is complete.
|
|
72
|
+
Railtie.setup_base_controllers
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
def setup_base_controllers
|
|
77
|
+
return unless JSONAPI.configuration.base_controller_overridden?
|
|
78
|
+
|
|
79
|
+
base_controller_class = resolve_base_controller_class
|
|
80
|
+
return unless base_controller_class
|
|
81
|
+
|
|
82
|
+
rebuild_controllers_if_needed(base_controller_class)
|
|
83
|
+
include_jsonapi_concerns(base_controller_class)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def resolve_base_controller_class
|
|
89
|
+
class_name = JSONAPI.configuration.instance_variable_get(:@base_controller_class)
|
|
90
|
+
class_name.constantize
|
|
91
|
+
rescue NameError
|
|
92
|
+
# Controller not yet loaded - this can happen in test environments
|
|
93
|
+
# where eager_load is false and autoloading hasn't run yet.
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def rebuild_controllers_if_needed(base_controller_class)
|
|
98
|
+
return if JSONAPI::BaseController.superclass == base_controller_class
|
|
99
|
+
|
|
100
|
+
JSONAPI.send(:remove_const, :BaseController)
|
|
101
|
+
load "json_api/controllers/base_controller.rb"
|
|
102
|
+
JSONAPI.send(:remove_const, :RelationshipsController)
|
|
103
|
+
load "json_api/controllers/relationships_controller.rb"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def include_jsonapi_concerns(base_controller_class)
|
|
107
|
+
base_controller_class.include(JSONAPI::ControllerHelpers)
|
|
108
|
+
base_controller_class.include(JSONAPI::ResourceActions)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
class ActiveStorageBlobResource < Resource
|
|
5
|
+
attributes :filename, :content_type, :byte_size, :checksum, :url
|
|
6
|
+
|
|
7
|
+
def self.model_class
|
|
8
|
+
::ActiveStorage::Blob
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def url
|
|
12
|
+
return nil unless resource.persisted?
|
|
13
|
+
|
|
14
|
+
Rails.application.routes.url_helpers.rails_blob_path(resource, only_path: true)
|
|
15
|
+
rescue StandardError
|
|
16
|
+
"/rails/active_storage/blobs/#{resource.signed_id}/#{resource.filename}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module AttributesDsl
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def attributes(*attrs)
|
|
10
|
+
@attributes ||= []
|
|
11
|
+
@attributes.concat(attrs.map(&:to_sym))
|
|
12
|
+
@attributes.uniq!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def creatable_fields(*fields)
|
|
16
|
+
@creatable_fields ||= []
|
|
17
|
+
@creatable_fields.concat(fields.map(&:to_sym))
|
|
18
|
+
@creatable_fields.uniq!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def updatable_fields(*fields)
|
|
22
|
+
@updatable_fields ||= []
|
|
23
|
+
@updatable_fields.concat(fields.map(&:to_sym))
|
|
24
|
+
@updatable_fields.uniq!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module FieldResolution
|
|
29
|
+
def permitted_attributes
|
|
30
|
+
declared_attributes = instance_variable_defined?(:@attributes)
|
|
31
|
+
attrs = @attributes || []
|
|
32
|
+
attrs = superclass.permitted_attributes + attrs if should_inherit_attributes?(declared_attributes)
|
|
33
|
+
attrs.uniq
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def permitted_creatable_fields
|
|
37
|
+
resolve_field_list(:@creatable_fields, :permitted_creatable_fields)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def permitted_updatable_fields
|
|
41
|
+
resolve_field_list(:@updatable_fields, :permitted_updatable_fields)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_field_list(ivar, method)
|
|
45
|
+
return (instance_variable_get(ivar) || []).uniq if instance_variable_defined?(ivar)
|
|
46
|
+
return superclass.public_send(method).uniq if inherits_field?(ivar, method)
|
|
47
|
+
|
|
48
|
+
permitted_attributes.uniq
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def inherits_field?(ivar, method)
|
|
52
|
+
superclass != JSONAPI::Resource &&
|
|
53
|
+
superclass.respond_to?(method) &&
|
|
54
|
+
superclass.instance_variable_defined?(ivar)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def should_inherit_attributes?(declared_attributes)
|
|
58
|
+
!declared_attributes &&
|
|
59
|
+
superclass != JSONAPI::Resource &&
|
|
60
|
+
superclass.respond_to?(:permitted_attributes)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
included do
|
|
65
|
+
extend FieldResolution
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module FiltersDsl
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def filters(*filter_names)
|
|
10
|
+
@filters ||= []
|
|
11
|
+
@filters.concat(filter_names.map(&:to_sym))
|
|
12
|
+
@filters.uniq!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def permitted_filters_through
|
|
16
|
+
relationship_names
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def permitted_filters
|
|
20
|
+
declared_filters = instance_variable_defined?(:@filters)
|
|
21
|
+
filter_list = @filters || []
|
|
22
|
+
if !declared_filters &&
|
|
23
|
+
superclass != JSONAPI::Resource &&
|
|
24
|
+
superclass.respond_to?(:permitted_filters)
|
|
25
|
+
filter_list = superclass.permitted_filters + filter_list
|
|
26
|
+
end
|
|
27
|
+
filter_list.uniq
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module MetaDsl
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def meta(hash = nil, &block)
|
|
10
|
+
@meta = hash || block
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resource_meta
|
|
14
|
+
if instance_variable_defined?(:@meta)
|
|
15
|
+
@meta
|
|
16
|
+
elsif superclass != JSONAPI::Resource && superclass.respond_to?(:resource_meta)
|
|
17
|
+
superclass.resource_meta
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module ModelClassHelpers
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def resource_for_model(model_class)
|
|
10
|
+
resource_const = "#{model_class.name}Resource"
|
|
11
|
+
resource_const.safe_constantize if resource_const.respond_to?(:safe_constantize) || defined?(ActiveSupport)
|
|
12
|
+
rescue NameError
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def model_class
|
|
17
|
+
name.sub(/Resource$/, "").classify.constantize
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def safe_model_class
|
|
21
|
+
return nil unless respond_to?(:name) && name
|
|
22
|
+
return nil unless defined?(ActiveSupport)
|
|
23
|
+
|
|
24
|
+
name.sub(/Resource$/, "").classify.safe_constantize
|
|
25
|
+
rescue NoMethodError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reflection_model_class
|
|
30
|
+
model_class
|
|
31
|
+
rescue StandardError
|
|
32
|
+
safe_model_class
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module RelationshipsDsl
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def has_one(name, meta: nil, **options)
|
|
10
|
+
@relationships ||= []
|
|
11
|
+
detect_polymorphic(name, options)
|
|
12
|
+
@relationships << { name: name.to_sym, type: :has_one, meta:, options: }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def has_many(name, meta: nil, **options)
|
|
16
|
+
@relationships ||= []
|
|
17
|
+
validate_append_only_options!(options)
|
|
18
|
+
detect_polymorphic(name, options)
|
|
19
|
+
@relationships << { name: name.to_sym, type: :has_many, meta:, options: }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def belongs_to(name, meta: nil, **options)
|
|
23
|
+
@relationships ||= []
|
|
24
|
+
detect_polymorphic(name, options)
|
|
25
|
+
@relationships << { name: name.to_sym, type: :belongs_to, meta:, options: }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def relationship_definitions
|
|
29
|
+
declared_relationships = instance_variable_defined?(:@relationships)
|
|
30
|
+
rels = @relationships || []
|
|
31
|
+
rels = superclass.relationship_definitions + rels if should_inherit_relationships?(declared_relationships)
|
|
32
|
+
rels.uniq { |r| r[:name] }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def relationship_names
|
|
36
|
+
relationship_definitions.map { |r| r[:name] }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module RelationshipHelperMethods
|
|
41
|
+
def validate_append_only_options!(options)
|
|
42
|
+
if options[:append_only] && options[:purge_on_nil] == true
|
|
43
|
+
raise ArgumentError, "Cannot use append_only: true with purge_on_nil: true"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
options[:purge_on_nil] = false if options[:append_only] && !options.key?(:purge_on_nil)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def detect_polymorphic(name, options)
|
|
50
|
+
return if options.key?(:polymorphic)
|
|
51
|
+
|
|
52
|
+
model_klass = reflection_model_class
|
|
53
|
+
return unless model_klass.respond_to?(:reflect_on_association)
|
|
54
|
+
|
|
55
|
+
reflection = model_klass.reflect_on_association(name)
|
|
56
|
+
options[:polymorphic] = reflection&.polymorphic?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def should_inherit_relationships?(declared_relationships)
|
|
60
|
+
!declared_relationships &&
|
|
61
|
+
superclass != JSONAPI::Resource &&
|
|
62
|
+
superclass.respond_to?(:relationship_definitions)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
included do
|
|
67
|
+
extend RelationshipHelperMethods
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module SortableFieldsDsl
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def sortable_fields(*field_names)
|
|
10
|
+
@sortable_fields ||= []
|
|
11
|
+
@sortable_fields.concat(field_names.map(&:to_sym))
|
|
12
|
+
@sortable_fields.uniq!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def permitted_sortable_fields
|
|
16
|
+
sort_fields = @sortable_fields || []
|
|
17
|
+
sort_fields = inherited_sort_only_fields + sort_fields if should_inherit_sortable_fields?
|
|
18
|
+
(permitted_attributes + sort_fields).uniq
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def should_inherit_sortable_fields?
|
|
22
|
+
!instance_variable_defined?(:@sortable_fields) &&
|
|
23
|
+
!instance_variable_defined?(:@attributes) &&
|
|
24
|
+
superclass != JSONAPI::Resource &&
|
|
25
|
+
superclass.respond_to?(:permitted_sortable_fields)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def inherited_sort_only_fields
|
|
29
|
+
parent_sort_fields = superclass.permitted_sortable_fields
|
|
30
|
+
parent_attributes = superclass.permitted_attributes
|
|
31
|
+
parent_sort_fields - parent_attributes
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "concerns/attributes_dsl"
|
|
4
|
+
require_relative "concerns/filters_dsl"
|
|
5
|
+
require_relative "concerns/sortable_fields_dsl"
|
|
6
|
+
require_relative "concerns/relationships_dsl"
|
|
7
|
+
require_relative "concerns/meta_dsl"
|
|
8
|
+
require_relative "concerns/model_class_helpers"
|
|
9
|
+
|
|
10
|
+
module JSONAPI
|
|
11
|
+
class Resource
|
|
12
|
+
include Resources::AttributesDsl
|
|
13
|
+
include Resources::FiltersDsl
|
|
14
|
+
include Resources::SortableFieldsDsl
|
|
15
|
+
include Resources::RelationshipsDsl
|
|
16
|
+
include Resources::MetaDsl
|
|
17
|
+
include Resources::ModelClassHelpers
|
|
18
|
+
|
|
19
|
+
def initialize(record = nil, context = {})
|
|
20
|
+
@record = record
|
|
21
|
+
@context = context
|
|
22
|
+
@transformed_params = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :record
|
|
26
|
+
alias resource record
|
|
27
|
+
|
|
28
|
+
def transformed_params
|
|
29
|
+
@transformed_params || {}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
class ResourceLoader
|
|
5
|
+
class MissingResourceClass < JSONAPI::Error
|
|
6
|
+
def initialize(resource_type)
|
|
7
|
+
super("Resource class for '#{resource_type}' not found. Define #{resource_type.singularize.classify}Resource < JSONAPI::Resource")
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.find(resource_type)
|
|
12
|
+
resource_class_name = "#{resource_type.singularize.classify}Resource"
|
|
13
|
+
resource_class_name.constantize
|
|
14
|
+
rescue NameError
|
|
15
|
+
raise MissingResourceClass, resource_type
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.find_for_model(model_class)
|
|
19
|
+
# Handle ActiveStorage::Blob specially
|
|
20
|
+
return ActiveStorageBlobResource if defined?(::ActiveStorage) && model_class == ::ActiveStorage::Blob
|
|
21
|
+
|
|
22
|
+
# For STI subclasses, try the specific subclass resource first
|
|
23
|
+
resource_type = model_class.name.underscore.pluralize
|
|
24
|
+
begin
|
|
25
|
+
find(resource_type)
|
|
26
|
+
rescue MissingResourceClass
|
|
27
|
+
# For STI subclasses, fall back to base class resource
|
|
28
|
+
raise unless model_class.respond_to?(:base_class) && model_class.base_class != model_class
|
|
29
|
+
|
|
30
|
+
base_resource_type = model_class.base_class.name.underscore.pluralize
|
|
31
|
+
find(base_resource_type)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Routing
|
|
5
|
+
def jsonapi_resources(resource, controller: nil, defaults: {}, sti: false, **options, &)
|
|
6
|
+
resource_name = resource.to_s
|
|
7
|
+
controller = detect_controller(resource_name) if controller.nil?
|
|
8
|
+
|
|
9
|
+
JSONAPI::ResourceLoader.find(resource_name)
|
|
10
|
+
defaults = defaults.merge(format: :jsonapi, resource_type: resource_name)
|
|
11
|
+
options[:only] = :index if sti
|
|
12
|
+
|
|
13
|
+
define_resource_routes(resource, controller, defaults, options, &)
|
|
14
|
+
define_sti_routes(resource, resource_name, defaults, sti)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def detect_controller(resource_name)
|
|
20
|
+
potential_controller_name = build_controller_name(resource_name)
|
|
21
|
+
potential_controller_name.constantize
|
|
22
|
+
nil
|
|
23
|
+
rescue NameError
|
|
24
|
+
"json_api/resources"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build_controller_name(resource_name)
|
|
28
|
+
scoped_module = @scope[:module]
|
|
29
|
+
base_name = "#{resource_name.pluralize.camelize}Controller"
|
|
30
|
+
return base_name unless scoped_module
|
|
31
|
+
|
|
32
|
+
"#{scoped_module.to_s.camelize}::#{base_name}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def define_resource_routes(resource, controller, defaults, options, &block)
|
|
36
|
+
resources(resource, controller:, defaults:, **options) do
|
|
37
|
+
define_relationship_routes
|
|
38
|
+
instance_eval(&block) if block
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def define_relationship_routes
|
|
43
|
+
member do
|
|
44
|
+
get "relationships/:relationship_name", to: "json_api/relationships#show", as: :relationship
|
|
45
|
+
patch "relationships/:relationship_name", to: "json_api/relationships#update"
|
|
46
|
+
delete "relationships/:relationship_name", to: "json_api/relationships#destroy"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def define_sti_routes(resource, resource_name, defaults, sti)
|
|
51
|
+
return unless sti
|
|
52
|
+
|
|
53
|
+
if sti.is_a?(Array)
|
|
54
|
+
define_explicit_sti_routes(sti,
|
|
55
|
+
defaults,)
|
|
56
|
+
else
|
|
57
|
+
define_auto_sti_routes(resource, resource_name,
|
|
58
|
+
defaults,)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def define_explicit_sti_routes(sti_resources, defaults)
|
|
63
|
+
sti_resources.each { |sub_resource_name| jsonapi_resources(sub_resource_name, defaults:) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def define_auto_sti_routes(resource, resource_name, defaults)
|
|
67
|
+
resource_class = JSONAPI::ResourceLoader.find(resource_name)
|
|
68
|
+
model_class = resource_class.model_class
|
|
69
|
+
return unless model_class.respond_to?(:descendants)
|
|
70
|
+
|
|
71
|
+
model_class.descendants.each do |subclass|
|
|
72
|
+
sub_resource_name = subclass.name.underscore.pluralize.to_sym
|
|
73
|
+
next if sub_resource_name == resource.to_sym
|
|
74
|
+
|
|
75
|
+
jsonapi_resources(sub_resource_name, defaults:)
|
|
76
|
+
end
|
|
77
|
+
rescue NameError, JSONAPI::ResourceLoader::MissingResourceClass
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|