floe 0.11.3 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb1a786256a191ba7d1b3b08f6e3cd02fa778fcf28ab44a7353f36ae7979039b
4
- data.tar.gz: ca5ab5d0b5ed1949945593bb1f07141e554fbbac4d825cbf6333bb92e5b17b6d
3
+ metadata.gz: 2eb195983f47d7cac44f18e962bf314c3ff4fefbdf3ac5cae32e960bfc2c0fb0
4
+ data.tar.gz: 6a0f8d91337297786c03e99fc96cb118ed8cb76244e52cf8253702d1517a6f5d
5
5
  SHA512:
6
- metadata.gz: e96574a58740f8659f27947144bdc5067fbade1645a0b651e66607494fce5b0a16ef762bbba1638d54a8804425c2de2c3938884b4c18346be3623b4ffbdc821b
7
- data.tar.gz: da673ecc866d1df2f6f75872df183beda957996fd54ff09177ebd84b27bc64fade88122eaaf34f7134e3a92d74b9d9f7a7322648042df444ad889de9a053d298
6
+ metadata.gz: 99f5a3c8e67a9f5c97b43795739c201a49a8f1195d40a1da38f2aa0e16d200266783336e4be9ada5d41b566c545144a4533db258546ebbf04a23c454a489e6a1
7
+ data.tar.gz: a77bfe436b37adf9dbdc2a279b9b1f70f48980062aaaec191a8e1fd57238e8b8b50b02828025e561c7ccb8429cb67fefb3f7ed044ed309d82eaae8dd8d9d407b
data/.yamllint CHANGED
@@ -1,8 +1,6 @@
1
- ---
2
1
  extends: relaxed
3
-
4
2
  rules:
5
3
  indentation:
6
4
  indent-sequences: false
7
5
  line-length:
