floe 0.13.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fb1e855fda7d4a00a72e494b820a89304350945c14a71675d5cc6306e69f079
4
- data.tar.gz: 9780e210073c41b4a5c534bdabd301c2df6275c13328dbb1ee43569470e8d590
3
+ metadata.gz: 75d5be2f5b9cdcfc64b4b3994d32b9e3955486fd99bf4e05c62c2414485188c8
4
+ data.tar.gz: 7257009d5942f157f2ccef6499c77e1f9033e0ac4846a08f3f589943bd4794ef
5
5
  SHA512:
6
- metadata.gz: bec5c7f6337c74258e185b76abfc3953f05ef16cf4de640972d23b7dfa81bf4439df3ce00dd539c618f7d18905d783bfa55dc777365e1a48969942b182448107
7
- data.tar.gz: cf584d349c69927dec6945fe21d652b79e96f6228754b1a91a57b3241825661ed6d977a7d53e3ebfae526551da2a4a573148e9d855840d8edc52b8ebeffc948b
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
- version: '2'
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.13.1...HEAD
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
- context = Floe::Workflow::Context.new(opts[:context], :input => input, :credentials => credentials)
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 = {}, secrets = {}, _context = {})
21
+ def run_async!(resource, env, secrets, context)
22
22
  raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
23
23
 
24
- image = resource.sub("docker://", "")
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 = {}, secrets = {}, _context = {})
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
- id, status, exit_code = JSON.parse(notice).values_at("ID", "Status", "ContainerExitCode")
59
+ notice = JSON.parse(notice)
60
+ id, status, exit_code, attributes = notice.values_at("ID", "Status", "ContainerExitCode", "Attributes")
59
61
 
60
- event = podman_event_status_to_event(status)
61
- running = event != :delete
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
- parser_error!(name, "field \"#{field_name}\"#{" value \"#{field_value}\"" unless field_value.nil?} #{comment}")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Floe
4
- VERSION = "0.13.1"
4
+ VERSION = "0.15.0"
5
5
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class Branch < Floe::WorkflowBase
6
+ end
7
+ end
8
+ end
@@ -4,16 +4,20 @@ module Floe
4
4
  class Workflow
5
5
  class ChoiceRule
6
6
  class Data < Floe::Workflow::ChoiceRule
7
- COMPARE_KEYS = %w[IsNull IsPresent IsNumeric IsString IsBoolean IsTimestamp String Numeric Boolean Timestamp].freeze
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, :value, :path
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", payload)
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
- rhs = compare_value(context, input)
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 rhs is true, then presence check was successful, return true.
71
- rhs
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 rhs is false, then it successfully wasn't present, return true.
75
- !rhs
78
+ # If predicate is false, then it successfully wasn't present, return true.
79
+ !predicate
76
80
  end
77
81
 
78
- def is_null?(value) # rubocop:disable Naming/PredicateName
79
- value.nil?
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) # rubocop:disable Naming/PredicateName
83
- !value.nil?
88
+ def is_present?(value, predicate = true)
89
+ !value.nil? == predicate
84
90
  end
85
91
 
86
- def is_numeric?(value) # rubocop:disable Naming/PredicateName
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) # rubocop:disable Naming/PredicateName
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) # rubocop:disable Naming/PredicateName
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) # rubocop:disable Naming/PredicateName
104
+ def is_timestamp?(value, predicate = true)
99
105
  require "date"
100
106
 
101
107
  DateTime.rfc3339(value)
102
- true
108
+ predicate
103
109
  rescue TypeError, Date::Error
104
- false
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
- @compare_key = payload.keys.detect { |key| key.match?(/^(#{COMPARE_KEYS.join("|")})/) }
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
- @path = compare_key.end_with?("Path")
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 ? value.value(context, input) : value
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.value(context, input)
151
+ fetch_path("Variable", variable, context, input)
120
152
  end
121
153
 
122
- def parse_path(field_name, payload)
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
@@ -48,7 +48,7 @@ module Floe
48
48
  return Errno::EAGAIN unless ready?(context)
49
49
 
50
50
  finish(context)
51
- rescue Floe::Error => e
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" => "States.Runtime", "Cause" => exception.message}
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