cuprum-rails 0.1.0 → 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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -0
  3. data/DEVELOPMENT.md +20 -0
  4. data/README.md +356 -63
  5. data/lib/cuprum/rails/action.rb +32 -16
  6. data/lib/cuprum/rails/actions/create.rb +62 -15
  7. data/lib/cuprum/rails/actions/destroy.rb +23 -7
  8. data/lib/cuprum/rails/actions/edit.rb +23 -7
  9. data/lib/cuprum/rails/actions/index.rb +30 -10
  10. data/lib/cuprum/rails/actions/middleware/associations/cache.rb +112 -0
  11. data/lib/cuprum/rails/actions/middleware/associations/find.rb +23 -0
  12. data/lib/cuprum/rails/actions/middleware/associations/parent.rb +70 -0
  13. data/lib/cuprum/rails/actions/middleware/associations/query.rb +140 -0
  14. data/lib/cuprum/rails/actions/middleware/associations.rb +12 -0
  15. data/lib/cuprum/rails/actions/middleware/log_request.rb +126 -0
  16. data/lib/cuprum/rails/actions/middleware/log_result.rb +51 -0
  17. data/lib/cuprum/rails/actions/middleware/resources/find.rb +44 -0
  18. data/lib/cuprum/rails/actions/middleware/resources/query.rb +91 -0
  19. data/lib/cuprum/rails/actions/middleware/resources.rb +11 -0
  20. data/lib/cuprum/rails/actions/middleware.rb +13 -0
  21. data/lib/cuprum/rails/actions/new.rb +16 -4
  22. data/lib/cuprum/rails/actions/parameter_validation.rb +60 -0
  23. data/lib/cuprum/rails/actions/resource_action.rb +119 -42
  24. data/lib/cuprum/rails/actions/show.rb +23 -7
  25. data/lib/cuprum/rails/actions/update.rb +70 -22
  26. data/lib/cuprum/rails/actions.rb +11 -7
  27. data/lib/cuprum/rails/collection.rb +27 -47
  28. data/lib/cuprum/rails/command.rb +3 -1
  29. data/lib/cuprum/rails/commands/destroy_one.rb +10 -6
  30. data/lib/cuprum/rails/commands/find_many.rb +8 -1
  31. data/lib/cuprum/rails/commands/find_matching.rb +1 -1
  32. data/lib/cuprum/rails/commands/find_one.rb +8 -0
  33. data/lib/cuprum/rails/commands/insert_one.rb +17 -6
  34. data/lib/cuprum/rails/commands/update_one.rb +16 -5
  35. data/lib/cuprum/rails/constraints/parameters_contract.rb +14 -0
  36. data/lib/cuprum/rails/constraints.rb +10 -0
  37. data/lib/cuprum/rails/controller.rb +12 -2
  38. data/lib/cuprum/rails/controllers/action.rb +100 -0
  39. data/lib/cuprum/rails/controllers/class_methods/actions.rb +33 -7
  40. data/lib/cuprum/rails/controllers/class_methods/configuration.rb +36 -0
  41. data/lib/cuprum/rails/controllers/class_methods/middleware.rb +88 -0
  42. data/lib/cuprum/rails/controllers/class_methods/validations.rb +2 -2
  43. data/lib/cuprum/rails/controllers/configuration.rb +41 -1
  44. data/lib/cuprum/rails/controllers/middleware.rb +59 -0
  45. data/lib/cuprum/rails/controllers.rb +2 -0
  46. data/lib/cuprum/rails/errors/invalid_parameters.rb +55 -0
  47. data/lib/cuprum/rails/errors/invalid_statement.rb +11 -0
  48. data/lib/cuprum/rails/errors/missing_parameter.rb +42 -0
  49. data/lib/cuprum/rails/errors/resource_error.rb +46 -0
  50. data/lib/cuprum/rails/errors.rb +6 -1
  51. data/lib/cuprum/rails/map_errors.rb +29 -1
  52. data/lib/cuprum/rails/query.rb +1 -1
  53. data/lib/cuprum/rails/repository.rb +12 -25
  54. data/lib/cuprum/rails/request.rb +149 -60
  55. data/lib/cuprum/rails/resource.rb +119 -85
  56. data/lib/cuprum/rails/responders/base_responder.rb +78 -0
  57. data/lib/cuprum/rails/responders/html/plural_resource.rb +9 -39
  58. data/lib/cuprum/rails/responders/html/rendering.rb +81 -0
  59. data/lib/cuprum/rails/responders/html/resource.rb +107 -0
  60. data/lib/cuprum/rails/responders/html/singular_resource.rb +9 -38
  61. data/lib/cuprum/rails/responders/html.rb +2 -0
  62. data/lib/cuprum/rails/responders/html_responder.rb +8 -52
  63. data/lib/cuprum/rails/responders/json/resource.rb +3 -3
  64. data/lib/cuprum/rails/responders/json_responder.rb +31 -16
  65. data/lib/cuprum/rails/responders/matching.rb +29 -27
  66. data/lib/cuprum/rails/responders/serialization.rb +11 -9
  67. data/lib/cuprum/rails/responders.rb +1 -0
  68. data/lib/cuprum/rails/responses/head_response.rb +24 -0
  69. data/lib/cuprum/rails/responses/html/redirect_back_response.rb +55 -0
  70. data/lib/cuprum/rails/responses/html/redirect_response.rb +19 -4
  71. data/lib/cuprum/rails/responses/html/render_response.rb +17 -5
  72. data/lib/cuprum/rails/responses/html.rb +6 -2
  73. data/lib/cuprum/rails/responses.rb +1 -0
  74. data/lib/cuprum/rails/result.rb +36 -0
  75. data/lib/cuprum/rails/routes.rb +36 -23
  76. data/lib/cuprum/rails/rspec/contract_helpers.rb +57 -0
  77. data/lib/cuprum/rails/rspec/contracts/action_contracts.rb +754 -0
  78. data/lib/cuprum/rails/rspec/contracts/actions/create_contracts.rb +289 -0
  79. data/lib/cuprum/rails/rspec/contracts/actions/destroy_contracts.rb +164 -0
  80. data/lib/cuprum/rails/rspec/contracts/actions/edit_contracts.rb +73 -0
  81. data/lib/cuprum/rails/rspec/contracts/actions/index_contracts.rb +108 -0
  82. data/lib/cuprum/rails/rspec/contracts/actions/new_contracts.rb +111 -0
  83. data/lib/cuprum/rails/rspec/contracts/actions/show_contracts.rb +72 -0
  84. data/lib/cuprum/rails/rspec/contracts/actions/update_contracts.rb +263 -0
  85. data/lib/cuprum/rails/rspec/contracts/actions.rb +8 -0
  86. data/lib/cuprum/rails/rspec/contracts/command_contracts.rb +479 -0
  87. data/lib/cuprum/rails/rspec/contracts/responder_contracts.rb +232 -0
  88. data/lib/cuprum/rails/rspec/contracts/routes_contracts.rb +363 -0
  89. data/lib/cuprum/rails/rspec/contracts/serializers_contracts.rb +70 -0
  90. data/lib/cuprum/rails/rspec/contracts.rb +8 -0
  91. data/lib/cuprum/rails/rspec/matchers/be_a_result_matcher.rb +64 -0
  92. data/lib/cuprum/rails/rspec/matchers.rb +41 -0
  93. data/lib/cuprum/rails/serializers/base_serializer.rb +60 -0
  94. data/lib/cuprum/rails/serializers/context.rb +84 -0
  95. data/lib/cuprum/rails/serializers/json/active_record_serializer.rb +2 -2
  96. data/lib/cuprum/rails/serializers/json/array_serializer.rb +9 -8
  97. data/lib/cuprum/rails/serializers/json/attributes_serializer.rb +95 -172
  98. data/lib/cuprum/rails/serializers/json/error_serializer.rb +2 -2
  99. data/lib/cuprum/rails/serializers/json/hash_serializer.rb +9 -8
  100. data/lib/cuprum/rails/serializers/json/identity_serializer.rb +3 -3
  101. data/lib/cuprum/rails/serializers/json/properties_serializer.rb +252 -0
  102. data/lib/cuprum/rails/serializers/json.rb +2 -1
  103. data/lib/cuprum/rails/serializers.rb +3 -1
  104. data/lib/cuprum/rails/version.rb +1 -1
  105. data/lib/cuprum/rails.rb +19 -16
  106. metadata +73 -131
  107. data/lib/cuprum/rails/controller_action.rb +0 -121
  108. data/lib/cuprum/rails/errors/missing_parameters.rb +0 -33
  109. data/lib/cuprum/rails/errors/missing_primary_key.rb +0 -46
  110. data/lib/cuprum/rails/errors/undefined_permitted_attributes.rb +0 -34
  111. data/lib/cuprum/rails/rspec/command_contract.rb +0 -460
  112. data/lib/cuprum/rails/rspec/define_route_contract.rb +0 -84
  113. data/lib/cuprum/rails/serializers/json/serializer.rb +0 -66
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
4
+
3
5
  require 'cuprum/command'