8
- max: 120
6
+ max: 1000
data/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.12.0] - 2024-07-31
8
+ ### Added
9
+ - Set Floe.logger.level if DEBUG env var set ([#234](https://github.com/ManageIQ/floe/pull/234))
10
+ - Add InstrinsicFunction foundation + States.Array, States.UUID ([#194](https://github.com/ManageIQ/floe/pull/194))
11
+ - Evaluate IntrinsicFunctions from PayloadTemplate ([#236](https://github.com/ManageIQ/floe/pull/236))
12
+ - Add more intrinsic functions ([#242](https://github.com/ManageIQ/floe/pull/242))
13
+ - Add State#cause_path, error_path ([#249](https://github.com/ManageIQ/floe/pull/249))
14
+ - Validate Catcher Next ([#250](https://github.com/ManageIQ/floe/pull/250))
15
+ - Add spec/supports ([#248](https://github.com/ManageIQ/floe/pull/248))
16
+
17
+ ### Fixed
18
+ - Handle non-hash input/output values ([#214](https://github.com/ManageIQ/floe/pull/214))
19
+ - Fix Input/Output handling for Pass, Choice, Succeed states ([#225](https://github.com/ManageIQ/floe/pull/225))
20
+ - Wrap Parslet::ParseFailed errors as Floe::InvalidWorkflowError ([#235](https://github.com/ManageIQ/floe/pull/235))
21
+ - Fix edge cases with States.Array ([#237](https://github.com/ManageIQ/floe/pull/237))
22
+ - Fix sporadic test failure with Wait state ([#243](https://github.com/ManageIQ/floe/pull/243))
23
+ - Fix invalid container names after normalizing ([#252](https://github.com/ManageIQ/floe/pull/252))
24
+
25
+ ### Changed
26
+ - Extract ErrorMatcherMixin from Catch and Retry ([#186](https://github.com/ManageIQ/floe/pull/186))
27
+ - Output should be JSON ([#230](https://github.com/ManageIQ/floe/pull/230))
28
+ - Normalize functions to all take arguments ([#238](https://github.com/ManageIQ/floe/pull/238))
29
+ - Pass full path name to State.new for better errors ([#229](https://github.com/ManageIQ/floe/pull/229))
30
+ - Validate that state machine input is valid JSON ([#227](https://github.com/ManageIQ/floe/pull/227))
31
+ - Move the Parslet parse into initialize so that invalid function definition will fail on workflow load ([#245](https://github.com/ManageIQ/floe/pull/245))
32
+
7
33
  ## [0.11.3] - 2024-06-20
8
34
  ### Fixed
9
35
  - ResultPath=$ replaces complete output ([#199](https://github.com/ManageIQ/floe/pull/199))
@@ -199,7 +225,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
199
225
  ### Added
200
226
  - Initial release
201
227
 
202
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.11.3...HEAD
228
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.12.0...HEAD
229
+ [0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
203
230
  [0.11.3]: https://github.com/ManageIQ/floe/compare/v0.11.2...v0.11.3
204
231
  [0.11.2]: https://github.com/ManageIQ/floe/compare/v0.11.1...v0.11.2
205
232
  [0.11.1]: https://github.com/ManageIQ/floe/compare/v0.11.0...v0.11.1
data/floe.gemspec CHANGED
@@ -36,11 +36,11 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency "jsonpath", "~>1.1"
37
37
  spec.add_dependency "kubeclient", "~>4.7"
38
38
  spec.add_dependency "optimist", "~>3.0"
39
+ spec.add_dependency "parslet", "~>2.0"
39
40
 
40
- spec.add_development_dependency "manageiq-style"
41
+ spec.add_development_dependency "manageiq-style", ">= 1.5.2"
41
42
  spec.add_development_dependency "rake", "~> 13.0"
42
43
  spec.add_development_dependency "rspec"
43
- spec.add_development_dependency "rubocop"
44
44
  spec.add_development_dependency "simplecov", ">= 0.21.2"
45
45
  spec.add_development_dependency "timecop"
46
46
  end
data/lib/floe/cli.rb CHANGED
@@ -7,6 +7,7 @@ module Floe
7
7
  require "logger"
8
8
 
9
9
  Floe.logger = Logger.new($stdout)
10
+ Floe.logger.level = 0 if ENV["DEBUG"]
10
11
  end
11
12
 
12
13
  def run(args = ARGV)
@@ -30,10 +31,12 @@ module Floe
30
31
  # Display status
31
32
  workflows.each do |workflow|
32
33
  puts "", "#{workflow.name}#{" (#{workflow.status})" unless workflow.context.success?}", "===" if workflows.size > 1
33
- puts workflow.output.inspect
34
+ puts workflow.output
34
35
  end
35
36
 
36
37
  workflows.all? { |workflow| workflow.context.success? }
38
+ rescue Floe::Error => err
39
+ abort(err.message)
37
40
  end
38
41
 
39
42
  private
@@ -24,6 +24,9 @@ module Floe
24
24
  # This does not follow the leading and trailing character restriction because we will embed it
25
25
  # below with a prefix and suffix that already conform to the RFC.
26
26
  normalized_name = name.downcase.gsub(/[^a-z0-9-]/, "-")[0, MAX_CONTAINER_NAME_SIZE]
27
+ # Ensure that the normalized_name doesn't end in any invalid characters after we
28
+ # limited the length to the MAX_CONTAINER_NAME_SIZE.
29
+ normalized_name.gsub!(/[^a-z0-9]+$/, "")
27
30
 
28
31
  "floe-#{normalized_name}-#{SecureRandom.hex(4)}"
29
32
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ module ValidationMixin
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ def parser_error!(comment)
10
+ self.class.parser_error!(name, comment)
11
+ end
12
+
13
+ def missing_field_error!(field_name)
14
+ self.class.missing_field_error!(name, field_name)
15
+ end
16
+
17
+ def invalid_field_error!(field_name, field_value = nil, comment = nil)
18
+ self.class.invalid_field_error!(name, field_name, field_value, comment)
19
+ end
20
+
21
+ def workflow_state?(field_value, workflow)
22
+ workflow.payload["States"] ? workflow.payload["States"].include?(field_value) : true
23
+ end
24
+
25
+ def wrap_parser_error(field_name, field_value)
26
+ yield
27
+ rescue ArgumentError, InvalidWorkflowError => error
28
+ invalid_field_error!(field_name, field_value, error.message)
29
+ end
30
+
31
+ module ClassMethods
32
+ def parser_error!(name, comment)
33
+ name = name.join(".") if name.kind_of?(Array)
34
+ raise Floe::InvalidWorkflowError, "#{name} #{comment}"
35
+ end
36
+
37
+ def missing_field_error!(name, field_name)
38
+ parser_error!(name, "does not have required field \"#{field_name}\"")
39
+ end
40
+
41
+ def invalid_field_error!(name, field_name, field_value, comment)
42
+ # instead of displaying a large hash or array, just displaying the word Hash or Array
43
+ field_value = field_value.class if field_value.kind_of?(Hash) || field_value.kind_of?(Array)
44
+
45
+ parser_error!(name, "field \"#{field_name}\"#{" value \"#{field_value}\"" unless field_value.nil?} #{comment}")
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/floe/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Floe
4
- VERSION = "0.11.3"
4
+ VERSION = "0.12.0"
5
5
  end
@@ -3,14 +3,28 @@
3
3
  module Floe
4
4
  class Workflow
5
5
  class Catcher
6
- attr_reader :error_equals, :next, :result_path
6
+ include ErrorMatcherMixin
7
+ include ValidationMixin
7
8
 
8
- def initialize(payload)
9
- @payload = payload
9
+ attr_reader :error_equals, :next, :result_path, :name
10
+
11
+ def initialize(workflow, name, payload)
12
+ @name = name
13
+ @payload = payload
10
14
 
11
15
  @error_equals = payload["ErrorEquals"]
12
16
  @next = payload["Next"]
13
17
  @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
18
+
19
+ missing_field_error!("ErrorEquals") if !@error_equals.kind_of?(Array) || @error_equals.empty?
20
+ validate_state_next!(workflow)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_state_next!(workflow)
26
+ missing_field_error!("Next") if @next.nil?
27
+ invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !workflow_state?(@next, workflow)
14
28
  end
15
29
  end
16
30
  end
@@ -4,26 +4,31 @@ module Floe
4
4
  class Workflow
5
5
  class ChoiceRule
6
6
  class << self
7
- def build(payload)
7
+ def build(workflow, name, payload)
8
8
  if (sub_payloads = payload["Not"])
9
- Floe::Workflow::ChoiceRule::Not.new(payload, build_children([sub_payloads]))
9
+ name += ["Not"]
10
+ Floe::Workflow::ChoiceRule::Not.new(workflow, name, payload, build_children(workflow, name, [sub_payloads]))
10
11
  elsif (sub_payloads = payload["And"])
11
- Floe::Workflow::ChoiceRule::And.new(payload, build_children(sub_payloads))
12
+ name += ["And"]
13
+ Floe::Workflow::ChoiceRule::And.new(workflow, name, payload, build_children(workflow, name, sub_payloads))
12
14
  elsif (sub_payloads = payload["Or"])
13
- Floe::Workflow::ChoiceRule::Or.new(payload, build_children(sub_payloads))
15
+ name += ["Or"]
16
+ Floe::Workflow::ChoiceRule::Or.new(workflow, name, payload, build_children(workflow, name, sub_payloads))
14
17
  else
15
- Floe::Workflow::ChoiceRule::Data.new(payload)
18
+ name += ["Data"]
19
+ Floe::Workflow::ChoiceRule::Data.new(workflow, name, payload)
16
20
  end
17
21
  end
18
22
 
19
- def build_children(sub_payloads)
20
- sub_payloads.map { |payload| build(payload) }
23
+ def build_children(workflow, name, sub_payloads)
24
+ sub_payloads.map.with_index { |payload, i| build(workflow, name + [i.to_s], payload) }
21
25
  end
22
26
  end
23
27
 
24
- attr_reader :next, :payload, :variable, :children
28
+ attr_reader :next, :payload, :variable, :children, :name
25
29
 
26
- def initialize(payload, children = nil)
30
+ def initialize(_workflow, name, payload, children = nil)
31
+ @name = name
27
32
  @payload = payload
28
33
  @children = children
29
34
 
@@ -9,9 +9,7 @@ module Floe
9
9
  # @param input [Hash] (default: {})
10
10
  def initialize(context = nil, input: nil, credentials: {})
11
11
  context = JSON.parse(context) if context.kind_of?(String)
12
-
13
- input ||= {}
14
- input = JSON.parse(input) if input.kind_of?(String)
12
+ input = JSON.parse(input || "{}")
15
13
 
16
14
  @context = context || {}
17
15
  self["Execution"] ||= {}
@@ -23,7 +21,7 @@ module Floe
23
21
 
24
22
  @credentials = credentials || {}
25
23
  rescue JSON::ParserError => err
26
- raise Floe::InvalidWorkflowError, err.message
24
+ raise Floe::InvalidExecutionInput, "Invalid State Machine Execution Input: #{err}: was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')"
27
25
  end
28
26
 
29
27
  def execution
@@ -39,7 +37,7 @@ module Floe
39
37
  end
40
38
 
41
39
  def failed?
42
- output&.key?("Error") || false
40
+ (output.kind_of?(Hash) && output.key?("Error")) || false
43
41
  end
44
42
 
45
43
  def ended?
@@ -54,10 +52,18 @@ module Floe
54
52
  state["Input"]
55
53
  end
56
54
 
55
+ def json_input
56
+ input.to_json
57
+ end
58
+
57
59
  def output
58
60
  state["Output"]
59
61
  end
60
62
 
63
+ def json_output
64
+ output.to_json
65
+ end
66
+
61
67
  def output=(val)
62
68
  state["Output"] = val
63
69
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ # Methods for common error handling
6
+ module ErrorMatcherMixin
7
+ # @param [String] error the error thrown
8
+ def match_error?(error)
9
+ return false if error == "States.Runtime"
10
+ return true if error_equals.include?("States.ALL")
11
+ return true if error_equals.include?("States.Timeout") && error == "States.HeartbeatTimeout"
12
+
13
+ error_equals.include?(error)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,100 @@
1
+ require "parslet"
2
+
3
+ module Floe
4
+ class Workflow
5
+ class IntrinsicFunction
6
+ class Parser < Parslet::Parser
7
+ rule(:spaces) { str(' ').repeat(1) }
8
+ rule(:spaces?) { spaces.maybe }
9
+ rule(:digit) { match('[0-9]') }
10
+ rule(:quote) { str('\'') }
11
+
12
+ rule(:comma_sep) { str(',') >> spaces? }
13
+
14
+ rule(:true_literal) { str('true').as(:true_literal) }
15
+ rule(:false_literal) { str('false').as(:false_literal) }
16
+ rule(:null_literal) { str('null').as(:null_literal) }
17
+
18
+ rule(:number) do
19
+ (
20
+ str('-').maybe >> (
21
+ str('0') | (match('[1-9]') >> digit.repeat)
22
+ ) >> (
23
+ str('.') >> digit.repeat(1)
24
+ ).maybe >> (
25
+ match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
26
+ ).maybe
27
+ ).as(:number)
28
+ end
29
+
30
+ rule(:string) do
31
+ (
32
+ quote >> (
33
+ (str('\\') >> any) | (quote.absent? >> any)
34
+ ).repeat >> quote
35
+ ).as(:string)
36
+ end
37
+
38
+ rule(:jsonpath) do
39
+ (
40
+ str('$') >> match('[^,)]').repeat(0)
41
+ ).as(:jsonpath)
42
+ end
43
+
44
+ rule(:arg) do
45
+ (
46
+ string | number | jsonpath | true_literal | false_literal | null_literal | expression
47
+ ).as(:arg)
48
+ end
49
+
50
+ rule(:args) do
51
+ (
52
+ arg >> (comma_sep >> arg).repeat
53
+ ).maybe.as(:args)
54
+ end
55
+
56
+ [
57
+ :states_array, "States.Array",
58
+ :states_array_partition, "States.ArrayPartition",
59
+ :states_array_contains, "States.ArrayContains",
60
+ :states_array_range, "States.ArrayRange",
61
+ :states_array_get_item, "States.ArrayGetItem",
62
+ :states_array_length, "States.ArrayLength",
63
+ :states_array_unique, "States.ArrayUnique",
64
+ :states_base64_encode, "States.Base64Encode",
65
+ :states_base64_decode, "States.Base64Decode",
66
+ :states_hash, "States.Hash",
67
+ :states_math_random, "States.MathRandom",
68
+ :states_math_add, "States.MathAdd",
69
+ :states_string_split, "States.StringSplit",
70
+ :states_uuid, "States.UUID",
71
+ ].each_slice(2) do |function_symbol, function_name|
72
+ rule(function_symbol) do
73
+ (
74
+ str(function_name) >> str('(') >> args >> str(')')
75
+ ).as(function_symbol)
76
+ end
77
+ end
78
+
79
+ rule(:expression) do
80
+ states_array |
81
+ states_array_partition |
82
+ states_array_contains |
83
+ states_array_range |
84
+ states_array_get_item |
85
+ states_array_length |
86
+ states_array_unique |
87
+ states_base64_encode |
88
+ states_base64_decode |
89
+ states_hash |
90
+ states_math_random |
91
+ states_math_add |
92
+ states_string_split |
93
+ states_uuid
94
+ end
95
+
96
+ root(:expression)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,196 @@
1
+ # Disable rubocops against the `match` method, since this is a Parslet specific
2
+ # match method and not the typical `Object#match`.
3
+ # rubocop:disable Performance/RegexpMatch, Performance/RedundantMatch
4
+
5
+ require "parslet"
6
+
7
+ module Floe
8
+ class Workflow
9
+ class IntrinsicFunction
10
+ class Transformer < Parslet::Transform
11
+ OptionalArg = Struct.new(:type)
12
+
13
+ class << self
14
+ def process_args(args, function, signature = nil)
15
+ args = resolve_args(args)
16
+ if signature
17
+ check_arity(args, function, signature)
18
+ check_types(args, function, signature)
19
+ end
20
+ args
21
+ end
22
+
23
+ private
24
+
25
+ def resolve_args(args)
26
+ if args.nil?
27
+ # 0 args
28
+ []
29
+ elsif args.kind_of?(Array)
30
+ # >1 arg
31
+ args.map { |a| a[:arg] }
32
+ else
33
+ # 1 arg
34
+ [args[:arg]]
35
+ end
36
+ end
37
+
38
+ def check_arity(args, function, signature)
39
+ if signature.any?(OptionalArg)
40
+ signature_without_optional = signature.reject { |a| a.kind_of?(OptionalArg) }
41
+ signature_size = (signature_without_optional.size..signature.size)
42
+
43
+ raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature_size})" unless signature_size.include?(args.size)
44
+ else
45
+ raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature.size})" unless signature.size == args.size
46
+ end
47
+ end
48
+
49
+ def check_types(args, function, signature)
50
+ args.zip(signature).each_with_index do |(arg, type), index|
51
+ type = type.type if type.kind_of?(OptionalArg)
52
+
53
+ raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type)
54
+ end
55
+ end
56
+ end
57
+
58
+ rule(:null_literal => simple(:v)) { nil }
59
+ rule(:true_literal => simple(:v)) { true }
60
+ rule(:false_literal => simple(:v)) { false }
61
+
62
+ rule(:string => simple(:v)) { v.to_s[1..-2] }
63
+ rule(:number => simple(:v)) { v.match(/[eE.]/) ? Float(v) : Integer(v) }
64
+ rule(:jsonpath => simple(:v)) { Floe::Workflow::Path.value(v.to_s, context, input) }
65
+
66
+ rule(:states_array => {:args => subtree(:args)}) do
67
+ Transformer.process_args(args, "States.Array")
68
+ end
69
+
70
+ rule(:states_array_partition => {:args => subtree(:args)}) do
71
+ args = Transformer.process_args(args(), "States.ArrayPartition", [Array, Integer])
72
+ array, chunk = *args
73
+ raise ArgumentError, "invalid value for argument 2 to States.ArrayPartition (given #{chunk}, expected a positive Integer)" unless chunk.positive?
74
+
75
+ array.each_slice(chunk).to_a
76
+ end
77
+
78
+ rule(:states_array_contains => {:args => subtree(:args)}) do
79
+ args = Transformer.process_args(args(), "States.ArrayContains", [Array, Object])
80
+ array, target = *args
81
+
82
+ array.include?(target)
83
+ end
84
+
85
+ rule(:states_array_range => {:args => subtree(:args)}) do
86
+ args = Transformer.process_args(args(), "States.ArrayRange", [Integer, Integer, Integer])
87
+ range_begin, range_end, increment = *args
88
+ raise ArgumentError, "invalid value for argument 3 to States.ArrayRange (given #{increment}, expected a non-zero Integer)" if increment.zero?
89
+
90
+ (range_begin..range_end).step(increment).to_a
91
+ end
92
+
93
+ rule(:states_array_get_item => {:args => subtree(:args)}) do
94
+ args = Transformer.process_args(args(), "States.ArrayGetItem", [Array, Integer])
95
+ array, index = *args
96
+ raise ArgumentError, "invalid value for argument 2 to States.ArrayGetItem (given #{index}, expected 0 or a positive Integer)" unless index >= 0
97
+
98
+ array[index]
99
+ end
100
+
101
+ rule(:states_array_length => {:args => subtree(:args)}) do
102
+ args = Transformer.process_args(args(), "States.ArrayLength", [Array])
103
+ array = args.first
104
+
105
+ array.size
106
+ end
107
+
108
+ rule(:states_array_unique => {:args => subtree(:args)}) do
109
+ args = Transformer.process_args(args(), "States.ArrayUnique", [Array])
110
+ array = args.first
111
+
112
+ array.uniq
113
+ end
114
+
115
+ rule(:states_base64_encode => {:args => subtree(:args)}) do
116
+ args = Transformer.process_args(args(), "States.Base64Encode", [String])
117
+ str = args.first
118
+
119
+ require "base64"
120
+ Base64.strict_encode64(str).force_encoding("UTF-8")
121
+ end
122
+
123
+ rule(:states_base64_decode => {:args => subtree(:args)}) do
124
+ args = Transformer.process_args(args(), "States.Base64Decode", [String])
125
+ str = args.first
126
+
127
+ require "base64"
128
+ begin
129
+ Base64.strict_decode64(str)
130
+ rescue ArgumentError => err
131
+ raise ArgumentError, "invalid value for argument 1 to States.Base64Decode (#{err})"
132
+ end
133
+ end
134
+
135
+ rule(:states_hash => {:args => subtree(:args)}) do
136
+ args = Transformer.process_args(args(), "States.Hash", [Object, String])
137
+ data, algorithm = *args
138
+ raise NotImplementedError if data.kind_of?(Hash)
139
+ if data.nil?
140
+ raise ArgumentError, "invalid value for argument 1 to States.Hash (given null, expected non-null)"
141
+ end
142
+
143
+ algorithms = %w[MD5 SHA-1 SHA-256 SHA-384 SHA-512]
144
+ unless algorithms.include?(algorithm)
145
+ raise ArgumentError, "invalid value for argument 2 to States.Hash (given #{algorithm.inspect}, expected one of #{algorithms.map(&:inspect).join(", ")})"
146
+ end
147
+
148
+ require "openssl"
149
+ algorithm = algorithm.sub("-", "")
150
+ data = data.to_json unless data.kind_of?(String)
151
+ OpenSSL::Digest.hexdigest(algorithm, data)
152
+ end
153
+
154
+ rule(:states_math_random => {:args => subtree(:args)}) do
155
+ args = Transformer.process_args(args(), "States.MathRandom", [Integer, Integer, OptionalArg[Integer]])
156
+ range_start, range_end, seed = *args
157
+ unless range_start < range_end
158
+ raise ArgumentError, "invalid values for arguments to States.MathRandom (start must be less than end)"
159
+ end
160
+
161
+ seed ||= Random.new_seed
162
+ Random.new(seed).rand(range_start..range_end)
163
+ end
164
+
165
+ rule(:states_math_add => {:args => subtree(:args)}) do
166
+ args = Transformer.process_args(args(), "States.MathAdd", [Integer, Integer])
167
+
168
+ args.sum
169
+ end
170
+
171
+ rule(:states_string_split => {:args => subtree(:args)}) do
172
+ args = Transformer.process_args(args(), "States.StringSplit", [String, String])
173
+ str, delimeter = *args
174
+
175
+ case delimeter.size
176
+ when 0
177
+ str.empty? ? [] : [str]
178
+ when 1
179
+ str.split(delimeter)
180
+ else
181
+ str.split(/[#{Regexp.escape(delimeter)}]+/)
182
+ end
183
+ end
184
+
185
+ rule(:states_uuid => {:args => subtree(:args)}) do
186
+ Transformer.process_args(args, "States.UUID", [])
187
+
188
+ require "securerandom"
189
+ SecureRandom.uuid
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ # rubocop:enable Performance/RegexpMatch, Performance/RedundantMatch
@@ -0,0 +1,34 @@
1
+ require "parslet"
2
+
3
+ module Floe
4
+ class Workflow
5
+ class IntrinsicFunction
6
+ def self.value(payload, context = {}, input = {})
7
+ new(payload).value(context, input)
8
+ end
9
+
10
+ def self.intrinsic_function?(payload)
11
+ payload.start_with?("States.")
12
+ end
13
+
14
+ attr_reader :payload
15
+
16
+ def initialize(payload)
17
+ @payload = payload
18
+ @tree = Parser.new.parse(payload)
19
+
20
+ Floe.logger.debug { "Parsed intrinsic function: #{payload.inspect} => #{tree.inspect}" }
21
+ rescue Parslet::ParseFailed => err
22
+ raise Floe::InvalidWorkflowError, err.message
23
+ end
24
+
25
+ def value(context = {}, input = {})
26
+ Transformer.new.apply(tree, :context => context, :input => input)
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :tree
32
+ end
33
+ end
34
+ end
@@ -4,6 +4,10 @@ module Floe
4
4
  class Workflow
5
5
  class Path
6
6
  class << self
7
+ def path?(payload)
8
+ payload.start_with?("$")
9
+ end
10
+
7
11
  def value(payload, context, input = {})
8
12
  new(payload).value(context, input)
9
13
  end
@@ -26,8 +30,10 @@ module Floe
26
30
  [input, payload]
27
31
  end
28
32
 
29
- results = JsonPath.on(obj, path)
33
+ # If path is $ then just return the entire input
34
+ return obj if path == "$"
30
35
 
36
+ results = JsonPath.on(obj, path)
31
37
  results.count < 2 ? results.first : results
32
38
  end
33
39
  end
@@ -42,14 +42,17 @@ module Floe
42
42
  end
43
43
 
44
44
  def parse_payload_string(value)
45
- value.start_with?("$") ? Path.new(value) : value
45
+ return Path.new(value) if Path.path?(value)
46
+ return IntrinsicFunction.new(value) if IntrinsicFunction.intrinsic_function?(value)
47
+
48
+ value
46
49
  end
47
50
 
48
51
  def interpolate_value(value, context, inputs)
49
52
  case value
50
- when Array then interpolate_value_array(value, context, inputs)
51
- when Hash then interpolate_value_hash(value, context, inputs)
52
- when Path then value.value(context, inputs)
53
+ when Array then interpolate_value_array(value, context, inputs)
54
+ when Hash then interpolate_value_hash(value, context, inputs)
55
+ when Path, IntrinsicFunction then value.value(context, inputs)
53
56
  else
54
57
  value
55
58
  end
@@ -3,15 +3,21 @@
3
3
  module Floe
4
4
  class Workflow
5
5
  class Retrier
6
- attr_reader :error_equals, :interval_seconds, :max_attempts, :backoff_rate
6
+ include ErrorMatcherMixin
7
+ include ValidationMixin
7
8
 
8
- def initialize(payload)
9
- @payload = payload
9
+ attr_reader :error_equals, :interval_seconds, :max_attempts, :backoff_rate, :name
10
+
11
+ def initialize(_workflow, name, payload)
12
+ @name = name
13
+ @payload = payload
10
14
 
11
15
  @error_equals = payload["ErrorEquals"]
12
16
  @interval_seconds = payload["IntervalSeconds"] || 1.0
13
17
  @max_attempts = payload["MaxAttempts"] || 3
14
18
  @backoff_rate = payload["BackoffRate"] || 2.0
19
+
20
+ missing_field_error!("ErrorEquals") if !@error_equals.kind_of?(Array) || @error_equals.empty?
15
21
  end
16
22
 
17
23
  # @param [Integer] attempt 1 for the first attempt
@@ -4,16 +4,18 @@ module Floe
4
4
  class Workflow
5
5
  class State
6
6
  include Logging
7
+ include ValidationMixin
7
8
 
8
9
  class << self
9
10
  def build!(workflow, name, payload)
10
11
  state_type = payload["Type"]
11
- raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
12
+ missing_field_error!(name, "Type") if payload["Type"].nil?
13
+ invalid_field_error!(name[0..-2], "Name", name.last, "must be less than or equal to 80 characters") if name.last.length > 80
12
14
 
13
15
  begin
14
16
  klass = Floe::Workflow::States.const_get(state_type)
15
17
  rescue NameError
16
- raise Floe::InvalidWorkflowError, "Invalid state type: [#{state_type}]"
18
+ invalid_field_error!(name, "Type", state_type, "is not valid")
17
19
  end
18
20
 
19
21
  klass.new(workflow, name, payload)
@@ -22,14 +24,11 @@ module Floe
22
24
 
23
25
  attr_reader :comment, :name, :type, :payload
24
26
 
25
- def initialize(workflow, name, payload)
27
+ def initialize(_workflow, name, payload)
26
28
  @name = name
27
29
  @payload = payload
28
30
  @type = payload["Type"]
29
31
  @comment = payload["Comment"]
30
-
31
- raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
32
- raise Floe::InvalidWorkflowError, "State name [#{name[..79]}...] must be less than or equal to 80 characters" if name.length > 80
33
32
  end
34
33
 
35
34
  def wait(context, timeout: nil)
@@ -54,7 +53,7 @@ module Floe
54
53
  def start(context)
55
54
  context.state["EnteredTime"] = Time.now.utc.iso8601
56
55
 
57
- logger.info("Running state: [#{long_name}] with input [#{context.input}]...")
56
+ logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
58
57
  end
59
58
 
60
59
  def finish(context)
@@ -64,8 +63,8 @@ module Floe
64
63
  context.state["FinishedTime"] ||= finished_time.iso8601
65
64
  context.state["Duration"] = finished_time - entered_time
66
65
 
67
- level = context.output&.[]("Error") ? :error : :info
68
- logger.public_send(level, "Running state: [#{long_name}] with input [#{context.input}]...Complete #{context.next_state ? "- next state [#{context.next_state}]" : "workflow -"} output: [#{context.output}]")
66
+ level = context.failed? ? :error : :info
67
+ logger.public_send(level, "Running state: [#{long_name}] with input [#{context.json_input}]...Complete #{context.next_state ? "- next state [#{context.next_state}]" : "workflow -"} output: [#{context.json_output}]")
69
68
 
70
69
  0
71
70
  end
@@ -86,8 +85,12 @@ module Floe
86
85
  context.state["WaitUntil"] && Time.parse(context.state["WaitUntil"])
87
86
  end
88
87
 
88
+ def short_name
89
+ name.last
90
+ end
91
+
89
92
  def long_name
90
- "#{@type}:#{name}"
93
+ "#{type}:#{short_name}"
91
94
  end
92
95
 
93
96
  private
@@ -95,7 +98,7 @@ module Floe
95
98
  def wait_until!(context, seconds: nil, time: nil)
96
99
  context.state["WaitUntil"] =
97
100
  if seconds
98
- (Time.parse(context.state["EnteredTime"]) + seconds).iso8601
101
+ (Time.now + seconds).iso8601
99
102
  elsif time.kind_of?(String)
100
103
  time
101
104
  else
@@ -11,7 +11,7 @@ module Floe
11
11
 
12
12
  validate_state!(workflow)
13
13
 
14
- @choices = payload["Choices"].map { |choice| ChoiceRule.build(choice) }
14
+ @choices = payload["Choices"].map.with_index { |choice, i| ChoiceRule.build(workflow, name + ["Choices", i.to_s], choice) }
15
15
  @default = payload["Default"]
16
16
 
17
17
  @input_path = Path.new(payload.fetch("InputPath", "$"))
@@ -19,7 +19,8 @@ module Floe
19
19
  end
20
20
 
21
21
  def finish(context)
22
- output = output_path.value(context, context.input)
22
+ input = input_path.value(context, context.input)
23
+ output = output_path.value(context, input)
23
24
  next_state = choices.detect { |choice| choice.true?(context, output) }&.next || default
24
25
 
25
26
  context.next_state = next_state
@@ -43,12 +44,12 @@ module Floe
43
44
  end
44
45
 
45
46
  def validate_state_choices!
46
- raise Floe::InvalidWorkflowError, "Choice state must have \"Choices\"" unless payload.key?("Choices")
47
- raise Floe::InvalidWorkflowError, "\"Choices\" must be a non-empty array" unless payload["Choices"].kind_of?(Array) && !payload["Choices"].empty?
47
+ missing_field_error!("Choices") unless payload.key?("Choices")
48
+ invalid_field_error!("Choices", nil, "must be a non-empty array") unless payload["Choices"].kind_of?(Array) && !payload["Choices"].empty?
48
49
  end
49
50
 
50
51
  def validate_state_default!(workflow)
51
- raise Floe::InvalidWorkflowError, "\"Default\" not in \"States\"" unless workflow.payload["States"].include?(payload["Default"])
52
+ invalid_field_error!("Default", payload["Default"], "is not found in \"States\"") if payload["Default"] && !workflow_state?(payload["Default"], workflow)
52
53
  end
53
54
  end
54
55
  end
@@ -4,7 +4,7 @@ module Floe
4
4
  class Workflow
5
5
  module States
6
6
  class Fail < Floe::Workflow::State
7
- attr_reader :cause, :error
7
+ attr_reader :cause, :error, :cause_path, :error_path
8
8
 
9
9
  def initialize(workflow, name, payload)
10
10
  super
@@ -12,8 +12,8 @@ module Floe
12
12
  end
13
13
 
14
14
  def validate_state_next!(workflow)
15
- raise Floe::InvalidWorkflowError, "Missing \"Next\" field in state [#{name}]" if @next.nil? && !@end
16
- raise Floe::InvalidWorkflowError, "\"Next\" [#{@next}] not in \"States\" for state [#{name}]" if @next && !workflow.payload["States"].key?(@next)
15
+ missing_field_error!("Next") if @next.nil? && !@end
16
+ invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !workflow_state?(@next, workflow)
17
17
  end
18
18
  end
19
19
  end
@@ -25,7 +25,8 @@ module Floe
25
25
  end
26
26
 
27
27
  def finish(context)
28
- context.output = process_output(context, result)
28
+ input = result.nil? ? process_input(context) : result
29
+ context.output = process_output(context, input)
29
30
  super
30
31
  end
31
32
 
@@ -6,9 +6,18 @@ module Floe
6
6
  class Succeed < Floe::Workflow::State
7
7
  attr_reader :input_path, :output_path
8
8
 
9
+ def initialize(workflow, name, payload)
10
+ super
11
+
12
+ @input_path = Path.new(payload.fetch("InputPath", "$"))
13
+ @output_path = Path.new(payload.fetch("OutputPath", "$"))
14
+ end
15
+
9
16
  def finish(context)
17
+ input = input_path.value(context, context.input)
18
+ context.output = output_path.value(context, input)
10
19
  context.next_state = nil
11
- context.output = context.input
20
+
12
21
  super
13
22
  end
14
23
 
@@ -18,10 +18,13 @@ module Floe
18
18
  @next = payload["Next"]
19
19
  @end = !!payload["End"]
20
20
  @resource = payload["Resource"]
21
- @runner = Floe::Runner.for_resource(@resource)
21
+
22
+ missing_field_error!("Resource") unless @resource.kind_of?(String)
23
+ @runner = wrap_parser_error("Resource", @resource) { Floe::Runner.for_resource(@resource) }
24
+
22
25
  @timeout_seconds = payload["TimeoutSeconds"]
23
- @retry = payload["Retry"].to_a.map { |retrier| Retrier.new(retrier) }
24
- @catch = payload["Catch"].to_a.map { |catcher| Catcher.new(catcher) }
26
+ @retry = payload["Retry"].to_a.map.with_index { |retrier, i| Retrier.new(workflow, name + ["Retry", i.to_s], retrier) }
27
+ @catch = payload["Catch"].to_a.map.with_index { |catcher, i| Catcher.new(workflow, name + ["Catch", i.to_s], catcher) }
25
28
  @input_path = Path.new(payload.fetch("InputPath", "$"))
26
29
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
27
30
  @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
@@ -30,8 +33,6 @@ module Floe
30
33
  @credentials = PayloadTemplate.new(payload["Credentials"]) if payload["Credentials"]
31
34
 
32
35
  validate_state!(workflow)
33
- rescue ArgumentError => err
34
- raise Floe::InvalidWorkflowError, err.message
35
36
  end
36
37
 
37
38
  def start(context)
@@ -82,11 +83,11 @@ module Floe
82
83
  end
83
84
 
84
85
  def find_retrier(error)
85
- self.retry.detect { |r| (r.error_equals & [error, "States.ALL"]).any? }
86
+ self.retry.detect { |r| r.match_error?(error) }
86
87
  end
87
88
 
88
89
  def find_catcher(error)
89
- self.catch.detect { |c| (c.error_equals & [error, "States.ALL"]).any? }
90
+ self.catch.detect { |c| c.match_error?(error) }
90
91
  end
91
92
 
92
93
  def retry_state!(context, error)
@@ -106,7 +107,7 @@ module Floe
106
107
  wait_until!(context, :seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
107
108
  context.next_state = context.state_name
108
109
  context.output = error
109
- logger.info("Running state: [#{long_name}] with input [#{context.input}] got error[#{context.output}]...Retry - delay: #{wait_until(context)}")
110
+ logger.info("Running state: [#{long_name}] with input [#{context.json_input}] got error[#{context.json_output}]...Retry - delay: #{wait_until(context)}")
110
111
  true
111
112
  end
112
113
 
@@ -116,7 +117,7 @@ module Floe
116
117
 
117
118
  context.next_state = catcher.next
118
119
  context.output = catcher.result_path.set(context.input, error)
119
- logger.info("Running state: [#{long_name}] with input [#{context.input}]...CatchError - next state: [#{context.next_state}] output: [#{context.output}]")
120
+ logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...CatchError - next state: [#{context.next_state}] output: [#{context.json_output}]")
120
121
 
121
122
  true
122
123
  end
@@ -126,7 +127,7 @@ module Floe
126
127
  # keeping in here for completeness
127
128
  context.next_state = nil
128
129
  context.output = error
129
- logger.error("Running state: [#{long_name}] with input [#{context.input}]...Complete workflow - output: [#{context.output}]")
130
+ logger.error("Running state: [#{long_name}] with input [#{context.json_input}]...Complete workflow - output: [#{context.json_output}]")
130
131
  end
131
132
 
132
133
  def parse_error(output)
data/lib/floe/workflow.rb CHANGED
@@ -6,6 +6,7 @@ require "json"
6
6
  module Floe
7
7
  class Workflow
8
8
  include Logging
9
+ include ValidationMixin
9
10
 
10
11
  class << self
11
12
  def load(path_or_io, context = nil, credentials = {}, name = nil)
@@ -96,19 +97,21 @@ module Floe
96
97
  # caller should really put credentials into context and not pass that variable
97
98
  context.credentials = credentials if credentials
98
99
 
99
- raise Floe::InvalidWorkflowError, "Missing field \"States\"" if payload["States"].nil?
100
- raise Floe::InvalidWorkflowError, "Missing field \"StartAt\"" if payload["StartAt"].nil?
101
- raise Floe::InvalidWorkflowError, "\"StartAt\" not in the \"States\" field" unless payload["States"].key?(payload["StartAt"])
102
-
103
- @name = name
100
+ # NOTE: this is a string, and states use an array
101
+ @name = name || "State Machine"
104
102
  @payload = payload
105
103
  @context = context
106
104
  @comment = payload["Comment"]
107
105
  @start_at = payload["StartAt"]
108
106
 
109
- @states = payload["States"].to_a.map { |state_name, state| State.build!(self, state_name, state) }
110
- @states_by_name = @states.each_with_object({}) { |state, result| result[state.name] = state }
111
- rescue Floe::InvalidWorkflowError
107
+ # NOTE: Everywhere else we include our name (i.e.: parent name) when building the child name.
108
+ # When creating the states, we are dropping our name (i.e.: the workflow name)
109
+ @states = payload["States"].to_a.map { |state_name, state| State.build!(self, ["States", state_name], state) }
110
+
111
+ validate_workflow
112
+
113
+ @states_by_name = @states.each_with_object({}) { |state, result| result[state.short_name] = state }
114
+ rescue Floe::Error
112
115
  raise
113
116
  rescue => err
114
117
  raise Floe::InvalidWorkflowError, err.message
@@ -157,7 +160,7 @@ module Floe
157
160
  end
158
161
 
159
162
  def output
160
- context.output if end?
163
+ context.json_output if end?
161
164
  end
162
165
 
163
166
  def end?
@@ -187,8 +190,15 @@ module Floe
187
190
  def credentials
188
191
  @context.credentials
189
192
  end
193
+
190
194
  private
191
195
 
196
+ def validate_workflow
197
+ missing_field_error!("States") if @states.empty?
198
+ missing_field_error!("StartAt") if @start_at.nil?
199
+ invalid_field_error!("StartAt", @start_at, "is not found in \"States\"") unless workflow_state?(@start_at, self)
200
+ end
201
+
192
202
  def step!
193
203
  next_state = {"Name" => context.next_state, "Guid" => SecureRandom.uuid, "PreviousStateGuid" => context.state["Guid"]}
194
204
 
data/lib/floe.rb CHANGED
@@ -7,7 +7,9 @@ require_relative "floe/logging"
7
7
 
8
8
  require_relative "floe/runner"
9
9
 
10
+ require_relative "floe/validation_mixin"
10
11
  require_relative "floe/workflow"
12
+ require_relative "floe/workflow/error_matcher_mixin"
11
13
  require_relative "floe/workflow/catcher"
12
14
  require_relative "floe/workflow/choice_rule"
13
15
  require_relative "floe/workflow/choice_rule/not"
@@ -15,6 +17,9 @@ require_relative "floe/workflow/choice_rule/or"
15
17
  require_relative "floe/workflow/choice_rule/and"
16
18
  require_relative "floe/workflow/choice_rule/data"
17
19
  require_relative "floe/workflow/context"
20
+ require_relative "floe/workflow/intrinsic_function"
21
+ require_relative "floe/workflow/intrinsic_function/parser"
22
+ require_relative "floe/workflow/intrinsic_function/transformer"
18
23
  require_relative "floe/workflow/path"
19
24
  require_relative "floe/workflow/payload_template"
20
25
  require_relative "floe/workflow/reference_path"
@@ -37,6 +42,7 @@ require "time"
37
42
  module Floe
38
43
  class Error < StandardError; end
39
44
  class InvalidWorkflowError < Error; end
45
+ class InvalidExecutionInput < Error; end
40
46
 
41
47
  def self.logger
42
48
  @logger ||= NullLogger.new
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.3
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ManageIQ Developers
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-20 00:00:00.000000000 Z
11
+ date: 2024-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_spawn
@@ -80,20 +80,34 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: parslet
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: manageiq-style
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - ">="
88
102
  - !ruby/object:Gem::Version
89
- version: '0'
103
+ version: 1.5.2
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - ">="
95
109
  - !ruby/object:Gem::Version
96
- version: '0'
110
+ version: 1.5.2
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rake
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -122,20 +136,6 @@ dependencies:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: rubocop
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: simplecov
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -196,6 +196,7 @@ files:
196
196
  - lib/floe/logging.rb
197
197
  - lib/floe/null_logger.rb
198
198
  - lib/floe/runner.rb
199
+ - lib/floe/validation_mixin.rb
199
200
  - lib/floe/version.rb
200
201
  - lib/floe/workflow.rb
201
202
  - lib/floe/workflow/catcher.rb
@@ -205,6 +206,10 @@ files:
205
206
  - lib/floe/workflow/choice_rule/not.rb
206
207
  - lib/floe/workflow/choice_rule/or.rb
207
208
  - lib/floe/workflow/context.rb
209
+ - lib/floe/workflow/error_matcher_mixin.rb
210
+ - lib/floe/workflow/intrinsic_function.rb
211
+ - lib/floe/workflow/intrinsic_function/parser.rb
212
+ - lib/floe/workflow/intrinsic_function/transformer.rb
208
213
  - lib/floe/workflow/path.rb
209
214
  - lib/floe/workflow/payload_template.rb
210
215
  - lib/floe/workflow/reference_path.rb