floe 0.11.2 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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