floe 0.0.1 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +8 -1
- data/README.md +32 -13
- data/Rakefile +3 -0
- data/examples/workflow.asl +95 -0
- data/exe/floe +21 -0
- data/floe.gemspec +34 -18
- data/lib/floe/logging.rb +13 -0
- data/lib/floe/null_logger.rb +13 -0
- data/lib/floe/version.rb +3 -1
- data/lib/floe/workflow/catcher.rb +17 -0
- data/lib/floe/workflow/choice_rule/boolean.rb +19 -0
- data/lib/floe/workflow/choice_rule/data.rb +94 -0
- data/lib/floe/workflow/choice_rule.rb +41 -0
- data/lib/floe/workflow/path.rb +34 -0
- data/lib/floe/workflow/payload_template.rb +37 -0
- data/lib/floe/workflow/reference_path.rb +44 -0
- data/lib/floe/workflow/retrier.rb +22 -0
- data/lib/floe/workflow/runner/docker.rb +45 -0
- data/lib/floe/workflow/runner/kubernetes.rb +118 -0
- data/lib/floe/workflow/runner/podman.rb +42 -0
- data/lib/floe/workflow/runner.rb +33 -0
- data/lib/floe/workflow/state.rb +76 -0
- data/lib/floe/workflow/states/choice.rb +53 -0
- data/lib/floe/workflow/states/fail.rb +37 -0
- data/lib/floe/workflow/states/map.rb +13 -0
- data/lib/floe/workflow/states/parallel.rb +13 -0
- data/lib/floe/workflow/states/pass.rb +31 -0
- data/lib/floe/workflow/states/succeed.rb +26 -0
- data/lib/floe/workflow/states/task.rb +82 -0
- data/lib/floe/workflow/states/wait.rb +25 -0
- data/lib/floe/workflow.rb +82 -0
- data/lib/floe.rb +41 -2
- data/sig/floe.rbs/floe.rbs +4 -0
- metadata +116 -37
- data/.gitignore +0 -17
- data/LICENSE.txt +0 -22
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 12212f802a614d03428c29b36a882efc7b12a2ae0e61ec1ac118aea28c34d95b
|
4
|
+
data.tar.gz: 5d62f266c83af21dffce7f8c2a1a8a3686ad24f85ad5421bced3cd483903ccb8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 16f88b2b6f1cbb7baa4038f5c6f55da0a1f74b2a1a54b49e74ac24480dddb0b64d59fda3c6b26512a116d8060be3a171a68f6f52f4a9e4ff9a971737312c327c
|
7
|
+
data.tar.gz: b5a318594423446d5de524950a894bbcac334e5875cc2d52cdfcfaef08f068eb946a1a9120c1626392917a8a325944652b4a8d8fb21aa270393df25a3460fac7
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
This project adheres to [Semantic Versioning](http://semver.org/).
|
4
|
+
|
5
|
+
## [Unreleased]
|
6
|
+
|
7
|
+
## [0.1.1] - 2023-06-05
|
8
|
+
### Fixed
|
9
|
+
- Fix States::Wait Path initializer arguments (#47)
|
10
|
+
|
11
|
+
## [0.1.0] - 2023-03-13
|
12
|
+
### Added
|
13
|
+
- Initial release
|
14
|
+
|
15
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.1.0...HEAD
|
data/Gemfile
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
plugin "bundler-inject", "~> 2.0"
|
6
|
+
require File.join(Bundler::Plugin.index.load_paths("bundler-inject")[0], "bundler-inject") rescue nil
|
2
7
|
|
3
8
|
# Specify your gem's dependencies in floe.gemspec
|
4
9
|
gemspec
|
10
|
+
|
11
|
+
gem "rake", "~> 13.0"
|
data/README.md
CHANGED
@@ -1,29 +1,48 @@
|
|
1
1
|
# Floe
|
2
2
|
|
3
|
-
|
3
|
+
[![CI](https://github.com/ManageIQ/floe/actions/workflows/ci.yaml/badge.svg)](https://github.com/ManageIQ/floe/actions/workflows/ci.yaml)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/ManageIQ/floe.svg)](https://codeclimate.com/github/ManageIQ/floe)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/ManageIQ/floe/badges/coverage.svg)](https://codeclimate.com/github/ManageIQ/floe/coverage)
|
4
6
|
|
5
|
-
##
|
7
|
+
## Overview
|
6
8
|
|
7
|
-
|
9
|
+
Floe is a runner for [Amazon States Language](https://states-language.net/) workflows with support for Docker resources and running on Docker, Podman, or Kubernetes.
|
8
10
|
|
9
|
-
|
11
|
+
## Installation
|
10
12
|
|
11
|
-
|
13
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
14
|
|
13
|
-
$ bundle
|
15
|
+
$ bundle add floe
|
14
16
|
|
15
|
-
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
18
|
|
17
19
|
$ gem install floe
|
18
20
|
|
19
21
|
## Usage
|
20
22
|
|
21
|
-
|
23
|
+
Floe can be run as a command-line utility or as a ruby class.
|
24
|
+
|
25
|
+
### Command Line
|
26
|
+
|
27
|
+
```
|
28
|
+
bundle exec ruby exe/floe --workflow examples/workflow.asl --inputs='{"foo": 1}'
|
29
|
+
```
|
30
|
+
|
31
|
+
### Ruby Library
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require 'floe'
|
35
|
+
|
36
|
+
workflow = Floe::Workflow.load(File.read("workflow.asl"))
|
37
|
+
workflow.run!
|
38
|
+
```
|
39
|
+
|
40
|
+
## Development
|
41
|
+
|
42
|
+
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.
|
43
|
+
|
44
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
22
45
|
|
23
46
|
## Contributing
|
24
47
|
|
25
|
-
|
26
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
-
5. Create new Pull Request
|
48
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ManageIQ/floe.
|
data/Rakefile
CHANGED
@@ -0,0 +1,95 @@
|
|
1
|
+
{
|
2
|
+
"Comment": "An example of the Amazon States Language using a choice state.",
|
3
|
+
"StartAt": "FirstState",
|
4
|
+
"States": {
|
5
|
+
"FirstState": {
|
6
|
+
"Type": "Task",
|
7
|
+
"Resource": "docker://agrare/hello-world:latest",
|
8
|
+
"Credentials": {
|
9
|
+
"mysecret": "dont tell anyone"
|
10
|
+
},
|
11
|
+
"Retry": [
|
12
|
+
{
|
13
|
+
"ErrorEquals": [ "States.Timeout" ],
|
14
|
+
"IntervalSeconds": 3,
|
15
|
+
"MaxAttempts": 2,
|
16
|
+
"BackoffRate": 1.5
|
17
|
+
}
|
18
|
+
],
|
19
|
+
"Catch": [
|
20
|
+
{
|
21
|
+
"ErrorEquals": [ "States.ALL" ],
|
22
|
+
"Next": "FailState"
|
23
|
+
}
|
24
|
+
],
|
25
|
+
"Next": "ChoiceState"
|
26
|
+
},
|
27
|
+
|
28
|
+
"ChoiceState": {
|
29
|
+
"Type" : "Choice",
|
30
|
+
"Choices": [
|
31
|
+
{
|
32
|
+
"Variable": "$.foo",
|
33
|
+
"NumericEquals": 1,
|
34
|
+
"Next": "FirstMatchState"
|
35
|
+
},
|
36
|
+
{
|
37
|
+
"Variable": "$.foo",
|
38
|
+
"NumericEquals": 2,
|
39
|
+
"Next": "SecondMatchState"
|
40
|
+
},
|
41
|
+
{
|
42
|
+
"Variable": "$.foo",
|
43
|
+
"NumericEquals": 3,
|
44
|
+
"Next": "SuccessState"
|
45
|
+
}
|
46
|
+
],
|
47
|
+
"Default": "FailState"
|
48
|
+
},
|
49
|
+
|
50
|
+
"FirstMatchState": {
|
51
|
+
"Type" : "Task",
|
52
|
+
"Resource": "docker://agrare/hello-world:latest",
|
53
|
+
"Next": "PassState"
|
54
|
+
},
|
55
|
+
|
56
|
+
"SecondMatchState": {
|
57
|
+
"Type" : "Task",
|
58
|
+
"Resource": "docker://agrare/hello-world:latest",
|
59
|
+
"Next": "NextState"
|
60
|
+
},
|
61
|
+
|
62
|
+
"PassState": {
|
63
|
+
"Type": "Pass",
|
64
|
+
"Result": {
|
65
|
+
"foo": "bar",
|
66
|
+
"bar": "baz"
|
67
|
+
},
|
68
|
+
"ResultPath": "$.result",
|
69
|
+
"Next": "WaitState"
|
70
|
+
},
|
71
|
+
|
72
|
+
"WaitState": {
|
73
|
+
"Type": "Wait",
|
74
|
+
"Seconds": 1,
|
75
|
+
"Next": "NextState"
|
76
|
+
},
|
77
|
+
|
78
|
+
"FailState": {
|
79
|
+
"Type": "Fail",
|
80
|
+
"Error": "FailStateError",
|
81
|
+
"Cause": "No Matches!"
|
82
|
+
},
|
83
|
+
|
84
|
+
"SuccessState": {
|
85
|
+
"Type": "Succeed"
|
86
|
+
},
|
87
|
+
|
88
|
+
"NextState": {
|
89
|
+
"Type": "Task",
|
90
|
+
"Resource": "docker://agrare/hello-world:latest",
|
91
|
+
"Secrets": ["vmdb:aaa-bbb-ccc"],
|
92
|
+
"End": true
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
data/exe/floe
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "floe"
|
5
|
+
require "optimist"
|
6
|
+
|
7
|
+
opts = Optimist.options do
|
8
|
+
version "v#{Floe::VERSION}\n"
|
9
|
+
opt :workflow, "Path to your workflow json", :type => :string, :required => true
|
10
|
+
opt :inputs, "JSON payload to input to the workflow", :type => :string, :default => '{}'
|
11
|
+
opt :credentials, "JSON payload with credentials", :type => :string, :default => '{}'
|
12
|
+
end
|
13
|
+
|
14
|
+
require "logger"
|
15
|
+
Floe.logger = Logger.new(STDOUT)
|
16
|
+
|
17
|
+
workflow = Floe::Workflow.load(opts[:workflow], opts[:inputs], opts[:credentials])
|
18
|
+
|
19
|
+
output = workflow.run!
|
20
|
+
|
21
|
+
puts output.inspect
|
data/floe.gemspec
CHANGED
@@ -1,23 +1,39 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
require 'floe/version'
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/floe/version"
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
|
11
|
-
spec.
|
12
|
-
spec.
|
13
|
-
spec.homepage
|
14
|
-
spec.
|
15
|
-
|
16
|
-
spec.
|
17
|
-
|
18
|
-
spec.
|
6
|
+
spec.name = "floe"
|
7
|
+
spec.version = Floe::VERSION
|
8
|
+
spec.authors = ["ManageIQ Developers"]
|
9
|
+
|
10
|
+
spec.summary = "Simple Workflow Runner."
|
11
|
+
spec.description = "Simple Workflow Runner."
|
12
|
+
spec.homepage = "https://github.com/ManageIQ/floe"
|
13
|
+
spec.required_ruby_version = ">= 2.7.0"
|
14
|
+
|
15
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
19
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
19
30
|
spec.require_paths = ["lib"]
|
20
31
|
|
21
|
-
spec.
|
22
|
-
spec.
|
32
|
+
spec.add_dependency "awesome_spawn", "~>1.0"
|
33
|
+
spec.add_dependency "jsonpath", "~>1.1"
|
34
|
+
spec.add_dependency "optimist", "~>3.0"
|
35
|
+
spec.add_dependency "more_core_extensions"
|
36
|
+
|
37
|
+
spec.add_development_dependency "rubocop"
|
38
|
+
spec.add_development_dependency "rspec"
|
23
39
|
end
|
data/lib/floe/logging.rb
ADDED
data/lib/floe/version.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class Catcher
|
6
|
+
attr_reader :error_equals, :next, :result_path
|
7
|
+
|
8
|
+
def initialize(payload)
|
9
|
+
@payload = payload
|
10
|
+
|
11
|
+
@error_equals = payload["ErrorEquals"]
|
12
|
+
@next = payload["Next"]
|
13
|
+
@result_path = payload.fetch("ResultPath", "$")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class ChoiceRule
|
6
|
+
class Boolean < Floe::Workflow::ChoiceRule
|
7
|
+
def true?(context, input)
|
8
|
+
if payload.key?("Not")
|
9
|
+
!ChoiceRule.true?(payload["Not"], context, input)
|
10
|
+
elsif payload.key?("And")
|
11
|
+
payload["And"].all? { |choice| ChoiceRule.true?(choice, context, input) }
|
12
|
+
else
|
13
|
+
payload["Or"].any? { |choice| ChoiceRule.true?(choice, context, input) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class ChoiceRule
|
6
|
+
class Data < Floe::Workflow::ChoiceRule
|
7
|
+
def true?(context, input)
|
8
|
+
|
9
|
+
lhs = variable_value(context, input)
|
10
|
+
rhs = compare_value(context, input)
|
11
|
+
|
12
|
+
validate!(lhs)
|
13
|
+
|
14
|
+
case compare_key
|
15
|
+
when "IsNull"; is_null?(lhs)
|
16
|
+
when "IsPresent"; is_present?(lhs)
|
17
|
+
when "IsNumeric"; is_numeric?(lhs)
|
18
|
+
when "IsString"; is_string?(lhs)
|
19
|
+
when "IsBoolean"; is_boolean?(lhs)
|
20
|
+
when "IsTimestamp"; is_timestamp?(lhs)
|
21
|
+
when "StringEquals", "StringEqualsPath",
|
22
|
+
"NumericEquals", "NumericEqualsPath",
|
23
|
+
"BooleanEquals", "BooleanEqualsPath",
|
24
|
+
"TimestampEquals", "TimestampEqualsPath"
|
25
|
+
lhs == rhs
|
26
|
+
when "StringLessThan", "StringLessThanPath",
|
27
|
+
"NumericLessThan", "NumericLessThanPath",
|
28
|
+
"TimestampLessThan", "TimestampLessThanPath"
|
29
|
+
lhs < rhs
|
30
|
+
when "StringGreaterThan", "StringGreaterThanPath",
|
31
|
+
"NumericGreaterThan", "NumericGreaterThanPath",
|
32
|
+
"TimestampGreaterThan", "TimestampGreaterThanPath"
|
33
|
+
lhs > rhs
|
34
|
+
when "StringLessThanEquals", "StringLessThanEqualsPath",
|
35
|
+
"NumericLessThanEquals", "NumericLessThanEqualsPath",
|
36
|
+
"TimestampLessThanEquals", "TimestampLessThanEqualsPath"
|
37
|
+
lhs <= rhs
|
38
|
+
when "StringGreaterThanEquals", "StringGreaterThanEqualsPath",
|
39
|
+
"NumericGreaterThanEquals", "NumericGreaterThanEqualsPath",
|
40
|
+
"TimestampGreaterThanEquals", "TimestampGreaterThanEqualsPath"
|
41
|
+
lhs >= rhs
|
42
|
+
when "StringMatches"
|
43
|
+
lhs.match?(Regexp.escape(rhs).gsub('\*','.*?'))
|
44
|
+
else
|
45
|
+
raise Floe::InvalidWorkflowError, "Invalid choice [#{compare_key}]"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def validate!(value)
|
52
|
+
raise RuntimeError, "No such variable [#{variable}]" if value.nil? && !%w[IsNull IsPresent].include?(compare_key)
|
53
|
+
end
|
54
|
+
|
55
|
+
def is_null?(value)
|
56
|
+
value.nil?
|
57
|
+
end
|
58
|
+
|
59
|
+
def is_present?(value)
|
60
|
+
!value.nil?
|
61
|
+
end
|
62
|
+
|
63
|
+
def is_numeric?(value)
|
64
|
+
value.kind_of?(Integer) || value.kind_of?(Float)
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_string?(value)
|
68
|
+
value.kind_of?(String)
|
69
|
+
end
|
70
|
+
|
71
|
+
def is_boolean?(value)
|
72
|
+
[true, false].include?(value)
|
73
|
+
end
|
74
|
+
|
75
|
+
def is_timestamp?(value)
|
76
|
+
require "date"
|
77
|
+
|
78
|
+
DateTime.rfc3339(value)
|
79
|
+
true
|
80
|
+
rescue TypeError, Date::Error
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
84
|
+
def compare_key
|
85
|
+
@compare_key ||= payload.keys.detect { |key| key.match?(/^(IsNull|IsPresent|IsNumeric|IsString|IsBoolean|IsTimestamp|String|Numeric|Boolean|Timestamp)/) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def compare_value(context, input)
|
89
|
+
compare_key.end_with?("Path") ? Path.value(payload[compare_key], context, input) : payload[compare_key]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class ChoiceRule
|
6
|
+
class << self
|
7
|
+
def true?(payload, context, input)
|
8
|
+
build(payload).true?(context, input)
|
9
|
+
end
|
10
|
+
|
11
|
+
def build(payload)
|
12
|
+
data_expression = (payload.keys & %w[And Not Or]).empty?
|
13
|
+
if data_expression
|
14
|
+
Floe::Workflow::ChoiceRule::Data.new(payload)
|
15
|
+
else
|
16
|
+
Floe::Workflow::ChoiceRule::Boolean.new(payload)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :next, :payload, :variable
|
22
|
+
|
23
|
+
def initialize(payload)
|
24
|
+
@payload = payload
|
25
|
+
|
26
|
+
@next = payload["Next"]
|
27
|
+
@variable = payload["Variable"]
|
28
|
+
end
|
29
|
+
|
30
|
+
def true?(*)
|
31
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def variable_value(context, input)
|
37
|
+
@variable_value ||= Path.value(variable, context, input)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class Path
|
6
|
+
class << self
|
7
|
+
def value(payload, context, input = {})
|
8
|
+
new(payload).value(context, input)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(payload)
|
13
|
+
@payload = payload
|
14
|
+
end
|
15
|
+
|
16
|
+
def value(context, input = {})
|
17
|
+
obj, path =
|
18
|
+
if payload.start_with?("$$")
|
19
|
+
[context, payload[1..]]
|
20
|
+
else
|
21
|
+
[input, payload]
|
22
|
+
end
|
23
|
+
|
24
|
+
results = JsonPath.on(obj, path)
|
25
|
+
|
26
|
+
results.count < 2 ? results.first : results
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :payload
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class PayloadTemplate
|
6
|
+
def initialize(payload)
|
7
|
+
@payload = payload
|
8
|
+
end
|
9
|
+
|
10
|
+
def value(context, inputs = {})
|
11
|
+
interpolate_value_nested(payload, context, inputs)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :payload
|
17
|
+
|
18
|
+
def interpolate_value_nested(value, context, inputs)
|
19
|
+
case value
|
20
|
+
when Array
|
21
|
+
value.map { |val| interpolate_value_nested(val, context, inputs) }
|
22
|
+
when Hash
|
23
|
+
value.to_h do |key, val|
|
24
|
+
val = interpolate_value_nested(val, context, inputs)
|
25
|
+
key = key.gsub(/\.\$$/, "") if key.end_with?(".$")
|
26
|
+
|
27
|
+
[key, val]
|
28
|
+
end
|
29
|
+
when String
|
30
|
+
value.start_with?("$") ? Path.value(value, context, inputs) : value
|
31
|
+
else
|
32
|
+
value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class ReferencePath < Path
|
6
|
+
class << self
|
7
|
+
def set (payload, context, value)
|
8
|
+
new(payload).set(context, value)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(*)
|
13
|
+
require "more_core_extensions/core_ext/hash/nested"
|
14
|
+
require "more_core_extensions/core_ext/array/nested"
|
15
|
+
|
16
|
+
super
|
17
|
+
|
18
|
+
raise Floe::InvalidWorkflowError, "Invalid Reference Path" if payload.match?(/@|,|:|\?/)
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(context, value)
|
22
|
+
result = context.dup
|
23
|
+
|
24
|
+
path = JsonPath.new(payload)
|
25
|
+
.path[1..]
|
26
|
+
.map { |v| v.match(/\[(?<name>.+)\]/)["name"] }
|
27
|
+
.map { |v| v[0] == "'" ? v.gsub("'", "") : v.to_i }
|
28
|
+
.compact
|
29
|
+
|
30
|
+
# If the payload is '$' then merge the value into the context
|
31
|
+
# otherwise use store path to set the value to a sub-key
|
32
|
+
#
|
33
|
+
# TODO: how to handle non-hash values, raise error if path=$ and value not a hash?
|
34
|
+
if path.empty?
|
35
|
+
result.merge!(value)
|
36
|
+
else
|
37
|
+
result.store_path(path, value)
|
38
|
+
end
|
39
|
+
|
40
|
+
result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class Retrier
|
6
|
+
attr_reader :error_equals, :interval_seconds, :max_attempts, :backoff_rate
|
7
|
+
|
8
|
+
def initialize(payload)
|
9
|
+
@payload = payload
|
10
|
+
|
11
|
+
@error_equals = payload["ErrorEquals"]
|
12
|
+
@interval_seconds = payload["IntervalSeconds"] || 1.0
|
13
|
+
@max_attempts = payload["MaxAttempts"] || 3
|
14
|
+
@backoff_rate = payload["BackoffRate"] || 2.0
|
15
|
+
end
|
16
|
+
|
17
|
+
def sleep_duration(attempt)
|
18
|
+
interval_seconds * (backoff_rate * attempt)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|