require_dependency 'mixin/rails_logger' require_dependency 'mixin/start_finish_logger' class Sequencer class State include ::Mixin::RailsLogger include ::Mixin::StartFinishLogger def initialize(sequence, parameters: {}, expecting: nil) @index = -1 @units = sequence.units @result_index = @units.count @values = {} initialize_attributes(sequence.units) initialize_parameters(parameters) initialize_expectations(expecting || sequence.expecting) end # Stores a value for the given attribute. Value can be a regular object # or the result of a given code block. # The attribute gets validated against the .provides list of attributes. # In the case than an attribute gets provided that is not declared to # be provided an exception will be raised. # # @param [Symbol] attribute the attribute for which the value gets provided. # @param [Object] value the value that should get stored for the given attribute. # @yield [] executes the given block and takes the result as the value. # @yieldreturn [Object] the value for the given attribute. # # @example # state.provide(:sum, 3) # # @example # state.provide(:sum) do # some_value = rand(100) # some_value * 3 # end # # @raise [RuntimeError] if the attribute is not provideable from the calling Unit # # @return [nil] def provide(attribute, value = nil) if provideable?(attribute) value = yield if block_given? set(attribute, value) else value = "UNEXECUTED BLOCK: #{caller(1..1).first}" if block_given? unprovideable_setter(attribute, value) end end # Returns the value of the given attribute. # The attribute gets validated against the .uses list of attributes. In the # case than an attribute gets used that is not declared to be used # an exception will be raised. # # @param [Symbol] attribute the attribute for which the value is requested. # # @example # state.use(:answer) # #=> 42 # # @raise [RuntimeError] if the attribute is not useable from the calling Unit # # @return [nil] def use(attribute) if useable?(attribute) get(attribute) else unaccessable_getter(attribute) end end # Returns the value of the given attribute. # The attribute DOES NOT get validated against the .uses list of attributes. # # @param [Symbol] attribute the attribute for which the value is requested. # # @example # state.optional(:answer) # #=> 42 # # @example # state.optional(:unknown) # #=> nil # # @return [Object, nil] def optional(attribute) return get(attribute) if @attributes.known?(attribute) logger.debug { "Access to unknown optional attribute '#{attribute}'." } nil end # Checks if a value for the given attribute is provided. # The attribute DOES NOT get validated against the .uses list of attributes. # # @param [Symbol] attribute the attribute which should get checked. # # @example # state.provided?(:answer) # #=> true # # @example # state.provided?(:unknown) # #=> false # # @return [Boolean] def provided?(attribute) optional(attribute) != nil end # Unsets the value for the given attribute. # The attribute gets validated against the .uses list of attributes. # In the case than an attribute gets unset that is not declared # to be used an exception will be raised. # # @param [Symbol] attribute the attribute for which the value gets unset. # # @example # state.unset(:answer) # # @raise [RuntimeError] if the attribute is not useable from the calling Unit # # @return [nil] def unset(attribute) value = nil if useable?(attribute) set(attribute, value) else unprovideable_setter(attribute, value) end end # Handles state processing of the next Unit in the Sequence while executing # the given block. After the Unit is processed the state will get cleaned up # and no longer needed attribute values will get discarded. # # @yield [] executes the given block and handles the state changes before and afterwards. # # @example # state.process do # unit.process # end # # @return [nil] def process @index += 1 yield cleanup end # Handles state processing of the next Unit in the Sequence while executing # the given block. After the Unit is processed the state will get cleaned up # and no longer needed attribute values will get discarded. # # @example # state.to_h # #=> {"ssl_verify"=>true, "host_url"=>"ldaps://192...", ...} # # @return [Hash{Symbol => Object}] def to_h available.map { |identifier| [identifier, @values[identifier]] }.to_h end private def available @attributes.select do |_identifier, attribute| @index.between?(attribute.from, attribute.to) end.keys end def unit(index = nil) @units[index || @index] end def provideable?(attribute) unit.provides.include?(attribute) end def useable?(attribute) unit.uses.include?(attribute) end def set(attribute, value) logger.debug { "Setting '#{attribute}' value (#{value.class.name}): #{value.inspect}" } @values[attribute] = value end def get(attribute) value = @values[attribute] logger.debug { "Getting '#{attribute}' value (#{value.class.name}): #{value.inspect}" } value end def unprovideable_setter(attribute, value) message = "Unprovideable attribute '#{attribute}' set with value (#{value.class.name}): #{value.inspect}" logger.error(message) raise message end def unaccessable_getter(attribute) message = "Unaccessable getter used for attribute '#{attribute}'" logger.error(message) raise message end def initialize_attributes(units) log_start_finish(:debug, 'Attributes lifespan initialization') do @attributes = Sequencer::Units::Attributes.new(units.declarations) logger.debug { "Attributes lifespan: #{@attributes.inspect}" } end end def initialize_parameters(parameters) logger.debug { "Initializing Sequencer::State with initial parameters: #{parameters.inspect}" } log_start_finish(:debug, 'Attribute value provisioning check and initialization') do @attributes.each do |identifier, attribute| if !attribute.will_be_used? logger.debug { "Attribute '#{identifier}' is provided by Unit(s) but never used." } next end init_param = parameters.key?(identifier) provided_attr = attribute.will_be_provided? if !init_param && !provided_attr message = "Attribute '#{identifier}' is used in Unit '#{unit(attribute.to).name}' (index: #{attribute.to}) but is not provided or given via initial parameters." logger.error(message) raise message end # skip if attribute is provided by an Unit but not # an initial parameter next if !init_param # update 'from' lifespan information for attribute # since it's provided via the initial parameter attribute.from = @index # set initial value set(identifier, parameters[identifier]) end end end def initialize_expectations(expected_attributes) expected_attributes.each do |identifier| logger.debug { "Adding attribute '#{identifier}' to the list of expected result attributes." } @attributes[identifier].to = @result_index end end def cleanup log_start_finish(:info, "State cleanup of Unit #{unit.name} (index: #{@index})") do @attributes.delete_if do |identifier, attribute| remove = !attribute.will_be_used? remove ||= attribute.to <= @index if remove && attribute.will_be_used? logger.debug { "Removing unneeded attribute '#{identifier}': #{@values[identifier].inspect}" } end remove end end end end end