floe 0.12.0 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -1
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/choice_rule/data.rb +17 -5
- data/lib/floe/workflow/intrinsic_function/parser.rb +9 -1
- data/lib/floe/workflow/intrinsic_function/transformer.rb +76 -6
- data/lib/floe/workflow/path.rb +12 -1
- data/lib/floe/workflow/state.rb +21 -3
- data/lib/floe.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: abe15498a790293b2a375c4538ed649bfa577edd1b7eaa38d02266fc4f7fc576
|
4
|
+
data.tar.gz: 733e0e6687ca143de8a74214f13b4a78118333a99f89a9a09a9998ab0319768a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d5dfc65c86e68d39b7c9bbeedd20e2a3ea844f158d601bb72bf35595e6d6597ac6571f7bc9ad6802f8ed33d6753b4906e7745a3e46cb38790492869aa95e6a02
|
7
|
+
data.tar.gz: 913032cb3a042e8b3464f05c7e0e65401f8532b28ded1cebd57df310899dcc8bd1bc26d9307a6b824837474c0e3ee9144477147779e0b40b6fd9c9fecfc7bddc
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,14 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
4
4
|
|
5
5
|
## [Unreleased]
|
6
6
|
|
7
|
+
## [0.13.0] - 2024-08-12
|
8
|
+
### Added
|
9
|
+
- Choice rule payload validation path ([#253](https://github.com/ManageIQ/floe/pull/253))
|
10
|
+
- Intrinsics JsonToString and StringToJson ([#256](https://github.com/ManageIQ/floe/pull/256))
|
11
|
+
- Add States.Format intrinsic function ([#258](https://github.com/ManageIQ/floe/pull/258))
|
12
|
+
- Intrinsics States.JsonMerge ([#255](https://github.com/ManageIQ/floe/pull/255))
|
13
|
+
- Enable support for Hashes in States.Hash ([#260](https://github.com/ManageIQ/floe/pull/260))
|
14
|
+
|
7
15
|
## [0.12.0] - 2024-07-31
|
8
16
|
### Added
|
9
17
|
- Set Floe.logger.level if DEBUG env var set ([#234](https://github.com/ManageIQ/floe/pull/234))
|
@@ -225,7 +233,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
225
233
|
### Added
|
226
234
|
- Initial release
|
227
235
|
|
228
|
-
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.
|
236
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.13.0...HEAD
|
237
|
+
[0.13.0]: https://github.com/ManageIQ/floe/compare/v0.12.0...v0.13.0
|
229
238
|
[0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
|
230
239
|
[0.11.3]: https://github.com/ManageIQ/floe/compare/v0.11.2...v0.11.3
|
231
240
|
[0.11.2]: https://github.com/ManageIQ/floe/compare/v0.11.1...v0.11.2
|
data/lib/floe/version.rb
CHANGED
@@ -5,14 +5,13 @@ module Floe
|
|
5
5
|
class ChoiceRule
|
6
6
|
class Data < Floe::Workflow::ChoiceRule
|
7
7
|
def true?(context, input)
|
8
|
+
return presence_check(context, input) if compare_key == "IsPresent"
|
9
|
+
|
8
10
|
lhs = variable_value(context, input)
|
9
11
|
rhs = compare_value(context, input)
|
10
12
|
|
11
|
-
validate!(lhs)
|
12
|
-
|
13
13
|
case compare_key
|
14
14
|
when "IsNull" then is_null?(lhs)
|
15
|
-
when "IsPresent" then is_present?(lhs)
|
16
15
|
when "IsNumeric" then is_numeric?(lhs)
|
17
16
|
when "IsString" then is_string?(lhs)
|
18
17
|
when "IsBoolean" then is_boolean?(lhs)
|
@@ -47,8 +46,21 @@ module Floe
|
|
47
46
|
|
48
47
|
private
|
49
48
|
|
50
|
-
def
|
51
|
-
|
49
|
+
def presence_check(context, input)
|
50
|
+
# Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
|
51
|
+
# If true then return true when present.
|
52
|
+
# If false then return true when not present.
|
53
|
+
rhs = compare_value(context, input)
|
54
|
+
# Don't need the variable_value, just need to see if the path finds the value.
|
55
|
+
variable_value(context, input)
|
56
|
+
|
57
|
+
# The variable_value is present
|
58
|
+
# If rhs is true, then presence check was successful, return true.
|
59
|
+
rhs
|
60
|
+
rescue Floe::PathError
|
61
|
+
# variable_value is not present. (the path lookup threw an error)
|
62
|
+
# If rhs is false, then it successfully wasn't present, return true.
|
63
|
+
!rhs
|
52
64
|
end
|
53
65
|
|
54
66
|
def is_null?(value) # rubocop:disable Naming/PredicateName
|
@@ -54,6 +54,9 @@ module Floe
|
|
54
54
|
end
|
55
55
|
|
56
56
|
[
|
57
|
+
:states_format, "States.Format",
|
58
|
+
:states_string_to_json, "States.StringToJson",
|
59
|
+
:states_json_to_string, "States.JsonToString",
|
57
60
|
:states_array, "States.Array",
|
58
61
|
:states_array_partition, "States.ArrayPartition",
|
59
62
|
:states_array_contains, "States.ArrayContains",
|
@@ -64,6 +67,7 @@ module Floe
|
|
64
67
|
:states_base64_encode, "States.Base64Encode",
|
65
68
|
:states_base64_decode, "States.Base64Decode",
|
66
69
|
:states_hash, "States.Hash",
|
70
|
+
:states_json_merge, "States.JsonMerge",
|
67
71
|
:states_math_random, "States.MathRandom",
|
68
72
|
:states_math_add, "States.MathAdd",
|
69
73
|
:states_string_split, "States.StringSplit",
|
@@ -77,7 +81,10 @@ module Floe
|
|
77
81
|
end
|
78
82
|
|
79
83
|
rule(:expression) do
|
80
|
-
|
84
|
+
states_format |
|
85
|
+
states_string_to_json |
|
86
|
+
states_json_to_string |
|
87
|
+
states_array |
|
81
88
|
states_array_partition |
|
82
89
|
states_array_contains |
|
83
90
|
states_array_range |
|
@@ -87,6 +94,7 @@ module Floe
|
|
87
94
|
states_base64_encode |
|
88
95
|
states_base64_decode |
|
89
96
|
states_hash |
|
97
|
+
states_json_merge |
|
90
98
|
states_math_random |
|
91
99
|
states_math_add |
|
92
100
|
states_string_split |
|
@@ -9,6 +9,7 @@ module Floe
|
|
9
9
|
class IntrinsicFunction
|
10
10
|
class Transformer < Parslet::Transform
|
11
11
|
OptionalArg = Struct.new(:type)
|
12
|
+
VariadicArgs = Struct.new(:type)
|
12
13
|
|
13
14
|
class << self
|
14
15
|
def process_args(args, function, signature = nil)
|
@@ -37,20 +38,33 @@ module Floe
|
|
37
38
|
|
38
39
|
def check_arity(args, function, signature)
|
39
40
|
if signature.any?(OptionalArg)
|
40
|
-
|
41
|
-
signature_size = (
|
41
|
+
signature_required = signature.reject { |a| a.kind_of?(OptionalArg) }
|
42
|
+
signature_size = (signature_required.size..signature.size)
|
42
43
|
|
43
44
|
raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature_size})" unless signature_size.include?(args.size)
|
45
|
+
elsif signature.any?(VariadicArgs)
|
46
|
+
signature_required = signature.reject { |a| a.kind_of?(VariadicArgs) }
|
47
|
+
|
48
|
+
raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected at least #{signature_required.size})" unless args.size >= signature_required.size
|
44
49
|
else
|
45
50
|
raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature.size})" unless signature.size == args.size
|
46
51
|
end
|
47
52
|
end
|
48
53
|
|
49
54
|
def check_types(args, function, signature)
|
55
|
+
# Adjust the signature for VariadicArgs to create a copy of the expected type for each given arg
|
56
|
+
if signature.last.kind_of?(VariadicArgs)
|
57
|
+
signature = signature[0..-2] + Array.new(args.size - signature.size + 1, signature.last.type)
|
58
|
+
end
|
59
|
+
|
50
60
|
args.zip(signature).each_with_index do |(arg, type), index|
|
51
61
|
type = type.type if type.kind_of?(OptionalArg)
|
52
62
|
|
53
|
-
|
63
|
+
if type.kind_of?(Array)
|
64
|
+
raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected one of #{type.join(", ")})" unless type.any? { |t| arg.kind_of?(t) }
|
65
|
+
else
|
66
|
+
raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type)
|
67
|
+
end
|
54
68
|
end
|
55
69
|
end
|
56
70
|
end
|
@@ -63,6 +77,50 @@ module Floe
|
|
63
77
|
rule(:number => simple(:v)) { v.match(/[eE.]/) ? Float(v) : Integer(v) }
|
64
78
|
rule(:jsonpath => simple(:v)) { Floe::Workflow::Path.value(v.to_s, context, input) }
|
65
79
|
|
80
|
+
STATES_FORMAT_PLACEHOLDER = /(?<!\\)\{\}/.freeze
|
81
|
+
|
82
|
+
rule(:states_format => {:args => subtree(:args)}) do
|
83
|
+
args = Transformer.process_args(args(), "States.Format", [String, VariadicArgs[[String, TrueClass, FalseClass, Integer, Float, NilClass]]])
|
84
|
+
str, *rest = *args
|
85
|
+
|
86
|
+
# TODO: Handle templates with escaped characters, including invalid templates
|
87
|
+
# See https://states-language.net/#intrinsic-functions (number 6)
|
88
|
+
|
89
|
+
expected_args = str.scan(STATES_FORMAT_PLACEHOLDER).size
|
90
|
+
actual_args = rest.size
|
91
|
+
if expected_args != actual_args
|
92
|
+
raise ArgumentError, "number of arguments to States.Format do not match the occurrences of {} (given #{actual_args}, expected #{expected_args})"
|
93
|
+
end
|
94
|
+
|
95
|
+
rest.each do |arg|
|
96
|
+
str = str.sub(STATES_FORMAT_PLACEHOLDER, arg.nil? ? "null" : arg.to_s)
|
97
|
+
end
|
98
|
+
|
99
|
+
# TODO: Handle arguments that have escape characters within them but are interpolated
|
100
|
+
str.gsub!("\\'", "'")
|
101
|
+
str.gsub!("\\{", "{")
|
102
|
+
str.gsub!("\\}", "}")
|
103
|
+
str.gsub!("\\\\", "\\")
|
104
|
+
|
105
|
+
str
|
106
|
+
end
|
107
|
+
|
108
|
+
rule(:states_json_to_string => {:args => subtree(:args)}) do
|
109
|
+
args = Transformer.process_args(args(), "States.JsonToString", [Object])
|
110
|
+
json = args.first
|
111
|
+
|
112
|
+
JSON.generate(json)
|
113
|
+
end
|
114
|
+
|
115
|
+
rule(:states_string_to_json => {:args => subtree(:args)}) do
|
116
|
+
args = Transformer.process_args(args(), "States.StringToJson", [String])
|
117
|
+
str = args.first
|
118
|
+
|
119
|
+
JSON.parse(str)
|
120
|
+
rescue JSON::ParserError => e
|
121
|
+
raise ArgumentError, "invalid value for argument 1 to States.StringToJson (invalid json: #{e.message})"
|
122
|
+
end
|
123
|
+
|
66
124
|
rule(:states_array => {:args => subtree(:args)}) do
|
67
125
|
Transformer.process_args(args, "States.Array")
|
68
126
|
end
|
@@ -135,7 +193,7 @@ module Floe
|
|
135
193
|
rule(:states_hash => {:args => subtree(:args)}) do
|
136
194
|
args = Transformer.process_args(args(), "States.Hash", [Object, String])
|
137
195
|
data, algorithm = *args
|
138
|
-
|
196
|
+
|
139
197
|
if data.nil?
|
140
198
|
raise ArgumentError, "invalid value for argument 1 to States.Hash (given null, expected non-null)"
|
141
199
|
end
|
@@ -147,8 +205,20 @@ module Floe
|
|
147
205
|
|
148
206
|
require "openssl"
|
149
207
|
algorithm = algorithm.sub("-", "")
|
150
|
-
data = data
|
151
|
-
OpenSSL::Digest.hexdigest(algorithm, data)
|
208
|
+
data = JSON.generate(data) unless data.kind_of?(String)
|
209
|
+
OpenSSL::Digest.hexdigest(algorithm, data).force_encoding("UTF-8")
|
210
|
+
end
|
211
|
+
|
212
|
+
rule(:states_json_merge => {:args => subtree(:args)}) do
|
213
|
+
args = Transformer.process_args(args(), "States.JsonMerge", [Hash, Hash, [TrueClass, FalseClass]])
|
214
|
+
left, right, deep = *args
|
215
|
+
|
216
|
+
if deep
|
217
|
+
# NOTE: not implemented by AWS Step Functions and nuances not defined in docs
|
218
|
+
left.merge(right) { |_key, l, r| l.kind_of?(Hash) && r.kind_of?(Hash) ? l.merge(r) : r }
|
219
|
+
else
|
220
|
+
left.merge(right)
|
221
|
+
end
|
152
222
|
end
|
153
223
|
|
154
224
|
rule(:states_math_random => {:args => subtree(:args)}) do
|
data/lib/floe/workflow/path.rb
CHANGED
@@ -34,7 +34,18 @@ module Floe
|
|
34
34
|
return obj if path == "$"
|
35
35
|
|
36
36
|
results = JsonPath.on(obj, path)
|
37
|
-
results.count
|
37
|
+
case results.count
|
38
|
+
when 0
|
39
|
+
raise Floe::PathError, "Path [#{payload}] references an invalid value"
|
40
|
+
when 1
|
41
|
+
results.first
|
42
|
+
else
|
43
|
+
results
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
payload
|
38
49
|
end
|
39
50
|
end
|
40
51
|
end
|
data/lib/floe/workflow/state.rb
CHANGED
@@ -48,17 +48,27 @@ module Floe
|
|
48
48
|
return Errno::EAGAIN unless ready?(context)
|
49
49
|
|
50
50
|
finish(context)
|
51
|
+
rescue Floe::Error => e
|
52
|
+
mark_error(context, e)
|
51
53
|
end
|
52
54
|
|
53
55
|
def start(context)
|
56
|
+
mark_started(context)
|
57
|
+
end
|
58
|
+
|
59
|
+
def finish(context)
|
60
|
+
mark_finished(context)
|
61
|
+
end
|
62
|
+
|
63
|
+
def mark_started(context)
|
54
64
|
context.state["EnteredTime"] = Time.now.utc.iso8601
|
55
65
|
|
56
66
|
logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
|
57
67
|
end
|
58
68
|
|
59
|
-
def
|
60
|
-
finished_time
|
61
|
-
entered_time
|
69
|
+
def mark_finished(context)
|
70
|
+
finished_time = Time.now.utc
|
71
|
+
entered_time = Time.parse(context.state["EnteredTime"])
|
62
72
|
|
63
73
|
context.state["FinishedTime"] ||= finished_time.iso8601
|
64
74
|
context.state["Duration"] = finished_time - entered_time
|
@@ -69,6 +79,14 @@ module Floe
|
|
69
79
|
0
|
70
80
|
end
|
71
81
|
|
82
|
+
def mark_error(context, exception)
|
83
|
+
# InputPath or OutputPath were bad.
|
84
|
+
context.next_state = nil
|
85
|
+
context.output = {"Error" => "States.Runtime", "Cause" => exception.message}
|
86
|
+
# Since finish threw an exception, super was never called. Calling that now.
|
87
|
+
mark_finished(context)
|
88
|
+
end
|
89
|
+
|
72
90
|
def ready?(context)
|
73
91
|
!context.state_started? || !running?(context)
|
74
92
|
end
|
data/lib/floe.rb
CHANGED
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.13.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-08-
|
11
|
+
date: 2024-08-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: awesome_spawn
|