floe 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -3,13 +3,13 @@
3
3
  module Floe
4
4
  class Workflow
5
5
  class Context
6
+ attr_accessor :credentials
7
+
6
8
  # @param context [Json|Hash] (default, create another with input and execution params)
7
9
  # @param input [Hash] (default: {})
8
- def initialize(context = nil, input: nil)
10
+ def initialize(context = nil, input: nil, credentials: {})
9
11
  context = JSON.parse(context) if context.kind_of?(String)
10
-
11
- input ||= {}
12
- input = JSON.parse(input) if input.kind_of?(String)
12
+ input = JSON.parse(input || "{}")
13
13
 
14
14
  @context = context || {}
15
15
  self["Execution"] ||= {}
@@ -18,6 +18,10 @@ module Floe
18
18
  self["StateHistory"] ||= []
19
19
  self["StateMachine"] ||= {}
20
20
  self["Task"] ||= {}
21
+
22
+ @credentials = credentials || {}
23
+ rescue JSON::ParserError => err
24
+ raise Floe::InvalidExecutionInput, "Invalid State Machine Execution Input: #{err}: was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')"
21
25
  end
22
26
 
23
27
  def execution
@@ -33,7 +37,7 @@ module Floe
33
37
  end
34
38
 
35
39
  def failed?
36
- output&.key?("Error") || false
40
+ (output.kind_of?(Hash) && output.key?("Error")) || false
37
41
  end
38
42
 
39
43
  def ended?
@@ -48,10 +52,18 @@ module Floe
48
52
  state["Input"]
49
53
  end
50
54
 
55
+ def json_input
56
+ input.to_json
57
+ end
58
+
51
59
  def output
52
60
  state["Output"]
53
61
  end
54
62
 
63
+ def json_output
64
+ output.to_json
65
+ end
66
+
55
67
  def output=(val)
56
68
  state["Output"] = val
57
69
  end
@@ -84,6 +96,16 @@ module Floe
84
96
  status == "success"
85
97
  end
86
98
 
99
+ def state_started?
100
+ state.key?("EnteredTime")
101
+ end
102
+
103
+ # State#running? also checks docker to see if it is running.
104
+ # You possibly want to use that instead
105
+ def state_finished?
106
+ state.key?("FinishedTime")
107
+ end
108
+
87
109
  def state=(val)
88
110
  @context["State"] = val
89
111
  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
@@ -25,12 +25,9 @@ module Floe
25
25
  def set(context, value)
26
26
  result = context.dup
27
27
 
28
- # If the payload is '$' then merge the value into the context
29
- # otherwise store the value under the path
30
- #
31
- # TODO: how to handle non-hash values, raise error if path=$ and value not a hash?
28
+ # If the payload is '$' then replace the output with the value
32
29
  if path.empty?
33
- result.merge!(value)
30
+ result = value.dup
34
31
  else
35
32
  child = result
36
33
  keys = path.dup
@@ -3,19 +3,26 @@
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
 
23
+ # @param [Integer] attempt 1 for the first attempt
17
24
  def sleep_duration(attempt)
18
- interval_seconds * (backoff_rate * attempt)
25
+ interval_seconds * (backoff_rate**(attempt - 1))
19
26
  end
20
27
  end
21
28
  end