4
6
 
5
7
  require 'cuprum/rails'
@@ -7,37 +9,51 @@ require 'cuprum/rails'
7
9
  module Cuprum::Rails
8
10
  # Abstract command that implement a controller action.
9
11
  class Action < Cuprum::Command
10
- # @param resource [Cuprum::Rails::Resource] The controller resource.
11
- def initialize(resource:)
12
- super()
13
-
14
- @resource = resource
15
- end
12
+ extend Forwardable
16
13
 
17
- # @!method call(request:)
14
+ # @!method call(request:, repository: nil, **options)
18
15
  # Performs the controller action.
19
16
  #
20
17
  # Subclasses should implement a #process method with the :request keyword,
21
18
  # which accepts an ActionDispatch::Request instance.
22
19
  #
23
- # @param request [ActionDispatch::Request] The Rails request.
20
+ # @param request [ActionDispatch::Request] the Rails request.
21
+ # @param repository [Cuprum::Collections::Repository] the repository
22
+ # containing the data collections for the application or scope.
23
+ # @param options [Hash<Symbol, Object>] additional options for the action.
24
24
  #
25
25
  # @return [Cuprum::Result] the result of the action.
26
26
 
27
- # @return [Cuprum::Rails::Resource] the controller resource.
28
- attr_reader :resource
27
+ # @!method params
28
+ # @return [Hash<String, Object>] the request parameters.
29
+ def_delegators :@request, :params
29
30
 
