floe 0.11.3 → 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.
- 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
|