floe 0.11.3 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|