30
- private
31
+ # @return [Hash<Symbol, Object>] additional options for the action.
32
+ attr_reader :options
31
33
 
34
+ # @return [Cuprum::Collections::Repository] the repository containing the
35
+ # data collections for the application or scope.
36
+ attr_reader :repository
37
+
38
+ # @return [Cuprum::Rails::Request] the formatted request.
32
39
  attr_reader :request
33
40
 
34
- def params
35
- @params ||= ActionController::Parameters.new(request.params)
41
+ private
42
+
43
+ def build_result(error: nil, metadata: nil, status: nil, value: nil)
44
+ Cuprum::Rails::Result.new(
45
+ error: error,
46
+ metadata: metadata,
47
+ status: status,
48
+ value: value
49
+ )
36
50
  end
37
51
 
38
- def process(request:)
39
- @params = nil
40
- @request = request
52
+ def process(request:, repository: nil, **options)
53
+ @params = nil
54
+ @repository = repository
55
+ @request = request
56
+ @options = options
41
57
 
42
58
  nil
43
59
  end
@@ -10,21 +10,20 @@ module Cuprum::Rails::Actions
10
10
  class Create < Cuprum::Rails::Actions::ResourceAction
11
11
  private
12
12
 
13
- def create_resource
14
- entity = nil
13
+ attr_reader :entity
15
14
 
16
- result = steps do
17
- attributes = step { resource_params }
18
- entity = step { collection.build_one.call(attributes: attributes) }
15
+ def build_response
16
+ { resource.singular_name => entity }
17
+ end
18
+
19
+ def create_entity(attributes:)
20
+ steps do
21
+ @entity = step { collection.build_one.call(attributes: attributes) }
19
22
 
20
23
  step { collection.validate_one.call(entity: entity) }
21
24
 
22
25
  step { collection.insert_one.call(entity: entity) }
23
-
24
- { singular_resource_name => entity }
25
26
  end
26
-
27
- [entity, result]
28
27
  end
29
28
 
30
29
  def failed_validation?(result)
@@ -32,17 +31,65 @@ module Cuprum::Rails::Actions
32
31
  result.error.is_a?(Cuprum::Collections::Errors::FailedValidation)
