floe 0.13.1 → 0.15.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/.codeclimate.yml +21 -1
- data/CHANGELOG.md +27 -1
- data/README.md +8 -0
- data/examples/map.asl +46 -0
- data/examples/parallel.asl +32 -0
- data/lib/floe/cli.rb +15 -8
- data/lib/floe/container_runner/docker.rb +10 -9
- data/lib/floe/container_runner/kubernetes.rb +10 -8
- data/lib/floe/container_runner/podman.rb +8 -5
- data/lib/floe/validation_mixin.rb +9 -1
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/branch.rb +8 -0
- data/lib/floe/workflow/choice_rule/data.rb +88 -32
- data/lib/floe/workflow/item_processor.rb +14 -0
- data/lib/floe/workflow/state.rb +2 -2
- data/lib/floe/workflow/states/child_workflow_mixin.rb +58 -0
- data/lib/floe/workflow/states/choice.rb +6 -6
- data/lib/floe/workflow/states/map.rb +116 -2
- data/lib/floe/workflow/states/parallel.rb +65 -2
- data/lib/floe/workflow/states/retry_catch_mixin.rb +57 -0
- data/lib/floe/workflow/states/task.rb +1 -48
- data/lib/floe/workflow.rb +16 -35
- data/lib/floe/workflow_base.rb +108 -0
- data/lib/floe.rb +21 -3
- data/tools/step_functions +110 -0
- metadata +10 -3
- data/sig/floe.rbs/floe.rbs +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 75d5be2f5b9cdcfc64b4b3994d32b9e3955486fd99bf4e05c62c2414485188c8
|
4
|
+
data.tar.gz: 7257009d5942f157f2ccef6499c77e1f9033e0ac4846a08f3f589943bd4794ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd9275c7e845841fc472e3e0ec92593354c6293bae3c8dc9d7258acb71fcd5db7abaca2c2aadcc4c7701952b65eae8a14da29c1e8e26d71a7d9df2c6aad9abeb
|
7
|
+
data.tar.gz: 8cc089f45244d80428d92feeeb162d8c65fafcea915e00405c54f5340dee4f9106113ad87e36c97a18fd159c3d338bee0f78869e91d200f19b145df494e5edfd
|
data/.codeclimate.yml
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
version: '2'
|
1
2
|
prepare:
|
2
3
|
fetch:
|
3
4
|
- url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_base.yml
|
@@ -8,9 +9,28 @@ prepare:
|
|
8
9
|
path: styles/base.yml
|
9
10
|
- url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/cc_base.yml
|
10
11
|
path: styles/cc_base.yml
|
12
|
+
checks:
|
13
|
+
argument-count:
|
14
|
+
enabled: false
|
15
|
+
complex-logic:
|
16
|
+
enabled: false
|
17
|
+
file-lines:
|
18
|
+
enabled: false
|
19
|
+
method-complexity:
|
20
|
+
config:
|
21
|
+
threshold: 11
|
22
|
+
method-count:
|
23
|
+
enabled: false
|
24
|
+
method-lines:
|
25
|
+
enabled: false
|
26
|
+
nested-control-flow:
|
27
|
+
enabled: false
|
28
|
+
return-statements:
|
29
|
+
enabled: false
|
11
30
|
plugins:
|
12
31
|
rubocop:
|
13
32
|
enabled: true
|
14
33
|
config: ".rubocop_cc.yml"
|
15
34
|
channel: rubocop-1-56-3
|
16
|
-
|
35
|
+
exclude_patterns:
|
36
|
+
- spec/
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,31 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
4
4
|
|
5
5
|
## [Unreleased]
|
6
6
|
|
7
|
+
## [0.15.0] - 2024-10-28
|
8
|
+
### Added
|
9
|
+
- Add WorkflowBase base class for Workflow ([#279](https://github.com/ManageIQ/floe/pull/279))
|
10
|
+
- Add tool for using the aws stepfunctions simulator ([#244](https://github.com/ManageIQ/floe/pull/244))
|
11
|
+
- Implement Map state ([#184](https://github.com/ManageIQ/floe/pull/184))
|
12
|
+
- Add Map State Tolerated Failure ([#282](https://github.com/ManageIQ/floe/pull/282))
|
13
|
+
- Run Map iterations in parallel up to MaxConcurrency ([#283](https://github.com/ManageIQ/floe/pull/283))
|
14
|
+
- Implement Parallel State ([#291](https://github.com/ManageIQ/floe/pull/291))
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
- More granular compare_key and determine path at initialization time ([#274](https://github.com/ManageIQ/floe/pull/274))
|
18
|
+
- For Choice validation, use instance variables and not payload ([#277](https://github.com/ManageIQ/floe/pull/277))
|
19
|
+
- Return ExceedToleratedFailureThreshold if ToleratedFailureCount/Percentage is present ([#285](https://github.com/ManageIQ/floe/pull/285))
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
- Fix case on log messages ([#280](https://github.com/ManageIQ/floe/pull/280))
|
23
|
+
- Handle either ToleratedFailureCount or ToleratedFailurePercentage ([#284](https://github.com/ManageIQ/floe/pull/284))
|
24
|
+
|
25
|
+
## [0.14.0] - 2024-08-20
|
26
|
+
### Added
|
27
|
+
- Implement "IsNumeric": false ([#266](https://github.com/ManageIQ/floe/pull/266))
|
28
|
+
- Support choices that do not have a Default defined ([#267](https://github.com/ManageIQ/floe/pull/267))
|
29
|
+
- Label containers/pods with workflow Execution ID ([#268](https://github.com/ManageIQ/floe/pull/268))
|
30
|
+
- Allow for Execution Id to be passed in ([#269](https://github.com/ManageIQ/floe/pull/269))
|
31
|
+
|
7
32
|
## [0.13.1] - 2024-08-16
|
8
33
|
### Fixed
|
9
34
|
- Fix podman/docker container_ref trailing newline ([#265](https://github.com/ManageIQ/floe/pull/265))
|
@@ -242,7 +267,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
242
267
|
### Added
|
243
268
|
- Initial release
|
244
269
|
|
245
|
-
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.
|
270
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.14.0...HEAD
|
271
|
+
[0.14.0]: https://github.com/ManageIQ/floe/compare/v0.13.1...v0.14.0
|
246
272
|
[0.13.1]: https://github.com/ManageIQ/floe/compare/v0.13.0...v0.13.1
|
247
273
|
[0.13.0]: https://github.com/ManageIQ/floe/compare/v0.12.0...v0.13.0
|
248
274
|
[0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
|
data/README.md
CHANGED
@@ -197,6 +197,14 @@ Options supported by the kubernetes docker runner are:
|
|
197
197
|
* `ca_file` - Path to a certificate-authority file for the kubernetes API, only valid if server and token are passed. If present `/run/secrets/kubernetes.io/serviceaccount/ca.crt` will be used
|
198
198
|
* `verify_ssl` - Controls if the kubernetes API certificate-authority should be verified, defaults to "true", only vaild if server and token are passed
|
199
199
|
|
200
|
+
## Features Not Yet Supported
|
201
|
+
|
202
|
+
The following are not yet supported:
|
203
|
+
- Map State Fields:
|
204
|
+
- ItemReader
|
205
|
+
- ItemSelector/ItemBatcher
|
206
|
+
- ResultWriter
|
207
|
+
|
200
208
|
## Development
|
201
209
|
|
202
210
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/examples/map.asl
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
{
|
2
|
+
"Comment": "Using Map state in Inline mode",
|
3
|
+
"StartAt": "Pass",
|
4
|
+
"States": {
|
5
|
+
"Pass": {
|
6
|
+
"Type": "Pass",
|
7
|
+
"Next": "Map demo",
|
8
|
+
"Result": {
|
9
|
+
"foo": "bar",
|
10
|
+
"colors": [
|
11
|
+
"red",
|
12
|
+
"green",
|
13
|
+
"blue",
|
14
|
+
"yellow",
|
15
|
+
"white"
|
16
|
+
]
|
17
|
+
}
|
18
|
+
},
|
19
|
+
"Map demo": {
|
20
|
+
"Type": "Map",
|
21
|
+
"ItemsPath": "$.colors",
|
22
|
+
"MaxConcurrency": 2,
|
23
|
+
"ItemProcessor": {
|
24
|
+
"ProcessorConfig": {
|
25
|
+
"Mode": "INLINE"
|
26
|
+
},
|
27
|
+
"StartAt": "Generate UUID",
|
28
|
+
"States": {
|
29
|
+
"Generate UUID": {
|
30
|
+
"Type": "Pass",
|
31
|
+
"Next": "Sleep",
|
32
|
+
"Parameters": {
|
33
|
+
"uuid.$": "States.UUID()"
|
34
|
+
}
|
35
|
+
},
|
36
|
+
"Sleep": {
|
37
|
+
"Type": "Task",
|
38
|
+
"Resource": "docker://docker.io/agrare/sleep:latest",
|
39
|
+
"End": true
|
40
|
+
}
|
41
|
+
}
|
42
|
+
},
|
43
|
+
"End": true
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
{
|
2
|
+
"Comment": "Parallel Example.",
|
3
|
+
"StartAt": "FunWithMath",
|
4
|
+
"States": {
|
5
|
+
"FunWithMath": {
|
6
|
+
"Type": "Parallel",
|
7
|
+
"End": true,
|
8
|
+
"Branches": [
|
9
|
+
{
|
10
|
+
"StartAt": "Add",
|
11
|
+
"States": {
|
12
|
+
"Add": {
|
13
|
+
"Type": "Task",
|
14
|
+
"Resource": "docker://docker.io/agrare/sleep:latest",
|
15
|
+
"End": true
|
16
|
+
}
|
17
|
+
}
|
18
|
+
},
|
19
|
+
{
|
20
|
+
"StartAt": "Subtract",
|
21
|
+
"States": {
|
22
|
+
"Subtract": {
|
23
|
+
"Type": "Task",
|
24
|
+
"Resource": "docker://docker.io/agrare/sleep:latest",
|
25
|
+
"End": true
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
]
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
data/lib/floe/cli.rb
CHANGED
@@ -13,17 +13,11 @@ module Floe
|
|
13
13
|
def run(args = ARGV)
|
14
14
|
workflows_inputs, opts = parse_options!(args)
|
15
15
|
|
16
|
-
credentials =
|
17
|
-
if opts[:credentials_given]
|
18
|
-
opts[:credentials] == "-" ? $stdin.read : opts[:credentials]
|
19
|
-
elsif opts[:credentials_file_given]
|
20
|
-
File.read(opts[:credentials_file])
|
21
|
-
end
|
16
|
+
credentials = create_credentials(opts)
|
22
17
|
|
23
18
|
workflows =
|
24
19
|
workflows_inputs.each_slice(2).map do |workflow, input|
|
25
|
-
|
26
|
-
Floe::Workflow.load(workflow, context)
|
20
|
+
create_workflow(workflow, opts[:context], input, credentials)
|
27
21
|
end
|
28
22
|
|
29
23
|
Floe::Workflow.wait(workflows, &:run_nonblock)
|
@@ -82,5 +76,18 @@ module Floe
|
|
82
76
|
|
83
77
|
return workflows_inputs, opts
|
84
78
|
end
|
79
|
+
|
80
|
+
def create_credentials(opts)
|
81
|
+
if opts[:credentials_given]
|
82
|
+
opts[:credentials] == "-" ? $stdin.read : opts[:credentials]
|
83
|
+
elsif opts[:credentials_file_given]
|
84
|
+
File.read(opts[:credentials_file])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_workflow(workflow, context_payload, input, credentials)
|
89
|
+
context = Floe::Workflow::Context.new(context_payload, :input => input, :credentials => credentials)
|
90
|
+
Floe::Workflow.load(workflow, context)
|
91
|
+
end
|
85
92
|
end
|
86
93
|
end
|
@@ -18,11 +18,11 @@ module Floe
|
|
18
18
|
@pull_policy = options["pull-policy"]
|
19
19
|
end
|
20
20
|
|
21
|
-
def run_async!(resource, env
|
21
|
+
def run_async!(resource, env, secrets, context)
|
22
22
|
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
23
23
|
|
24
|
-
image
|
25
|
-
|
24
|
+
image = resource.sub("docker://", "")
|
25
|
+
execution_id = context.execution["Id"]
|
26
26
|
runner_context = {}
|
27
27
|
|
28
28
|
if secrets && !secrets.empty?
|
@@ -30,7 +30,7 @@ module Floe
|
|
30
30
|
end
|
31
31
|
|
32
32
|
begin
|
33
|
-
runner_context["container_ref"] = run_container(image, env, runner_context["secrets_ref"])
|
33
|
+
runner_context["container_ref"] = run_container(image, env, execution_id, runner_context["secrets_ref"])
|
34
34
|
runner_context
|
35
35
|
rescue AwesomeSpawn::CommandResultError => err
|
36
36
|
cleanup(runner_context)
|
@@ -123,8 +123,8 @@ module Floe
|
|
123
123
|
|
124
124
|
attr_reader :network
|
125
125
|
|
126
|
-
def run_container(image, env, secrets_file)
|
127
|
-
params = run_container_params(image, env, secrets_file)
|
126
|
+
def run_container(image, env, execution_id, secrets_file)
|
127
|
+
params = run_container_params(image, env, execution_id, secrets_file)
|
128
128
|
|
129
129
|
logger.debug("Running #{AwesomeSpawn.build_command_line(self.class::DOCKER_COMMAND, params)}")
|
130
130
|
|
@@ -132,13 +132,14 @@ module Floe
|
|
132
132
|
result.output.chomp
|
133
133
|
end
|
134
134
|
|
135
|
-
def run_container_params(image, env, secrets_file)
|
135
|
+
def run_container_params(image, env, execution_id, secrets_file)
|
136
136
|
params = ["run"]
|
137
137
|
params << :detach
|
138
138
|
params += env.map { |k, v| [:e, "#{k}=#{v}"] }
|
139
139
|
params << [:e, "_CREDENTIALS=/run/secrets"] if secrets_file
|
140
140
|
params << [:pull, @pull_policy] if @pull_policy
|
141
141
|
params << [:net, "host"] if @network == "host"
|
142
|
+
params << [:label, "execution_id=#{execution_id}"]
|
142
143
|
params << [:v, "#{secrets_file}:/run/secrets:z"] if secrets_file
|
143
144
|
params << [:name, container_name(image)]
|
144
145
|
params << image
|
@@ -157,11 +158,11 @@ module Floe
|
|
157
158
|
event = docker_event_status_to_event(status)
|
158
159
|
running = event != :delete
|
159
160
|
|
160
|
-
name, exit_code = notice.dig("Actor", "Attributes")&.values_at("name", "exitCode")
|
161
|
+
name, exit_code, execution_id = notice.dig("Actor", "Attributes")&.values_at("name", "exitCode", "execution_id")
|
161
162
|
|
162
163
|
runner_context = {"container_ref" => name, "container_state" => {"Running" => running, "ExitCode" => exit_code.to_i}}
|
163
164
|
|
164
|
-
[event, runner_context]
|
165
|
+
[event, {"execution_id" => execution_id, "runner_context" => runner_context}]
|
165
166
|
rescue JSON::ParserError
|
166
167
|
[]
|
167
168
|
end
|
@@ -45,17 +45,17 @@ module Floe
|
|
45
45
|
super
|
46
46
|
end
|
47
47
|
|
48
|
-
def run_async!(resource, env
|
48
|
+
def run_async!(resource, env, secrets, context)
|
49
49
|
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
50
50
|
|
51
51
|
image = resource.sub("docker://", "")
|
52
52
|
name = container_name(image)
|
53
53
|
secret = create_secret!(secrets) if secrets && !secrets.empty?
|
54
|
-
|
54
|
+
execution_id = context.execution["Id"]
|
55
55
|
runner_context = {"container_ref" => name, "container_state" => {"phase" => "Pending"}, "secrets_ref" => secret}
|
56
56
|
|
57
57
|
begin
|
58
|
-
create_pod!(name, image, env, secret)
|
58
|
+
create_pod!(name, image, env, execution_id, secret)
|
59
59
|
runner_context
|
60
60
|
rescue Kubeclient::HttpError => err
|
61
61
|
cleanup(runner_context)
|
@@ -171,13 +171,14 @@ module Floe
|
|
171
171
|
failed_container_states(context).any?
|
172
172
|
end
|
173
173
|
|
174
|
-
def pod_spec(name, image, env, secret = nil)
|
174
|
+
def pod_spec(name, image, env, execution_id, secret = nil)
|
175
175
|
spec = {
|
176
176
|
:kind => "Pod",
|
177
177
|
:apiVersion => "v1",
|
178
178
|
:metadata => {
|
179
179
|
:name => name,
|
180
|
-
:namespace => namespace
|
180
|
+
:namespace => namespace,
|
181
|
+
:labels => {"execution_id" => execution_id}
|
181
182
|
},
|
182
183
|
:spec => {
|
183
184
|
:containers => [
|
@@ -219,8 +220,8 @@ module Floe
|
|
219
220
|
spec
|
220
221
|
end
|
221
222
|
|
222
|
-
def create_pod!(name, image, env, secret = nil)
|
223
|
-
kubeclient.create_pod(pod_spec(name, image, env, secret))
|
223
|
+
def create_pod!(name, image, env, execution_id, secret = nil)
|
224
|
+
kubeclient.create_pod(pod_spec(name, image, env, execution_id, secret))
|
224
225
|
end
|
225
226
|
|
226
227
|
def delete_pod!(name)
|
@@ -294,9 +295,10 @@ module Floe
|
|
294
295
|
|
295
296
|
pod = notice.object
|
296
297
|
container_ref = pod.metadata.name
|
298
|
+
execution_id = pod.metadata.labels["execution_id"]
|
297
299
|
container_state = pod.to_h[:status].deep_stringify_keys
|
298
300
|
|
299
|
-
{"container_ref" => container_ref, "container_state" => container_state}
|
301
|
+
{"execution_id" => execution_id, "runner_context" => {"container_ref" => container_ref, "container_state" => container_state}}
|
300
302
|
end
|
301
303
|
|
302
304
|
def kubeclient
|
@@ -30,13 +30,14 @@ module Floe
|
|
30
30
|
|
31
31
|
private
|
32
32
|
|
33
|
-
def run_container_params(image, env, secret)
|
33
|
+
def run_container_params(image, env, execution_id, secret)
|
34
34
|
params = ["run"]
|
35
35
|
params << :detach
|
36
36
|
params += env.map { |k, v| [:e, "#{k}=#{v}"] }
|
37
37
|
params << [:e, "_CREDENTIALS=/run/secrets/#{secret}"] if secret
|
38
38
|
params << [:pull, @pull_policy] if @pull_policy
|
39
39
|
params << [:net, "host"] if @network == "host"
|
40
|
+
params << [:label, "execution_id=#{execution_id}"]
|
40
41
|
params << [:secret, secret] if secret
|
41
42
|
params << [:name, container_name(image)]
|
42
43
|
params << image
|
@@ -55,14 +56,16 @@ module Floe
|
|
55
56
|
end
|
56
57
|
|
57
58
|
def parse_notice(notice)
|
58
|
-
|
59
|
+
notice = JSON.parse(notice)
|
60
|
+
id, status, exit_code, attributes = notice.values_at("ID", "Status", "ContainerExitCode", "Attributes")
|
59
61
|
|
60
|
-
|
61
|
-
|
62
|
+
execution_id = attributes&.dig("execution_id")
|
63
|
+
event = podman_event_status_to_event(status)
|
64
|
+
running = event != :delete
|
62
65
|
|
63
66
|
runner_context = {"container_ref" => id, "container_state" => {"Running" => running, "ExitCode" => exit_code.to_i}}
|
64
67
|
|
65
|
-
[event, runner_context]
|
68
|
+
[event, {"execution_id" => execution_id, "runner_context" => runner_context}]
|
66
69
|
rescue JSON::ParserError
|
67
70
|
[]
|
68
71
|
end
|
@@ -18,6 +18,10 @@ module Floe
|
|
18
18
|
self.class.invalid_field_error!(name, field_name, field_value, comment)
|
19
19
|
end
|
20
20
|
|
21
|
+
def runtime_field_error!(field_name, field_value, comment, floe_error: "States.Runtime")
|
22
|
+
raise Floe::ExecutionError.new(self.class.field_error_text(name, field_name, field_value, comment), floe_error)
|
23
|
+
end
|
24
|
+
|
21
25
|
def workflow_state?(field_value, workflow)
|
22
26
|
workflow.payload["States"] ? workflow.payload["States"].include?(field_value) : true
|
23
27
|
end
|
@@ -39,10 +43,14 @@ module Floe
|
|
39
43
|
end
|
40
44
|
|
41
45
|
def invalid_field_error!(name, field_name, field_value, comment)
|
46
|
+
raise Floe::InvalidWorkflowError, field_error_text(name, field_name, field_value, comment)
|
47
|
+
end
|
48
|
+
|
49
|
+
def field_error_text(name, field_name, field_value, comment = nil)
|
42
50
|
# instead of displaying a large hash or array, just displaying the word Hash or Array
|
43
51
|
field_value = field_value.class if field_value.kind_of?(Hash) || field_value.kind_of?(Array)
|
44
52
|
|
45
|
-
|
53
|
+
"#{Array(name).join(".")} field \"#{field_name}\"#{" value \"#{field_value}\"" unless field_value.nil?} #{comment}"
|
46
54
|
end
|
47
55
|
end
|
48
56
|
end
|
data/lib/floe/version.rb
CHANGED
@@ -4,16 +4,20 @@ module Floe
|
|
4
4
|
class Workflow
|
5
5
|
class ChoiceRule
|
6
6
|
class Data < Floe::Workflow::ChoiceRule
|
7
|
-
|
7
|
+
TYPES = ["String", "Numeric", "Boolean", "Timestamp", "Present", "Null"].freeze
|
8
|
+
COMPARES = ["Equals", "LessThan", "GreaterThan", "LessThanEquals", "GreaterThanEquals", "Matches"].freeze
|
9
|
+
# e.g.: (Is)(String), (Is)(Present)
|
10
|
+
TYPE_CHECK = /^(Is)(#{TYPES.join("|")})$/.freeze
|
11
|
+
# e.g.: (String)(LessThan)(Path), (Numeric)(GreaterThanEquals)()
|
12
|
+
OPERATION = /^(#{(TYPES - %w[Null Present]).join("|")})(#{COMPARES.join("|")})(Path)?$/.freeze
|
8
13
|
|
9
|
-
attr_reader :variable, :compare_key, :
|
14
|
+
attr_reader :variable, :compare_key, :type, :compare_predicate, :path
|
10
15
|
|
11
16
|
def initialize(_workflow, _name, payload)
|
12
17
|
super
|
13
18
|
|
14
|
-
@variable = parse_path("Variable"
|
19
|
+
@variable = parse_path("Variable")
|
15
20
|
parse_compare_key
|
16
|
-
@value = path ? parse_path(compare_key, payload) : payload[compare_key]
|
17
21
|
end
|
18
22
|
|
19
23
|
def true?(context, input)
|
@@ -23,11 +27,11 @@ module Floe
|
|
23
27
|
rhs = compare_value(context, input)
|
24
28
|
|
25
29
|
case compare_key
|
26
|
-
when "IsNull" then is_null?(lhs)
|
27
|
-
when "IsNumeric" then is_numeric?(lhs)
|
28
|
-
when "IsString" then is_string?(lhs)
|
29
|
-
when "IsBoolean" then is_boolean?(lhs)
|
30
|
-
when "IsTimestamp" then is_timestamp?(lhs)
|
30
|
+
when "IsNull" then is_null?(lhs, rhs)
|
31
|
+
when "IsNumeric" then is_numeric?(lhs, rhs)
|
32
|
+
when "IsString" then is_string?(lhs, rhs)
|
33
|
+
when "IsBoolean" then is_boolean?(lhs, rhs)
|
34
|
+
when "IsTimestamp" then is_timestamp?(lhs, rhs)
|
31
35
|
when "StringEquals", "StringEqualsPath",
|
32
36
|
"NumericEquals", "NumericEqualsPath",
|
33
37
|
"BooleanEquals", "BooleanEqualsPath",
|
@@ -62,68 +66,120 @@ module Floe
|
|
62
66
|
# Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
|
63
67
|
# If true then return true when present.
|
64
68
|
# If false then return true when not present.
|
65
|
-
|
69
|
+
predicate = compare_value(context, input)
|
66
70
|
# Don't need the variable_value, just need to see if the path finds the value.
|
67
71
|
variable_value(context, input)
|
68
72
|
|
69
73
|
# The variable_value is present
|
70
|
-
# If
|
71
|
-
|
74
|
+
# If predicate is true, then presence check was successful, return true.
|
75
|
+
predicate
|
72
76
|
rescue Floe::PathError
|
73
77
|
# variable_value is not present. (the path lookup threw an error)
|
74
|
-
# If
|
75
|
-
!
|
78
|
+
# If predicate is false, then it successfully wasn't present, return true.
|
79
|
+
!predicate
|
76
80
|
end
|
77
81
|
|
78
|
-
|
79
|
-
|
82
|
+
# rubocop:disable Naming/PredicateName
|
83
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
84
|
+
def is_null?(value, predicate = true)
|
85
|
+
value.nil? == predicate
|
80
86
|
end
|
81
87
|
|
82
|
-
def is_present?(value
|
83
|
-
!value.nil?
|
88
|
+
def is_present?(value, predicate = true)
|
89
|
+
!value.nil? == predicate
|
84
90
|
end
|
85
91
|
|
86
|
-
def is_numeric?(value
|
87
|
-
value.kind_of?(Numeric)
|
92
|
+
def is_numeric?(value, predicate = true)
|
93
|
+
value.kind_of?(Numeric) == predicate
|
88
94
|
end
|
89
95
|
|
90
|
-
def is_string?(value
|
91
|
-
value.kind_of?(String)
|
96
|
+
def is_string?(value, predicate = true)
|
97
|
+
value.kind_of?(String) == predicate
|
92
98
|
end
|
93
99
|
|
94
|
-
def is_boolean?(value
|
95
|
-
[true, false].include?(value)
|
100
|
+
def is_boolean?(value, predicate = true)
|
101
|
+
[true, false].include?(value) == predicate
|
96
102
|
end
|
97
103
|
|
98
|
-
def is_timestamp?(value
|
104
|
+
def is_timestamp?(value, predicate = true)
|
99
105
|
require "date"
|
100
106
|
|
101
107
|
DateTime.rfc3339(value)
|
102
|
-
|
108
|
+
predicate
|
103
109
|
rescue TypeError, Date::Error
|
104
|
-
|
110
|
+
!predicate
|
105
111
|
end
|
112
|
+
# rubocop:enable Naming/PredicateName
|
113
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
106
114
|
|
115
|
+
# parse the compare key at initialization time
|
107
116
|
def parse_compare_key
|
108
|
-
|
117
|
+
payload.each_key do |key|
|
118
|
+
# e.g. (String)(GreaterThan)(Path)
|
119
|
+
if (match_values = OPERATION.match(key))
|
120
|
+
@compare_key = key
|
121
|
+
@type, _operator, @path = match_values.captures
|
122
|
+
@compare_predicate = parse_predicate(type)
|
123
|
+
break
|
124
|
+
end
|
125
|
+
# e.g. (Is)(String)
|
126
|
+
if TYPE_CHECK.match?(key)
|
127
|
+
@compare_key = key
|
128
|
+
# type: nil means no runtime type checking.
|
129
|
+
@type = @path = nil
|
130
|
+
@compare_predicate = parse_predicate("Boolean")
|
131
|
+
break
|
132
|
+
end
|
133
|
+
end
|
109
134
|
parser_error!("requires a compare key") unless compare_key
|
135
|
+
end
|
110
136
|
|
111
|
-
|
137
|
+
# parse predicate at initilization time
|
138
|
+
# @return the right predicate attached to the compare key
|
139
|
+
def parse_predicate(data_type)
|
140
|
+
path ? parse_path(compare_key) : parse_field(compare_key, data_type)
|
112
141
|
end
|
113
142
|
|
143
|
+
# @return right hand predicate - input path or static payload value)
|
114
144
|
def compare_value(context, input)
|
115
|
-
path ?
|
145
|
+
path ? fetch_path(compare_key, compare_predicate, context, input) : compare_predicate
|
116
146
|
end
|
117
147
|
|
148
|
+
# feth the variable value at runtime
|
149
|
+
# @return variable value (left hand side )
|
118
150
|
def variable_value(context, input)
|
119
|
-
variable
|
151
|
+
fetch_path("Variable", variable, context, input)
|
120
152
|
end
|
121
153
|
|
122
|
-
|
154
|
+
# parse path at initilization time
|
155
|
+
# helper method to parse a path from the payload
|
156
|
+
def parse_path(field_name)
|
123
157
|
value = payload[field_name]
|
124
158
|
missing_field_error!(field_name) unless value
|
125
159
|
wrap_parser_error(field_name, value) { Path.new(value) }
|
126
160
|
end
|
161
|
+
|
162
|
+
# parse predicate field at initialization time
|
163
|
+
def parse_field(field_name, data_type)
|
164
|
+
value = payload[field_name]
|
165
|
+
return value if correct_type?(value, data_type)
|
166
|
+
|
167
|
+
invalid_field_error!(field_name, value, "required to be a #{data_type}")
|
168
|
+
end
|
169
|
+
|
170
|
+
# fetch a path at runtime
|
171
|
+
def fetch_path(field_name, field_path, context, input)
|
172
|
+
value = field_path.value(context, input)
|
173
|
+
return value if type.nil? || correct_type?(value, type)
|
174
|
+
|
175
|
+
runtime_field_error!(field_name, field_path.to_s, "required to point to a #{type}")
|
176
|
+
end
|
177
|
+
|
178
|
+
# if we have runtime checking, check against that type
|
179
|
+
# otherwise assume checking a TYPE_CHECK predicate and check against Boolean
|
180
|
+
def correct_type?(value, data_type)
|
181
|
+
send("is_#{data_type.downcase}?".to_sym, value)
|
182
|
+
end
|
127
183
|
end
|
128
184
|
end
|
129
185
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class ItemProcessor < Floe::WorkflowBase
|
6
|
+
attr_reader :processor_config
|
7
|
+
|
8
|
+
def initialize(payload, name = nil)
|
9
|
+
super
|
10
|
+
@processor_config = payload.fetch("ProcessorConfig", "INLINE")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/floe/workflow/state.rb
CHANGED
@@ -48,7 +48,7 @@ module Floe
|
|
48
48
|
return Errno::EAGAIN unless ready?(context)
|
49
49
|
|
50
50
|
finish(context)
|
51
|
-
rescue Floe::
|
51
|
+
rescue Floe::ExecutionError => e
|
52
52
|
mark_error(context, e)
|
53
53
|
end
|
54
54
|
|
@@ -82,7 +82,7 @@ module Floe
|
|
82
82
|
def mark_error(context, exception)
|
83
83
|
# InputPath or OutputPath were bad.
|
84
84
|
context.next_state = nil
|
85
|
-
context.output = {"Error" =>
|
85
|
+
context.output = {"Error" => exception.floe_error, "Cause" => exception.message}
|
86
86
|
# Since finish threw an exception, super was never called. Calling that now.
|
87
87
|
mark_finished(context)
|
88
88
|
end
|