lib/fluxo/operation.rb in fluxo-0.1.0 vs lib/fluxo/operation.rb in fluxo-0.2.0

- old
+ new

@@ -2,10 +2,12 @@ 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) @@ -38,28 +40,39 @@ 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: {}) - transient_attributes = attributes.dup + transient_attributes, transient_ids = attributes.dup, {ok: [], failure: [], exception: []} __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.each_with_index do |step, idx| - result = __wrap_result__(send(step, **transient_attributes)) + if step.is_a?(Hash) + 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)) + end + break unless result.success? + end + else + result = __wrap_result__(send(step, **transient_attributes)) + transient_ids.fetch(result.type).push(*result.ids) + end + break unless result.success? if steps[idx + 1] transient_attributes = __merge_result_attributes__( new_attributes: result.value, old_attributes: transient_attributes, next_step: steps[idx + 1] ) end end - result + result.tap { |r| r.ids = transient_ids.fetch(r.type).uniq } end # @param value_or_result_id [Any] The value for the result or the id when the result comes from block def Success(value_or_result_id = nil) attrs = {type: :ok, operation: self} @@ -88,50 +101,85 @@ 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.sloppy_attributes? && (extra = attributes.keys - self.class.attribute_names).any? + 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 sloppy_attributes to true. + Or you can disable strict attributes mode by setting the strict_attributes to true. ERROR end - method(first_step).parameters.select { |type, _| type == :keyreq }.each do |(_type, name)| - raise(MissingAttributeError, "Missing :#{name} attribute on #{self.class.name}#{first_step} step method.") unless attributes.key?(name) + __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.sloppy_transient_attributes? && + 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 sloppy_transient_attributes to true. + Or you can disable strict transient attributes mode by setting the strict_transient_attributes to true. ERROR end - method(next_step).parameters.select { |type, _| type == :keyreq }.each do |(_type, name)| - raise(MissingAttributeError, "Missing :#{name} transient attribute on #{self.class.name}##{next_step} step method.") unless attributes.key?(name) + __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 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 + # Wrap the step method result in a Fluxo::Result object. + # + # @param result [Fluxo::Result, *Object] The object to wrap + # @raise [Fluxo::InvalidResultError] When the result is not a Fluxo::Result config + # is set to not wrap results. + # @return [Fluxo::Result] The wrapped result def __wrap_result__(result) if result.is_a?(Fluxo::Result) return result elsif Fluxo.config.wrap_falsey_result && !result return Failure(:falsey) { result }