33
32
  end
34
33
 
35
- def process(request:)
36
- super
37
-
38
- entity, result = create_resource
34
+ def handle_failed_validation
35
+ result = yield
39
36
 
40
37
  return result unless failed_validation?(result)
41
38
 
42
39
  Cuprum::Result.new(
43
- error: result.error,
40
+ error: scope_validation_errors(result.error),
44
41
  status: :failure,
45
- value: { singular_resource_name => entity }
42
+ value: { resource.singular_name => entity }
43
+ )
44
+ end
45
+
46
+ def parameters_contract
47
+ return @parameters_contract if @parameters_contract
48
+
49
+ resource_name = resource.singular_name
50
+ parameters_constraint = require_parameters_constraint
51
+
52
+ @parameters_contract =
53
+ Cuprum::Rails::Constraints::ParametersContract.new do
54
+ key resource_name, parameters_constraint
55
+ end
56
+ end
57
+
58
+ def perform_action
59
+ handle_failed_validation do
60
+ create_entity(attributes: resource_params)
61
+ end
62
+ end
63
+
64
+ def process(**)
65
+ @entity = nil
66
+
67
+ super
68
+ end
69
+
70
+ def require_parameters_constraint
71
+ Stannum::Contract.new do
72
+ constraint Stannum::Constraints::Presence.new, sanity: true
73
+ constraint Stannum::Constraints::Types::HashType.new
74
+ end
75
+ end
76
+
77
+ def require_permitted_attributes?
78
+ true
79
+ end
80
+
81
+ def scope_validation_errors(error)
82
+ mapped_errors = Stannum::Errors.new
83
+
84
+ error.errors.each do |err|
85
+ mapped_errors
86
+ .dig(resource.singular_name, *err[:path].map(&:to_s))
87
+ .add(err[:type], message: err[:message], **err[:data])
88
+ end
89
+
90
+ Cuprum::Collections::Errors::FailedValidation.new(
91
+ entity_class: error.entity_class,
92
+ errors: mapped_errors
46
93
  )
47
94
  end
48
95
  end
@@ -8,15 +8,31 @@ module Cuprum::Rails::Actions
8
8
  class Destroy < Cuprum::Rails::Actions::ResourceAction
9
9
  private
10
10
 
11
- def process(request:)
12
- super
11
+ attr_reader :entity
12
+
13
+ def build_response
14
+ { resource.singular_name => entity }
15
+ end
16
+
17
+ def destroy_entity(primary_key:)
18
+ collection.destroy_one.call(primary_key: primary_key)
19
+ end
13
20
 
14
- primary_key = step { resource_id }
15
- entity = step do
16
- collection.destroy_one.call(primary_key: primary_key)
17
- end
21
+ def parameters_contract
22
+ @parameters_contract ||=
23
+ Cuprum::Rails::Constraints::ParametersContract.new do
24
+ key 'id', Stannum::Constraints::Presence.new
25
+ end
26
+ end
27
+
28
+ def perform_action
29
+ @entity = step { destroy_entity(primary_key: resource_id) }
30
+ end
18
31
 
19
- { singular_resource_name => entity }
32
+ def process(**)
33
+ @entity = nil
34
+
35
+ super
20
36
  end
21
37
  end
22
38
  end
@@ -8,15 +8,31 @@ module Cuprum::Rails::Actions
8
8
  class Edit < Cuprum::Rails::Actions::ResourceAction
9
9
  private
10
10
 
11
- def process(request:)
12
- super
11
+ attr_reader :entity
12
+
13
+ def build_response
14
+ { resource.singular_name => entity }
15
+ end
16
+
17
+ def find_entity(primary_key:)
18
+ collection.find_one.call(primary_key: primary_key)
19
+ end
13
20
 
14
- primary_key = step { resource_id }
15
- entity = step do
16
- collection.find_one.call(primary_key: primary_key)
17
- end
21
+ def parameters_contract
22
+ @parameters_contract ||=
23
+ Cuprum::Rails::Constraints::ParametersContract.new do
24
+ key 'id', Stannum::Constraints::Presence.new
25
+ end
26
+ end
27
+
28
+ def perform_action
29
+ @entity = step { find_entity(primary_key: resource_id) }
30
+ end
18
31
 
