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 +4 -4
- data/.yamllint +1 -3
- data/CHANGELOG.md +28 -1
- data/floe.gemspec +2 -2
- data/lib/floe/cli.rb +4 -1
- data/lib/floe/container_runner/docker_mixin.rb +3 -0
- data/lib/floe/validation_mixin.rb +49 -0
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/catcher.rb +17 -3
- data/lib/floe/workflow/choice_rule.rb +14 -9
- data/lib/floe/workflow/context.rb +11 -5
- data/lib/floe/workflow/error_matcher_mixin.rb +17 -0
- data/lib/floe/workflow/intrinsic_function/parser.rb +100 -0
- data/lib/floe/workflow/intrinsic_function/transformer.rb +196 -0
- data/lib/floe/workflow/intrinsic_function.rb +34 -0
- data/lib/floe/workflow/path.rb +7 -1
- data/lib/floe/workflow/payload_template.rb +7 -4
- data/lib/floe/workflow/retrier.rb +9 -3
- data/lib/floe/workflow/state.rb +14 -11
- data/lib/floe/workflow/states/choice.rb +6 -5
- data/lib/floe/workflow/states/fail.rb +1 -1
- data/lib/floe/workflow/states/non_terminal_mixin.rb +2 -2
- data/lib/floe/workflow/states/pass.rb +2 -1
- data/lib/floe/workflow/states/succeed.rb +10 -1
- data/lib/floe/workflow/states/task.rb +11 -10
- data/lib/floe/workflow.rb +19 -9
- data/lib/floe.rb +6 -0
- metadata +23 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2eb195983f47d7cac44f18e962bf314c3ff4fefbdf3ac5cae32e960bfc2c0fb0
|
4
|
+
data.tar.gz: 6a0f8d91337297786c03e99fc96cb118ed8cb76244e52cf8253702d1517a6f5d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99f5a3c8e67a9f5c97b43795739c201a49a8f1195d40a1da38f2aa0e16d200266783336e4be9ada5d41b566c545144a4533db258546ebbf04a23c454a489e6a1
|
7
|
+
data.tar.gz: a77bfe436b37adf9dbdc2a279b9b1f70f48980062aaaec191a8e1fd57238e8b8b50b02828025e561c7ccb8429cb67fefb3f7ed044ed309d82eaae8dd8d9d407b
|
data/.yamllint
CHANGED
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.
|
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
|
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
@@ -3,14 +3,28 @@
|
|
3
3
|
module Floe
|
4
4
|
class Workflow
|
5
5
|
class Catcher
|
6
|
-
|
6
|
+
include ErrorMatcherMixin
|
7
|
+
include ValidationMixin
|
7
8
|
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
15
|
+
name += ["Or"]
|
16
|
+
Floe::Workflow::ChoiceRule::Or.new(workflow, name, payload, build_children(workflow, name, sub_payloads))
|
14
17
|
else
|
15
|
-
|
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::
|
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
|
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
|
data/lib/floe/workflow/path.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
51
|
-
when Hash
|
52
|
-
when Path
|
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
|
-
|
6
|
+
include ErrorMatcherMixin
|
7
|
+
include ValidationMixin
|
7
8
|
|
8
|
-
|
9
|
-
|
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
|
data/lib/floe/workflow/state.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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(
|
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.
|
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.
|
68
|
-
logger.public_send(level, "Running state: [#{long_name}] with input [#{context.
|
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
|
-
"#{
|
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.
|
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
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
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
|
@@ -12,8 +12,8 @@ module Floe
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def validate_state_next!(workflow)
|
15
|
-
|
16
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
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|
|
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|
|
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.
|
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.
|
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.
|
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
|
-
|
100
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
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.
|
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.
|
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-
|
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:
|
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:
|
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
|