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