19
- { singular_resource_name => entity }
32
+ def process(**)
33
+ @entity = nil
34
+
35
+ super
20
36
  end
21
37
  end
22
38
  end
@@ -11,12 +11,27 @@ module Cuprum::Rails::Actions
11
11
 
12
12
  private
13
13
 
14
+ attr_reader :entities
15
+
16
+ def build_response
17
+ { resource.name => entities.to_a }
18
+ end
19
+
14
20
  # @note Overload this method to change how the filtering params are defined,
15
21
  # or override the #limit, #offset, #order, #where methods directly.
16
22
  def filter_params
17
23
  tools.hash_tools.convert_keys_to_strings(request.params)
18
24
  end
19
25
 
26
+ def find_entities(limit:, offset:, order:, &block)
27
+ collection.find_matching.call(
28
+ limit: limit,
29
+ offset: offset,
30
+ order: order,
31
+ &block
32
+ )
33
+ end
34
+
20
35
  def limit
21
36
  filter_params['limit']
22
37
  end
@@ -29,19 +44,24 @@ module Cuprum::Rails::Actions
29
44
  filter_params.fetch('order', default_order.presence)
30
45
  end
31
46
 
32
- def process(request:)
33
- super
34
-
47
+ def perform_action
35
48
  filters = where
36
49
  block = where.present? ? -> { filters } : nil
37
50
 
38
- collection.find_matching.call(
39
- envelope: true,
40
- limit: limit,
41
- offset: offset,
42
- order: order,
43
- &block
44
- )
51
+ @entities = step do
52
+ find_entities(
53
+ limit: limit,
54
+ offset: offset,
55
+ order: order,
56
+ &block
57
+ )
58
+ end
59
+ end
60
+
61
+ def process(**)
62
+ @entities = nil
63
+
64
+ super
45
65
  end
46
66
 
