state.rb 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. class Sequencer
  3. class State
  4. include ::Mixin::RailsLogger
  5. include ::Mixin::StartFinishLogger
  6. def initialize(sequence, parameters: {}, expecting: nil)
  7. @index = -1
  8. @units = sequence.units
  9. @result_index = @units.count
  10. @values = {}
  11. initialize_attributes(sequence.units)
  12. initialize_parameters(parameters)
  13. initialize_expectations(expecting || sequence.expecting)
  14. end
  15. # Stores a value for the given attribute. Value can be a regular object
  16. # or the result of a given code block.
  17. # The attribute gets validated against the .provides list of attributes.
  18. # In the case than an attribute gets provided that is not declared to
  19. # be provided an exception will be raised.
  20. #
  21. # @param [Symbol] attribute the attribute for which the value gets provided.
  22. # @param [Object] value the value that should get stored for the given attribute.
  23. # @yield [] executes the given block and takes the result as the value.
  24. # @yieldreturn [Object] the value for the given attribute.
  25. #
  26. # @example
  27. # state.provide(:sum, 3)
  28. #
  29. # @example
  30. # state.provide(:sum) do
  31. # some_value = ...
  32. # some_value * 3
  33. # end
  34. #
  35. # @raise [RuntimeError] if the attribute is not provideable from the calling Unit
  36. #
  37. # @return [nil]
  38. def provide(attribute, value = nil)
  39. if provideable?(attribute)
  40. value = yield if block_given?
  41. set(attribute, value)
  42. else
  43. value = "UNEXECUTED BLOCK: #{caller(1..1).first}" if block_given?
  44. unprovideable_setter(attribute, value)
  45. end
  46. end
  47. # Returns the value of the given attribute.
  48. # The attribute gets validated against the .uses and .optionals
  49. # lists of attributes. In the case that an attribute gets used
  50. # that is not declared to be used or optional, an exception
  51. # gets raised.
  52. #
  53. # @param [Symbol] attribute the attribute for which the value is requested.
  54. #
  55. # @example
  56. # state.use(:answer)
  57. # #=> 42
  58. #
  59. # @raise [RuntimeError] if the attribute is not useable from the calling Unit
  60. #
  61. # @return [nil]
  62. def use(attribute)
  63. if useable?(attribute)
  64. get(attribute)
  65. else
  66. unaccessable_getter(attribute)
  67. end
  68. end
  69. # Returns the value of the given attribute.
  70. # The attribute DOES NOT get validated against the .uses list of attributes.
  71. # Use this method only in edge cases and prefer .optional macro and state.use otherwise.
  72. #
  73. # @param [Symbol] attribute the attribute for which the value is requested.
  74. #
  75. # @example
  76. # state.optional(:answer)
  77. # #=> 42
  78. #
  79. # @example
  80. # state.optional(:unknown)
  81. # #=> nil
  82. #
  83. # @return [Object, nil]
  84. def optional(attribute)
  85. return get(attribute) if @attributes.known?(attribute)
  86. logger.public_send(log_level[:optional]) { "Access to unknown optional attribute '#{attribute}'." }
  87. nil
  88. end
  89. # Checks if a value for the given attribute is provided.
  90. # The attribute DOES NOT get validated against the .uses list of attributes.
  91. # Use this method only in edge cases and prefer .optional macro and state.use otherwise.
  92. #
  93. # @param [Symbol] attribute the attribute which should get checked.
  94. #
  95. # @example
  96. # state.provided?(:answer)
  97. # #=> true
  98. #
  99. # @example
  100. # state.provided?(:unknown)
  101. # #=> false
  102. #
  103. # @return [Boolean]
  104. def provided?(attribute)
  105. optional(attribute) != nil
  106. end
  107. # Unsets the value for the given attribute.
  108. # The attribute gets validated against the .uses list of attributes.
  109. # In the case than an attribute gets unset that is not declared
  110. # to be used an exception will be raised.
  111. #
  112. # @param [Symbol] attribute the attribute for which the value gets unset.
  113. #
  114. # @example
  115. # state.unset(:answer)
  116. #
  117. # @raise [RuntimeError] if the attribute is not useable from the calling Unit
  118. #
  119. # @return [nil]
  120. def unset(attribute)
  121. value = nil
  122. if useable?(attribute)
  123. set(attribute, value)
  124. else
  125. unprovideable_setter(attribute, value)
  126. end
  127. end
  128. # Handles state processing of the next Unit in the Sequence while executing
  129. # the given block. After the Unit is processed the state will get cleaned up
  130. # and no longer needed attribute values will get discarded.
  131. #
  132. # @yield [] executes the given block and handles the state changes before and afterwards.
  133. #
  134. # @example
  135. # state.process do
  136. # unit.process
  137. # end
  138. #
  139. # @return [nil]
  140. def process
  141. @index += 1
  142. yield
  143. cleanup
  144. end
  145. # Handles state processing of the next Unit in the Sequence while executing
  146. # the given block. After the Unit is processed the state will get cleaned up
  147. # and no longer needed attribute values will get discarded.
  148. #
  149. # @example
  150. # state.to_h
  151. # #=> {"ssl_verify"=>true, "host_url"=>"ldaps://192...", ...}
  152. #
  153. # @return [Hash{Symbol => Object}]
  154. def to_h
  155. available.index_with { |identifier| @values[identifier] }
  156. end
  157. private
  158. def available
  159. @attributes.select do |_identifier, attribute|
  160. @index.between?(attribute.from, attribute.till)
  161. end.keys
  162. end
  163. def unit(index = nil)
  164. @units[index || @index]
  165. end
  166. def provideable?(attribute)
  167. unit.provides.include?(attribute)
  168. end
  169. def useable?(attribute)
  170. return true if unit.uses.include?(attribute)
  171. unit.optional.include?(attribute)
  172. end
  173. def set(attribute, value)
  174. logger.public_send(log_level[:set]) { "Setting '#{attribute}' value (#{value.class.name}): #{value.inspect}" }
  175. @values[attribute] = value
  176. end
  177. def get(attribute)
  178. value = @values[attribute]
  179. logger.public_send(log_level[:get]) { "Getting '#{attribute}' value (#{value.class.name}): #{value.inspect}" }
  180. value
  181. end
  182. def unprovideable_setter(attribute, value)
  183. message = "Unprovideable attribute '#{attribute}' set with value (#{value.class.name}): #{value.inspect}"
  184. logger.error(message)
  185. raise message
  186. end
  187. def unaccessable_getter(attribute)
  188. message = "Unaccessable getter used for attribute '#{attribute}'"
  189. logger.error(message)
  190. raise message
  191. end
  192. def initialize_attributes(units)
  193. log_start_finish(log_level[:attribute_initialization][:start_finish], 'Attributes lifespan initialization') do
  194. @attributes = Sequencer::Units::Attributes.new(units.declarations)
  195. logger.public_send(log_level[:attribute_initialization][:attributes]) { "Attributes lifespan: #{@attributes.inspect}" }
  196. end
  197. end
  198. def initialize_parameters(parameters)
  199. logger.public_send(log_level[:parameter_initialization][:parameters]) { "Initializing Sequencer::State with initial parameters: #{parameters.inspect}" }
  200. log_start_finish(log_level[:parameter_initialization][:start_finish], 'Attribute value provisioning check and initialization') do
  201. @attributes.each do |identifier, attribute|
  202. if !attribute.will_be_used?
  203. logger.public_send(log_level[:parameter_initialization][:unused]) { "Attribute '#{identifier}' is provided by Unit(s) but never used." }
  204. next
  205. end
  206. init_param = parameters.key?(identifier)
  207. provided_attr = attribute.will_be_provided?
  208. if !init_param && !provided_attr
  209. next if attribute.optional?
  210. message = "Attribute '#{identifier}' is used in Unit '#{unit(attribute.to).name}' (index: #{attribute.to}) but is not provided or given via initial parameters."
  211. logger.error(message)
  212. raise message
  213. end
  214. # skip if attribute is provided by an Unit but not
  215. # an initial parameter
  216. next if !init_param
  217. # update 'from' lifespan information for attribute
  218. # since it's provided via the initial parameter
  219. attribute.from = @index
  220. # set initial value
  221. set(identifier, parameters[identifier])
  222. end
  223. end
  224. end
  225. def initialize_expectations(expected_attributes)
  226. expected_attributes.each do |identifier|
  227. logger.public_send(log_level[:expectations_initialization]) { "Adding attribute '#{identifier}' to the list of expected result attributes." }
  228. @attributes[identifier].to = @result_index
  229. end
  230. end
  231. def cleanup
  232. log_start_finish(log_level[:cleanup][:start_finish], "State cleanup of Unit #{unit.name} (index: #{@index})") do
  233. @attributes.delete_if do |identifier, attribute|
  234. remove = !attribute.will_be_used?
  235. remove ||= attribute.till <= @index
  236. if remove && attribute.will_be_used?
  237. logger.public_send(log_level[:cleanup][:remove]) { "Removing unneeded attribute '#{identifier}': #{@values[identifier].inspect}" }
  238. end
  239. remove
  240. end
  241. end
  242. end
  243. def log_level
  244. @log_level ||= Sequencer.log_level_for(:state)
  245. end
  246. end
  247. end