floe 0.11.3 → 0.13.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/.yamllint +1 -3
- data/CHANGELOG.md +37 -1
- data/floe.gemspec +2 -2
- data/lib/floe/cli.rb +4 -1
- data/lib/floe/container_runner/docker_mixin.rb +3 -0
- data/lib/floe/validation_mixin.rb +49 -0
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/catcher.rb +17 -3
- data/lib/floe/workflow/choice_rule/data.rb +17 -5
- data/lib/floe/workflow/choice_rule.rb +14 -9
- data/lib/floe/workflow/context.rb +11 -5
- data/lib/floe/workflow/error_matcher_mixin.rb +17 -0
- data/lib/floe/workflow/intrinsic_function/parser.rb +108 -0
- data/lib/floe/workflow/intrinsic_function/transformer.rb +266 -0
- data/lib/floe/workflow/intrinsic_function.rb +34 -0
- data/lib/floe/workflow/path.rb +18 -1
- data/lib/floe/workflow/payload_template.rb +7 -4
- data/lib/floe/workflow/retrier.rb +9 -3
- data/lib/floe/workflow/state.rb +35 -14
- data/lib/floe/workflow/states/choice.rb +6 -5
- data/lib/floe/workflow/states/fail.rb +1 -1
- data/lib/floe/workflow/states/non_terminal_mixin.rb +2 -2
- data/lib/floe/workflow/states/pass.rb +2 -1
- data/lib/floe/workflow/states/succeed.rb +10 -1
- data/lib/floe/workflow/states/task.rb +11 -10
- data/lib/floe/workflow.rb +19 -9
- data/lib/floe.rb +7 -0
- metadata +23 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: abe15498a790293b2a375c4538ed649bfa577edd1b7eaa38d02266fc4f7fc576
|
4
|
+
data.tar.gz: 733e0e6687ca143de8a74214f13b4a78118333a99f89a9a09a9998ab0319768a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d5dfc65c86e68d39b7c9bbeedd20e2a3ea844f158d601bb72bf35595e6d6597ac6571f7bc9ad6802f8ed33d6753b4906e7745a3e46cb38790492869aa95e6a02
|
7
|
+
data.tar.gz: 913032cb3a042e8b3464f05c7e0e65401f8532b28ded1cebd57df310899dcc8bd1bc26d9307a6b824837474c0e3ee9144477147779e0b40b6fd9c9fecfc7bddc
|
data/.yamllint
CHANGED
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,40 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
4
4
|
|
5
5
|
## [Unreleased]
|
6
6
|
|
7
|
+
## [0.13.0] - 2024-08-12
|
8
|
+
### Added
|
9
|
+
- Choice rule payload validation path ([#253](https://github.com/ManageIQ/floe/pull/253))
|
10
|
+
- Intrinsics JsonToString and StringToJson ([#256](https://github.com/ManageIQ/floe/pull/256))
|
11
|
+
- Add States.Format intrinsic function ([#258](https://github.com/ManageIQ/floe/pull/258))
|
12
|
+
- Intrinsics States.JsonMerge ([#255](https://github.com/ManageIQ/floe/pull/255))
|
13
|
+
- Enable support for Hashes in States.Hash ([#260](https://github.com/ManageIQ/floe/pull/260))
|
14
|
+
|
15
|
+
## [0.12.0] - 2024-07-31
|
16
|
+
### Added
|
17
|
+
- Set Floe.logger.level if DEBUG env var set ([#234](https://github.com/ManageIQ/floe/pull/234))
|
18
|
+
- Add InstrinsicFunction foundation + States.Array, States.UUID ([#194](https://github.com/ManageIQ/floe/pull/194))
|
19
|
+
- Evaluate IntrinsicFunctions from PayloadTemplate ([#236](https://github.com/ManageIQ/floe/pull/236))
|
20
|
+
- Add more intrinsic functions ([#242](https://github.com/ManageIQ/floe/pull/242))
|
21
|
+
- Add State#cause_path, error_path ([#249](https://github.com/ManageIQ/floe/pull/249))
|
22
|
+
- Validate Catcher Next ([#250](https://github.com/ManageIQ/floe/pull/250))
|
23
|
+
- Add spec/supports ([#248](https://github.com/ManageIQ/floe/pull/248))
|
24
|
+
|
25
|
+
### Fixed
|
26
|
+
- Handle non-hash input/output values ([#214](https://github.com/ManageIQ/floe/pull/214))
|
27
|
+
- Fix Input/Output handling for Pass, Choice, Succeed states ([#225](https://github.com/ManageIQ/floe/pull/225))
|
28
|
+
- Wrap Parslet::ParseFailed errors as Floe::InvalidWorkflowError ([#235](https://github.com/ManageIQ/floe/pull/235))
|
29
|
+
- Fix edge cases with States.Array ([#237](https://github.com/ManageIQ/floe/pull/237))
|
30
|
+
- Fix sporadic test failure with Wait state ([#243](https://github.com/ManageIQ/floe/pull/243))
|
31
|
+
- Fix invalid container names after normalizing ([#252](https://github.com/ManageIQ/floe/pull/252))
|
32
|
+
|
33
|
+
### Changed
|
34
|
+
- Extract ErrorMatcherMixin from Catch and Retry ([#186](https://github.com/ManageIQ/floe/pull/186))
|
35
|
+
- Output should be JSON ([#230](https://github.com/ManageIQ/floe/pull/230))
|
36
|
+
- Normalize functions to all take arguments ([#238](https://github.com/ManageIQ/floe/pull/238))
|
37
|
+
- Pass full path name to State.new for better errors ([#229](https://github.com/ManageIQ/floe/pull/229))
|
38
|
+
- Validate that state machine input is valid JSON ([#227](https://github.com/ManageIQ/floe/pull/227))
|
39
|
+
- Move the Parslet parse into initialize so that invalid function definition will fail on workflow load ([#245](https://github.com/ManageIQ/floe/pull/245))
|
40
|
+
|
7
41
|
## [0.11.3] - 2024-06-20
|
8
42
|
### Fixed
|
9
43
|
- ResultPath=$ replaces complete output ([#199](https://github.com/ManageIQ/floe/pull/199))
|
@@ -199,7 +233,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
199
233
|
### Added
|
200
234
|
- Initial release
|
201
235
|
|
202
|
-
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.
|
236
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.13.0...HEAD
|
237
|
+
[0.13.0]: https://github.com/ManageIQ/floe/compare/v0.12.0...v0.13.0
|
238
|
+
[0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
|
203
239
|
[0.11.3]: https://github.com/ManageIQ/floe/compare/v0.11.2...v0.11.3
|
204
240
|
[0.11.2]: https://github.com/ManageIQ/floe/compare/v0.11.1...v0.11.2
|
205
241
|
[0.11.1]: https://github.com/ManageIQ/floe/compare/v0.11.0...v0.11.1
|
data/floe.gemspec
CHANGED
@@ -36,11 +36,11 @@ Gem::Specification.new do |spec|
|
|
36
36
|
spec.add_dependency "jsonpath", "~>1.1"
|
37
37
|
spec.add_dependency "kubeclient", "~>4.7"
|
38
38
|
spec.add_dependency "optimist", "~>3.0"
|
39
|
+
spec.add_dependency "parslet", "~>2.0"
|
39
40
|
|
40
|
-
spec.add_development_dependency "manageiq-style"
|
41
|
+
spec.add_development_dependency "manageiq-style", ">= 1.5.2"
|
41
42
|
spec.add_development_dependency "rake", "~> 13.0"
|
42
43
|
spec.add_development_dependency "rspec"
|
43
|
-
spec.add_development_dependency "rubocop"
|
44
44
|
spec.add_development_dependency "simplecov", ">= 0.21.2"
|
45
45
|
spec.add_development_dependency "timecop"
|
46
46
|
end
|
data/lib/floe/cli.rb
CHANGED
@@ -7,6 +7,7 @@ module Floe
|
|
7
7
|
require "logger"
|
8
8
|
|
9
9
|
Floe.logger = Logger.new($stdout)
|
10
|
+
Floe.logger.level = 0 if ENV["DEBUG"]
|
10
11
|
end
|
11
12
|
|
12
13
|
def run(args = ARGV)
|
@@ -30,10 +31,12 @@ module Floe
|
|
30
31
|
# Display status
|
31
32
|
workflows.each do |workflow|
|
32
33
|
puts "", "#{workflow.name}#{" (#{workflow.status})" unless workflow.context.success?}", "===" if workflows.size > 1
|
33
|
-
puts workflow.output
|
34
|
+
puts workflow.output
|
34
35
|
end
|
35
36
|
|
36
37
|
workflows.all? { |workflow| workflow.context.success? }
|
38
|
+
rescue Floe::Error => err
|
39
|
+
abort(err.message)
|
37
40
|
end
|
38
41
|
|
39
42
|
private
|
@@ -24,6 +24,9 @@ module Floe
|
|
24
24
|
# This does not follow the leading and trailing character restriction because we will embed it
|
25
25
|
# below with a prefix and suffix that already conform to the RFC.
|
26
26
|
normalized_name = name.downcase.gsub(/[^a-z0-9-]/, "-")[0, MAX_CONTAINER_NAME_SIZE]
|
27
|
+
# Ensure that the normalized_name doesn't end in any invalid characters after we
|
28
|
+
# limited the length to the MAX_CONTAINER_NAME_SIZE.
|
29
|
+
normalized_name.gsub!(/[^a-z0-9]+$/, "")
|
27
30
|
|
28
31
|
"floe-#{normalized_name}-#{SecureRandom.hex(4)}"
|
29
32
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
module ValidationMixin
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
def parser_error!(comment)
|
10
|
+
self.class.parser_error!(name, comment)
|
11
|
+
end
|
12
|
+
|
13
|
+
def missing_field_error!(field_name)
|
14
|
+
self.class.missing_field_error!(name, field_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def invalid_field_error!(field_name, field_value = nil, comment = nil)
|
18
|
+
self.class.invalid_field_error!(name, field_name, field_value, comment)
|
19
|
+
end
|
20
|
+
|
21
|
+
def workflow_state?(field_value, workflow)
|
22
|
+
workflow.payload["States"] ? workflow.payload["States"].include?(field_value) : true
|
23
|
+
end
|
24
|
+
|
25
|
+
def wrap_parser_error(field_name, field_value)
|
26
|
+
yield
|
27
|
+
rescue ArgumentError, InvalidWorkflowError => error
|
28
|
+
invalid_field_error!(field_name, field_value, error.message)
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def parser_error!(name, comment)
|
33
|
+
name = name.join(".") if name.kind_of?(Array)
|
34
|
+
raise Floe::InvalidWorkflowError, "#{name} #{comment}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def missing_field_error!(name, field_name)
|
38
|
+
parser_error!(name, "does not have required field \"#{field_name}\"")
|
39
|
+
end
|
40
|
+
|
41
|
+
def invalid_field_error!(name, field_name, field_value, comment)
|
42
|
+
# instead of displaying a large hash or array, just displaying the word Hash or Array
|
43
|
+
field_value = field_value.class if field_value.kind_of?(Hash) || field_value.kind_of?(Array)
|
44
|
+
|
45
|
+
parser_error!(name, "field \"#{field_name}\"#{" value \"#{field_value}\"" unless field_value.nil?} #{comment}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/floe/version.rb
CHANGED
@@ -3,14 +3,28 @@
|
|
3
3
|
module Floe
|
4
4
|
class Workflow
|
5
5
|
class Catcher
|
6
|
-
|
6
|
+
include ErrorMatcherMixin
|
7
|
+
include ValidationMixin
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
attr_reader :error_equals, :next, :result_path, :name
|
10
|
+
|
11
|
+
def initialize(workflow, name, payload)
|
12
|
+
@name = name
|
13
|
+
@payload = payload
|
10
14
|
|
11
15
|
@error_equals = payload["ErrorEquals"]
|
12
16
|
@next = payload["Next"]
|
13
17
|
@result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
|
18
|
+
|
19
|
+
missing_field_error!("ErrorEquals") if !@error_equals.kind_of?(Array) || @error_equals.empty?
|
20
|
+
validate_state_next!(workflow)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_state_next!(workflow)
|
26
|
+
missing_field_error!("Next") if @next.nil?
|
27
|
+
invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !workflow_state?(@next, workflow)
|
14
28
|
end
|
15
29
|
end
|
16
30
|
end
|
@@ -5,14 +5,13 @@ module Floe
|
|
5
5
|
class ChoiceRule
|
6
6
|
class Data < Floe::Workflow::ChoiceRule
|
7
7
|
def true?(context, input)
|
8
|
+
return presence_check(context, input) if compare_key == "IsPresent"
|
9
|
+
|
8
10
|
lhs = variable_value(context, input)
|
9
11
|
rhs = compare_value(context, input)
|
10
12
|
|
11
|
-
validate!(lhs)
|
12
|
-
|
13
13
|
case compare_key
|
14
14
|
when "IsNull" then is_null?(lhs)
|
15
|
-
when "IsPresent" then is_present?(lhs)
|
16
15
|
when "IsNumeric" then is_numeric?(lhs)
|
17
16
|
when "IsString" then is_string?(lhs)
|
18
17
|
when "IsBoolean" then is_boolean?(lhs)
|
@@ -47,8 +46,21 @@ module Floe
|
|
47
46
|
|
48
47
|
private
|
49
48
|
|
50
|
-
def
|
51
|
-
|
49
|
+
def presence_check(context, input)
|
50
|
+
# Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
|
51
|
+
# If true then return true when present.
|
52
|
+
# If false then return true when not present.
|
53
|
+
rhs = compare_value(context, input)
|
54
|
+
# Don't need the variable_value, just need to see if the path finds the value.
|
55
|
+
variable_value(context, input)
|
56
|
+
|
57
|
+
# The variable_value is present
|
58
|
+
# If rhs is true, then presence check was successful, return true.
|
59
|
+
rhs
|
60
|
+
rescue Floe::PathError
|
61
|
+
# variable_value is not present. (the path lookup threw an error)
|
62
|
+
# If rhs is false, then it successfully wasn't present, return true.
|
63
|
+
!rhs
|
52
64
|
end
|
53
65
|
|
54
66
|
def is_null?(value) # rubocop:disable Naming/PredicateName
|
@@ -4,26 +4,31 @@ module Floe
|
|
4
4
|
class Workflow
|
5
5
|
class ChoiceRule
|
6
6
|
class << self
|
7
|
-
def build(payload)
|
7
|
+
def build(workflow, name, payload)
|
8
8
|
if (sub_payloads = payload["Not"])
|
9
|
-
|
9
|
+
name += ["Not"]
|
10
|
+
Floe::Workflow::ChoiceRule::Not.new(workflow, name, payload, build_children(workflow, name, [sub_payloads]))
|
10
11
|
elsif (sub_payloads = payload["And"])
|
11
|
-
|
12
|
+
name += ["And"]
|
13
|
+
Floe::Workflow::ChoiceRule::And.new(workflow, name, payload, build_children(workflow, name, sub_payloads))
|
12
14
|
elsif (sub_payloads = payload["Or"])
|
13
|
-
|
15
|
+
name += ["Or"]
|
16
|
+
Floe::Workflow::ChoiceRule::Or.new(workflow, name, payload, build_children(workflow, name, sub_payloads))
|
14
17
|
else
|
15
|
-
|
18
|
+
name += ["Data"]
|
19
|
+
Floe::Workflow::ChoiceRule::Data.new(workflow, name, payload)
|
16
20
|
end
|
17
21
|
end
|
18
22
|
|
19
|
-
def build_children(sub_payloads)
|
20
|
-
sub_payloads.map { |payload| build(payload) }
|
23
|
+
def build_children(workflow, name, sub_payloads)
|
24
|
+
sub_payloads.map.with_index { |payload, i| build(workflow, name + [i.to_s], payload) }
|
21
25
|
end
|
22
26
|
end
|
23
27
|
|
24
|
-
attr_reader :next, :payload, :variable, :children
|
28
|
+
attr_reader :next, :payload, :variable, :children, :name
|
25
29
|
|
26
|
-
def initialize(payload, children = nil)
|
30
|
+
def initialize(_workflow, name, payload, children = nil)
|
31
|
+
@name = name
|
27
32
|
@payload = payload
|
28
33
|
@children = children
|
29
34
|
|
@@ -9,9 +9,7 @@ module Floe
|
|
9
9
|
# @param input [Hash] (default: {})
|
10
10
|
def initialize(context = nil, input: nil, credentials: {})
|
11
11
|
context = JSON.parse(context) if context.kind_of?(String)
|
12
|
-
|
13
|
-
input ||= {}
|
14
|
-
input = JSON.parse(input) if input.kind_of?(String)
|
12
|
+
input = JSON.parse(input || "{}")
|
15
13
|
|
16
14
|
@context = context || {}
|
17
15
|
self["Execution"] ||= {}
|
@@ -23,7 +21,7 @@ module Floe
|
|
23
21
|
|
24
22
|
@credentials = credentials || {}
|
25
23
|
rescue JSON::ParserError => err
|
26
|
-
raise Floe::
|
24
|
+
raise Floe::InvalidExecutionInput, "Invalid State Machine Execution Input: #{err}: was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')"
|
27
25
|
end
|
28
26
|
|
29
27
|
def execution
|
@@ -39,7 +37,7 @@ module Floe
|
|
39
37
|
end
|
40
38
|
|
41
39
|
def failed?
|
42
|
-
output
|
40
|
+
(output.kind_of?(Hash) && output.key?("Error")) || false
|
43
41
|
end
|
44
42
|
|
45
43
|
def ended?
|
@@ -54,10 +52,18 @@ module Floe
|
|
54
52
|
state["Input"]
|
55
53
|
end
|
56
54
|
|
55
|
+
def json_input
|
56
|
+
input.to_json
|
57
|
+
end
|
58
|
+
|
57
59
|
def output
|
58
60
|
state["Output"]
|
59
61
|
end
|
60
62
|
|
63
|
+
def json_output
|
64
|
+
output.to_json
|
65
|
+
end
|
66
|
+
|
61
67
|
def output=(val)
|
62
68
|
state["Output"] = val
|
63
69
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
# Methods for common error handling
|
6
|
+
module ErrorMatcherMixin
|
7
|
+
# @param [String] error the error thrown
|
8
|
+
def match_error?(error)
|
9
|
+
return false if error == "States.Runtime"
|
10
|
+
return true if error_equals.include?("States.ALL")
|
11
|
+
return true if error_equals.include?("States.Timeout") && error == "States.HeartbeatTimeout"
|
12
|
+
|
13
|
+
error_equals.include?(error)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "parslet"
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class IntrinsicFunction
|
6
|
+
class Parser < Parslet::Parser
|
7
|
+
rule(:spaces) { str(' ').repeat(1) }
|
8
|
+
rule(:spaces?) { spaces.maybe }
|
9
|
+
rule(:digit) { match('[0-9]') }
|
10
|
+
rule(:quote) { str('\'') }
|
11
|
+
|
12
|
+
rule(:comma_sep) { str(',') >> spaces? }
|
13
|
+
|
14
|
+
rule(:true_literal) { str('true').as(:true_literal) }
|
15
|
+
rule(:false_literal) { str('false').as(:false_literal) }
|
16
|
+
rule(:null_literal) { str('null').as(:null_literal) }
|
17
|
+
|
18
|
+
rule(:number) do
|
19
|
+
(
|
20
|
+
str('-').maybe >> (
|
21
|
+
str('0') | (match('[1-9]') >> digit.repeat)
|
22
|
+
) >> (
|
23
|
+
str('.') >> digit.repeat(1)
|
24
|
+
).maybe >> (
|
25
|
+
match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
|
26
|
+
).maybe
|
27
|
+
).as(:number)
|
28
|
+
end
|
29
|
+
|
30
|
+
rule(:string) do
|
31
|
+
(
|
32
|
+
quote >> (
|
33
|
+
(str('\\') >> any) | (quote.absent? >> any)
|
34
|
+
).repeat >> quote
|
35
|
+
).as(:string)
|
36
|
+
end
|
37
|
+
|
38
|
+
rule(:jsonpath) do
|
39
|
+
(
|
40
|
+
str('$') >> match('[^,)]').repeat(0)
|
41
|
+
).as(:jsonpath)
|
42
|
+
end
|
43
|
+
|
44
|
+
rule(:arg) do
|
45
|
+
(
|
46
|
+
string | number | jsonpath | true_literal | false_literal | null_literal | expression
|
47
|
+
).as(:arg)
|
48
|
+
end
|
49
|
+
|
50
|
+
rule(:args) do
|
51
|
+
(
|
52
|
+
arg >> (comma_sep >> arg).repeat
|
53
|
+
).maybe.as(:args)
|
54
|
+
end
|
55
|
+
|
56
|
+
[
|
57
|
+
:states_format, "States.Format",
|
58
|
+
:states_string_to_json, "States.StringToJson",
|
59
|
+
:states_json_to_string, "States.JsonToString",
|
60
|
+
:states_array, "States.Array",
|
61
|
+
:states_array_partition, "States.ArrayPartition",
|
62
|
+
:states_array_contains, "States.ArrayContains",
|
63
|
+
:states_array_range, "States.ArrayRange",
|
64
|
+
:states_array_get_item, "States.ArrayGetItem",
|
65
|
+
:states_array_length, "States.ArrayLength",
|
66
|
+
:states_array_unique, "States.ArrayUnique",
|
67
|
+
:states_base64_encode, "States.Base64Encode",
|
68
|
+
:states_base64_decode, "States.Base64Decode",
|
69
|
+
:states_hash, "States.Hash",
|
70
|
+
:states_json_merge, "States.JsonMerge",
|
71
|
+
:states_math_random, "States.MathRandom",
|
72
|
+
:states_math_add, "States.MathAdd",
|
73
|
+
:states_string_split, "States.StringSplit",
|
74
|
+
:states_uuid, "States.UUID",
|
75
|
+
].each_slice(2) do |function_symbol, function_name|
|
76
|
+
rule(function_symbol) do
|
77
|
+
(
|
78
|
+
str(function_name) >> str('(') >> args >> str(')')
|
79
|
+
).as(function_symbol)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
rule(:expression) do
|
84
|
+
states_format |
|
85
|
+
states_string_to_json |
|
86
|
+
states_json_to_string |
|
87
|
+
states_array |
|
88
|
+
states_array_partition |
|
89
|
+
states_array_contains |
|
90
|
+
states_array_range |
|
91
|
+
states_array_get_item |
|
92
|
+
states_array_length |
|
93
|
+
states_array_unique |
|
94
|
+
states_base64_encode |
|
95
|
+
states_base64_decode |
|
96
|
+
states_hash |
|
97
|
+
states_json_merge |
|
98
|
+
states_math_random |
|
99
|
+
states_math_add |
|
100
|
+
states_string_split |
|
101
|
+
states_uuid
|
102
|
+
end
|
103
|
+
|
104
|
+
root(:expression)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|