47
67
  def tools
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/actions/middleware/associations'
4
+
5
+ module Cuprum::Rails::Actions::Middleware::Associations
6
+ # Pre-warm the association cache for a resource.
7
+ class Cache < Cuprum::Command
8
+ # Strategy for caching an association value on an ActiveRecord model.
9
+ ACTIVE_RECORD_STRATEGY = lambda do |entity:, name:, value:|
10
+ entity.send(:association_instance_set, name, value)
11
+
12
+ entity
13
+ end
14
+
15
+ # Generic strategy for caching an association value.
16
+ DEFAULT_STRATEGY = lambda do |entity:, name:, value:|
17
+ entity[name] = value
18
+
19
+ entity
20
+ end
21
+
22
+ STRATEGIES = {
23
+ Object => DEFAULT_STRATEGY,
24
+ ActiveRecord::Base => ACTIVE_RECORD_STRATEGY
25
+ }.freeze
26
+ private_constant :STRATEGIES
27
+
28
+ class << self
29
+ # Defines a strategy for caching an association value.
30
+ #
31
+ # @param klass [Class] the base class or module for matching entities.
32
+ #
33
+ # @yield the strategy for caching the association value.
34
+ #
35
+ # @yieldparam entity [Object] the base entity.
36
+ # @yieldparam name [String] the name of the association.
37
+ # @yieldparam value [Object] the associated entity to cache.
38
+ #
39
+ # @yieldreturn [Object] the entity with cached assocation value.
40
+ def define_strategy(klass, &block)
41
+ (@strategies ||= STRATEGIES.dup)[klass] = block
42
+ end
43
+
44
+ # @return [Proc] the defined strategies for caching association values.
45
+ def strategies
46
+ (@strategies ||= STRATEGIES.dup).reverse_each
47
+ end
48
+ end
49
+
50
+ # @param association [Cuprum::Associations::Association] the association
51
+ # to cache.
52
+ # @param resource [Cuprum::Rails::Resource] the resource to cache.
53
+ def initialize(association:, resource:)
54
+ super()
55
+
56
+ @association = association
57
+ @resource = resource
58
+ end
59
+
60
+ # @return [Cuprum::Associations::Association] the association to cache.
61
+ attr_reader :association
62
+
63
+ # @return [Cuprum::Rails::Resource] the resource to cache.
64
+ attr_reader :resource
65
+
66
+ private
67
+
68
+ def cache_association(entity:, value:)
69
+ value = value.first if association.singular?
70
+ strategy =
71
+ self.class.strategies.find { |klass, _| entity.is_a?(klass) }.last
72
+
73
+ strategy.call(
74
+ entity: entity,
75
+ name: association.name,
76
+ value: value
77
+ )
78
+ end
79
+
80
+ def convert_to_array(value)
81
+ return [] if value.nil?
82
+
83
+ return value if value.is_a?(Array)
84
+
85
+ [value]
86
+ end
87
+
88
+ def index_values(values:)
89
+ key_name = association.with_inverse(resource).query_key_name
90
+
91
+ values
92
+ .each
93
+ .with_object(Hash.new { |hsh, key| hsh[key] = [] }) do |value, hsh|
94
+ hsh[value[key_name]] << value
95
+
96
+ hsh
97
+ end
98
+ end
99
+
100
+ def process(entities:, values:)
101
+ indexed = index_values(values: convert_to_array(values))
102
+ cached = convert_to_array(entities).map do |entity|
103
+ cache_association(
104
+ entity: entity,
105
+ value: indexed[entity[association.inverse_key_name]]
106
+ )
107
+ end
108
+
109
+ entities.is_a?(Array) ? cached : cached.first
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/actions/middleware/associations'
4
+ require 'cuprum/rails/actions/middleware/associations/query'
5
+
6
+ module Cuprum::Rails::Actions::Middleware::Associations
7
+ # Middleware for querying an association from the action results.
8
+ class Find < Cuprum::Rails::Actions::Middleware::Associations::Query
9
+ private
10
+
11
+ def process(next_command, repository:, request:, resource:, **rest)
12
+ super
13
+
14
+ return result unless result.success?
15
+
16
+ values = step { perform_query }
17
+ entities = entities_from(result: result)
18
+ entities = step { cache_association(entities: entities, values: values) }
19
+
20
+ merge_result(entities: entities, result: result, values: values)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/commands/associations/require_many'
4
+
5
+ require 'cuprum/rails/actions/middleware/associations'
6
+ require 'cuprum/rails/actions/middleware/associations/query'
7
+ require 'cuprum/rails/errors/missing_parameter'
8
+
9
+ module Cuprum::Rails::Actions::Middleware::Associations
10
+ # Middleware for querying a parent association from a parameter.
11
+ class Parent < Cuprum::Rails::Actions::Middleware::Associations::Query
12
+ # @param association_params [Hash] parameters to pass to the association.
13
+ def initialize(**association_params)
14
+ super(
15
+ association_type: :belongs_to,
16
+ **association_params
17
+ )
18
+ end
19
+
20
+ private
21
+
22
+ def cache_entities(result:, values:)
23
+ return unless result.success?
24
+
25
+ entities = entities_from(result: result)
26
+
27
+ cache_association(entities: entities, values: values)
28
+ end
29
+
30
+ def process(next_command, repository:, request:, resource:, **rest)
31
+ @repository = repository
32
+ @request = request
33
+ @resource = resource
34
+
35
+ primary_key = step { query_keys }
36
+ values = step { require_parent(primary_key: primary_key) }
37
+ result = super
38
+ entities = step { cache_entities(result: result, values: values) }
39
+
40
+ merge_result(entities: entities, result: result, values: values)
41
+ end
42
+
43
+ def query_command
44
+ Cuprum::Collections::Commands::Associations::RequireMany.new(
45
+ association: association,
46
+ repository: repository,
47
+ resource: resource
48
+ )
49
+ end
50
+
51
+ def query_keys
52
+ key = "#{association.singular_name}_id"
53
+ value = request.params[key]
54
+
55
+ return value if value.present?
56
+
57
+ error = Cuprum::Rails::Errors::MissingParameter.new(
58
+ parameter_name: key,
59
+ parameters: request.params
60
+ )
61
+ failure(error)
62
+ end
63
+
64
+ def require_parent(primary_key:)
65
+ values = step { perform_query(keys: primary_key) }
66
+
67
+ values.is_a?(Array) ? values.first : values
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/associations/belongs_to'
4
+ require 'cuprum/collections/commands/associations/find_many'
5
+
6
+ require 'cuprum/rails/actions/middleware/associations'
7
+ require 'cuprum/rails/actions/middleware/associations/cache'
8
+ require 'cuprum/rails/result'
9
+
10
+ module Cuprum::Rails::Actions::Middleware::Associations
11
+ # Abstract middleware for performing an association query.
12
+ class Query < Cuprum::Rails::Action
13
+ include Cuprum::Middleware
14
+
15
+ # @param association_type [String, Symbol] the type of association.
16
+ # @param association_params [Hash] parameters to pass to the association.
17
+ def initialize(association_type: nil, **association_params)
18
+ super()
19
+
20
+ @association_type = association_type&.intern
21
+ @association = build_association(**association_params)
22
+ end
23
+
24
+ # @return [Cuprum::Collections::Association] the association.
25
+ attr_reader :association
26
+
27
+ # @return [String, Symbol] the type of association.
28
+ attr_reader :association_type
29
+
30
+ private
31
+
32
+ attr_reader :repository
33
+
34
+ attr_reader :request
35
+
36
+ attr_reader :resource
37
+
38
+ attr_reader :result
39
+
40
+ def association_class
41
+ case association_type
42
+ when :belongs_to
43
+ Cuprum::Collections::Associations::BelongsTo
44
+ else
45
+ Cuprum::Collections::Association
46
+ end
47
+ end
48
+
49
+ def build_association(**params)
50
+ association_class.new(**params)
51
+ end
52
+
53
+ def cache_association(entities:, values:)
54
+ Cuprum::Rails::Actions::Middleware::Associations::Cache
55
+ .new(association: association, resource: resource)
56
+ .call(entities: entities, values: values)
57
+ end
58
+
59
+ def entities_from(result:)
60
+ return unless result.value.is_a?(Hash)
61
+
62
+ if result.value.key?(resource.singular_name)
63
+ return result.value[resource.singular_name]
64
+ end
65
+
66
+ result.value[resource.name]
67
+ end
68
+
69
+ def merge_result(entities:, result:, values:)
70
+ return result unless result.value.is_a?(Hash)
71
+
72
+ Cuprum::Rails::Result.new(
73
+ **result.properties,
74
+ value: merge_values(
75
+ entities: entities,
76
+ value: result.value,
77
+ values: values
78
+ )
79
+ )
80
+ end
81
+
82
+ def merge_values(entities:, value:, values:)
83
+ if entities.present?
84
+ key = pluralize_name(resource: resource, values: entities)
85
+ value = value.merge(key => entities)
86
+ end
87
+
88
+ if values.present?
89
+ key = pluralize_name(resource: association, values: values)
90
+ value = value.merge(key => values)
91
+ end
92
+
93
+ value
94
+ end
95
+
96
+ def pluralize_name(resource:, values:)
97
+ values.is_a?(Array) ? resource.plural_name : resource.singular_name
98
+ end
99
+
100
+ def process(next_command, repository:, request:, resource:, **rest)
101
+ @repository = repository
102
+ @request = request
103
+ @resource = resource
104
+ @result = next_command.call(
105
+ repository: repository,
106
+ request: request,
107
+ resource: resource,
108
+ **rest
109
+ )
110
+ end
111
+
112
+ def perform_query(keys: nil)
113
+ keys ||= step { query_keys }
114
+
115
+ if keys.is_a?(Array)
116
+ query_command.call(keys: keys)
117
+ else
118
+ query_command.call(key: keys)
119
+ end
120
+ end
121
+
122
+ def query_command
123
+ Cuprum::Collections::Commands::Associations::FindMany.new(
124
+ association: association,
125
+ repository: repository,
126
+ resource: resource
127
+ )
128
+ end
129
+
130
+ def query_keys
131
+ entities = entities_from(result: result)
132
+
133
+ if entities.is_a?(Array)
134
+ association.map_entities_to_keys(*entities)
135
+ else
136
+ association.map_entities_to_keys(entities).first
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rails/actions/middleware'
4
+
5
+ module Cuprum::Rails::Actions::Middleware
6
+ # Namespace for association middleware.
7
+ module Associations
8
+ autoload :Cache, 'cuprum/rails/actions/middleware/associations/cache'
9
+ autoload :Parent, 'cuprum/rails/actions/middleware/associations/parent'
10
+ autoload :Query, 'cuprum/rails/actions/middleware/associations/query'
11
+ end
12
+ end