lib/fluxo/operation.rb in fluxo-0.2.1 vs lib/fluxo/operation.rb in fluxo-0.3.0
- old
+ new
@@ -1,35 +1,30 @@
# frozen_string_literal: true
-require_relative "operation/constructor"
require_relative "operation/attributes"
module Fluxo
# I know that the underline instance method name is not the best, but I don't want to
# conflict with the Operation step methods that are going to inherit this class.
class Operation
include Attributes
- include Constructor
- def_Operation(::Fluxo)
-
class << self
def flow(*methods)
define_method(:call!) { |**attrs| __execute_flow__(steps: methods, attributes: attrs) }
end
def call(**attrs)
instance = new
begin
instance.__execute_flow__(steps: [:call!], attributes: attrs)
- rescue InvalidResultError, AttributeError, ValidationDefinitionError => e
- raise e
rescue => e
- Fluxo::Result.new(type: :exception, value: e, operation: instance, ids: %i[error]).tap do |result|
- Fluxo.config.error_handlers.each { |handler| handler.call(result) }
- end
+ result = Fluxo::Result.new(type: :exception, value: e, operation: instance, ids: %i[error])
+ Fluxo.config.error_handlers.each { |handler| handler.call(result) }
+ Fluxo::Errors.raise_operation_error!(result)
+ result
end
end
end
def call!(**)
@@ -39,23 +34,25 @@
ERROR
end
# Calls step-method by step-method always passing the value to the next step
# If one of the methods is a failure stop the execution and return a result.
- def __execute_flow__(steps: [], attributes: {})
+ def __execute_flow__(steps: [], attributes: {}, validate: true)
transient_attributes, transient_ids = attributes.dup, Hash.new { |h, k| h[k] = [] }
- __validate_attributes__(first_step: steps.first, attributes: transient_attributes)
result = nil
- steps.unshift(:__validate__) if self.class.validations_proxy # add validate step before the first step
+ steps.unshift(:__validate_required_attributes__) if self.class.required_attributes.any? && validate
+ steps.unshift(:__validate__) if self.class.validations_proxy && validate
steps.each_with_index do |step, idx|
if step.is_a?(Hash)
- step.each do |group_method, group_steps|
+ group_result = step.each do |group_method, group_steps|
send(group_method, **transient_attributes) do |group_attrs|
- result = __execute_flow__(steps: group_steps, attributes: (group_attrs || transient_attributes))
+ result = __execute_flow__(validate: false, steps: group_steps, attributes: (group_attrs || transient_attributes))
end
+ result = group_result if group_result.is_a?(Fluxo::Result)
break unless result.success?
+ transient_attributes = result.transient_attributes # Update transient attributes with the group result in case of value is not a Hash
end
else
result = __wrap_result__(send(step, **transient_attributes))
transient_ids[result.type].push(*result.ids)
end
@@ -67,10 +64,11 @@
new_attributes: result.value,
old_attributes: transient_attributes,
next_step: steps[idx + 1]
)
end
+ result.mutate(transient_attributes: transient_attributes)
end
result.mutate(ids: transient_ids[result.type].uniq, operation: self)
end
# @param value_or_result_id [Any] The value for the result or the id when the result comes from block
@@ -101,77 +99,42 @@
Success(:void) { nil }
end
private
- # Validates the operation was called with all the required keyword arguments.
- # @param first_step [Symbol, Hash] The first step method
- # @param attributes [Hash] The attributes to validate
- # @return [void]
- # @raise [MissingAttributeError] When a required attribute is missing
- def __validate_attributes__(attributes:, first_step:)
- if self.class.strict_attributes? && (extra = attributes.keys - self.class.attribute_names).any?
- raise NotDefinedAttributeError, <<~ERROR
- The following attributes are not defined: #{extra.join(", ")}
-
- You can use the #{self.class.name}.attributes method to specify list of allowed attributes.
- Or you can disable strict attributes mode by setting the strict_attributes to true.
- ERROR
- end
-
- __expand_step_method__(first_step).each do |step|
- method(step).parameters.select { |type, _| type == :keyreq }.each do |(_type, name)|
- raise(MissingAttributeError, "Missing :#{name} attribute on #{self.class.name}#{step} step method.") unless attributes.key?(name)
- end
- end
- end
-
# Merge the result attributes with the new attributes. Also checks if the upcomming step
# has the required attributes and transient attributes to a valid execution.
# @param new_attributes [Hash] The new attributes
# @param old_attributes [Hash] The old attributes
# @param next_step [Symbol, Hash] The next step method
def __merge_result_attributes__(new_attributes:, old_attributes:, next_step:)
return old_attributes unless new_attributes.is_a?(Hash)
- attributes = old_attributes.merge(new_attributes)
- allowed_attrs = self.class.attribute_names + self.class.transient_attribute_names
- if self.class.strict_transient_attributes? &&
- (extra = attributes.keys - allowed_attrs).any?
- raise NotDefinedAttributeError, <<~ERROR
- The following transient attributes are not defined: #{extra.join(", ")}
-
- You can use the #{self.class.name}.transient_attributes method to specify list of allowed attributes.
- Or you can disable strict transient attributes mode by setting the strict_transient_attributes to true.
- ERROR
- end
-
- __expand_step_method__(next_step).each do |step|
- method(step).parameters.select { |type, _| type == :keyreq }.each do |(_type, name)|
- raise(MissingAttributeError, "Missing :#{name} transient attribute on #{self.class.name}##{step} step method.") unless attributes.key?(name)
- end
- end
-
- attributes
+ old_attributes.merge(new_attributes.select { |k, _| k.is_a?(Symbol) })
end
- # Return the step method as an array. When it's a hash it suppose to be a
- # be a step group. In this case return its first key and its first value as
- # the array of step methods.
- #
- # @param step [Symbol, Hash] The step method name
- def __expand_step_method__(step)
- return [step] unless step.is_a?(Hash)
-
- key, value = step.first
- [key, Array(value).first].compact
- end
-
# Execute active_model validation as a flow step.
# @param attributes [Hash] The attributes to validate
# @return [Fluxo::Result] The result of the validation
def __validate__(**attributes)
self.class.validations_proxy.validate!(self, **attributes)
+ end
+
+ # Validates the operation was called with all the required keyword arguments.
+ # @param attributes [Hash] The attributes to validate
+ # @return [Fluxo::Result] The result of the validation
+ # @raise [ArgumentError] When a required attribute is missing
+ def __validate_required_attributes__(**attributes)
+ missing = self.class.required_attributes - attributes.keys
+ return Success(:required_attributes) { nil } if missing.none?
+
+ if self.class.strict?
+ raise ArgumentError, "Missing required attributes: #{missing.join(", ")}"
+ else
+ Failure(:required_attributes) do
+ {error: "Missing required attributes: #{missing.join(", ")}"}
+ end
+ end
end
# Wrap the step method result in a Fluxo::Result object.
#
# @param result [Fluxo::Result, *Object] The object to wrap