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 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