floe 0.12.0 → 0.13.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: 2eb195983f47d7cac44f18e962bf314c3ff4fefbdf3ac5cae32e960bfc2c0fb0
4
- data.tar.gz: 6a0f8d91337297786c03e99fc96cb118ed8cb76244e52cf8253702d1517a6f5d
3
+ metadata.gz: abe15498a790293b2a375c4538ed649bfa577edd1b7eaa38d02266fc4f7fc576
4
+ data.tar.gz: 733e0e6687ca143de8a74214f13b4a78118333a99f89a9a09a9998ab0319768a
5
5
  SHA512:
6
- metadata.gz: 99f5a3c8e67a9f5c97b43795739c201a49a8f1195d40a1da38f2aa0e16d200266783336e4be9ada5d41b566c545144a4533db258546ebbf04a23c454a489e6a1
7
- data.tar.gz: a77bfe436b37adf9dbdc2a279b9b1f70f48980062aaaec191a8e1fd57238e8b8b50b02828025e561c7ccb8429cb67fefb3f7ed044ed309d82eaae8dd8d9d407b
6
+ metadata.gz: d5dfc65c86e68d39b7c9bbeedd20e2a3ea844f158d601bb72bf35595e6d6597ac6571f7bc9ad6802f8ed33d6753b4906e7745a3e46cb38790492869aa95e6a02
7
+ data.tar.gz: 913032cb3a042e8b3464f05c7e0e65401f8532b28ded1cebd57df310899dcc8bd1bc26d9307a6b824837474c0e3ee9144477147779e0b40b6fd9c9fecfc7bddc
data/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ 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
+
7
15
  ## [0.12.0] - 2024-07-31
8
16
  ### Added
9
17
  - Set Floe.logger.level if DEBUG env var set ([#234](https://github.com/ManageIQ/floe/pull/234))
@@ -225,7 +233,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
225
233
  ### Added
226
234
  - Initial release
227
235
 
228
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.12.0...HEAD
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
229
238
  [0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
230
239
  [0.11.3]: https://github.com/ManageIQ/floe/compare/v0.11.2...v0.11.3
231
240
  [0.11.2]: https://github.com/ManageIQ/floe/compare/v0.11.1...v0.11.2
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.12.0"
4
+ VERSION = "0.13.0"
5
5
  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 validate!(value)
51
- raise "No such variable [#{variable}]" if value.nil? && !%w[IsNull IsPresent].include?(compare_key)
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
@@ -54,6 +54,9 @@ module Floe
54
54
  end
55
55
 
56
56
  [
57
+ :states_format, "States.Format",
58
+ :states_string_to_json, "States.StringToJson",
59
+ :states_json_to_string, "States.JsonToString",
57
60
  :states_array, "States.Array",
58
61
  :states_array_partition, "States.ArrayPartition",
59
62
  :states_array_contains, "States.ArrayContains",
@@ -64,6 +67,7 @@ module Floe
64
67
  :states_base64_encode, "States.Base64Encode",
65
68
  :states_base64_decode, "States.Base64Decode",
66
69
  :states_hash, "States.Hash",
70
+ :states_json_merge, "States.JsonMerge",
67
71
  :states_math_random, "States.MathRandom",
68
72
  :states_math_add, "States.MathAdd",
69
73
  :states_string_split, "States.StringSplit",
@@ -77,7 +81,10 @@ module Floe
77
81
  end
78
82
 
79
83
  rule(:expression) do
80
- states_array |
84
+ states_format |
85
+ states_string_to_json |
86
+ states_json_to_string |
87
+ states_array |
81
88
  states_array_partition |
82
89
  states_array_contains |
83
90
  states_array_range |
@@ -87,6 +94,7 @@ module Floe
87
94
  states_base64_encode |
88
95
  states_base64_decode |
89
96
  states_hash |
97
+ states_json_merge |
90
98
  states_math_random |
91
99
  states_math_add |
92
100
  states_string_split |
@@ -9,6 +9,7 @@ module Floe
9
9
  class IntrinsicFunction
10
10
  class Transformer < Parslet::Transform
11
11
  OptionalArg = Struct.new(:type)
12
+ VariadicArgs = Struct.new(:type)
12
13
 
13
14
  class << self
14
15
  def process_args(args, function, signature = nil)
@@ -37,20 +38,33 @@ module Floe
37
38
 
38
39
  def check_arity(args, function, signature)
39
40
  if signature.any?(OptionalArg)
40
- signature_without_optional = signature.reject { |a| a.kind_of?(OptionalArg) }
41
- signature_size = (signature_without_optional.size..signature.size)
41
+ signature_required = signature.reject { |a| a.kind_of?(OptionalArg) }
42
+ signature_size = (signature_required.size..signature.size)
42
43
 
43
44
  raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature_size})" unless signature_size.include?(args.size)
45
+ elsif signature.any?(VariadicArgs)
46
+ signature_required = signature.reject { |a| a.kind_of?(VariadicArgs) }
47
+
48
+ raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected at least #{signature_required.size})" unless args.size >= signature_required.size
44
49
  else
45
50
  raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature.size})" unless signature.size == args.size
46
51
  end
47
52
  end
48
53
 
49
54
  def check_types(args, function, signature)
55
+ # Adjust the signature for VariadicArgs to create a copy of the expected type for each given arg
56
+ if signature.last.kind_of?(VariadicArgs)
57
+ signature = signature[0..-2] + Array.new(args.size - signature.size + 1, signature.last.type)
58
+ end
59
+
50
60
  args.zip(signature).each_with_index do |(arg, type), index|
51
61
  type = type.type if type.kind_of?(OptionalArg)
52
62
 
53
- raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type)
63
+ if type.kind_of?(Array)
64
+ raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected one of #{type.join(", ")})" unless type.any? { |t| arg.kind_of?(t) }
65
+ else
66
+ raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type)
67
+ end
54
68
  end
55
69
  end
56
70
  end
@@ -63,6 +77,50 @@ module Floe
63
77
  rule(:number => simple(:v)) { v.match(/[eE.]/) ? Float(v) : Integer(v) }
64
78
  rule(:jsonpath => simple(:v)) { Floe::Workflow::Path.value(v.to_s, context, input) }
65
79
 
80
+ STATES_FORMAT_PLACEHOLDER = /(?<!\\)\{\}/.freeze
81
+
82
+ rule(:states_format => {:args => subtree(:args)}) do
83
+ args = Transformer.process_args(args(), "States.Format", [String, VariadicArgs[[String, TrueClass, FalseClass, Integer, Float, NilClass]]])
84
+ str, *rest = *args
85
+
86
+ # TODO: Handle templates with escaped characters, including invalid templates
87
+ # See https://states-language.net/#intrinsic-functions (number 6)
88
+
89
+ expected_args = str.scan(STATES_FORMAT_PLACEHOLDER).size
90
+ actual_args = rest.size
91
+ if expected_args != actual_args
92
+ raise ArgumentError, "number of arguments to States.Format do not match the occurrences of {} (given #{actual_args}, expected #{expected_args})"
93
+ end
94
+
95
+ rest.each do |arg|
96
+ str = str.sub(STATES_FORMAT_PLACEHOLDER, arg.nil? ? "null" : arg.to_s)
97
+ end
98
+
99
+ # TODO: Handle arguments that have escape characters within them but are interpolated
100
+ str.gsub!("\\'", "'")
101
+ str.gsub!("\\{", "{")
102
+ str.gsub!("\\}", "}")
103
+ str.gsub!("\\\\", "\\")
104
+
105
+ str
106
+ end
107
+
108
+ rule(:states_json_to_string => {:args => subtree(:args)}) do
109
+ args = Transformer.process_args(args(), "States.JsonToString", [Object])
110
+ json = args.first
111
+
112
+ JSON.generate(json)
113
+ end
114
+
115
+ rule(:states_string_to_json => {:args => subtree(:args)}) do
116
+ args = Transformer.process_args(args(), "States.StringToJson", [String])
117
+ str = args.first
118
+
119
+ JSON.parse(str)
120
+ rescue JSON::ParserError => e
121
+ raise ArgumentError, "invalid value for argument 1 to States.StringToJson (invalid json: #{e.message})"
122
+ end
123
+
66
124
  rule(:states_array => {:args => subtree(:args)}) do
67
125
  Transformer.process_args(args, "States.Array")
68
126
  end
@@ -135,7 +193,7 @@ module Floe
135
193
  rule(:states_hash => {:args => subtree(:args)}) do
136
194
  args = Transformer.process_args(args(), "States.Hash", [Object, String])
137
195
  data, algorithm = *args
138
- raise NotImplementedError if data.kind_of?(Hash)
196
+
139
197
  if data.nil?
140
198
  raise ArgumentError, "invalid value for argument 1 to States.Hash (given null, expected non-null)"
141
199
  end
@@ -147,8 +205,20 @@ module Floe
147
205
 
148
206
  require "openssl"
149
207
  algorithm = algorithm.sub("-", "")
150
- data = data.to_json unless data.kind_of?(String)
151
- OpenSSL::Digest.hexdigest(algorithm, data)
208
+ data = JSON.generate(data) unless data.kind_of?(String)
209
+ OpenSSL::Digest.hexdigest(algorithm, data).force_encoding("UTF-8")
210
+ end
211
+
212
+ rule(:states_json_merge => {:args => subtree(:args)}) do
213
+ args = Transformer.process_args(args(), "States.JsonMerge", [Hash, Hash, [TrueClass, FalseClass]])
214
+ left, right, deep = *args
215
+
216
+ if deep
217
+ # NOTE: not implemented by AWS Step Functions and nuances not defined in docs
218
+ left.merge(right) { |_key, l, r| l.kind_of?(Hash) && r.kind_of?(Hash) ? l.merge(r) : r }
219
+ else
220
+ left.merge(right)
221
+ end
152
222
  end
153
223
 
154
224
  rule(:states_math_random => {:args => subtree(:args)}) do
@@ -34,7 +34,18 @@ module Floe
34
34
  return obj if path == "$"
35
35
 
36
36
  results = JsonPath.on(obj, path)
37
- results.count < 2 ? results.first : results
37
+ case results.count
38
+ when 0
39
+ raise Floe::PathError, "Path [#{payload}] references an invalid value"
40
+ when 1
41
+ results.first
42
+ else
43
+ results
44
+ end
45
+ end
46
+
47
+ def to_s
48
+ payload
38
49
  end
39
50
  end
40
51
  end
@@ -48,17 +48,27 @@ module Floe
48
48
  return Errno::EAGAIN unless ready?(context)
49
49
 
50
50
  finish(context)
51
+ rescue Floe::Error => e
52
+ mark_error(context, e)
51
53
  end
52
54
 
53
55
  def start(context)
56
+ mark_started(context)
57
+ end
58
+
59
+ def finish(context)
60
+ mark_finished(context)
61
+ end
62
+
63
+ def mark_started(context)
54
64
  context.state["EnteredTime"] = Time.now.utc.iso8601
55
65
 
56
66
  logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
57
67
  end
58
68
 
59
- def finish(context)
60
- finished_time = Time.now.utc
61
- entered_time = Time.parse(context.state["EnteredTime"])
69
+ def mark_finished(context)
70
+ finished_time = Time.now.utc
71
+ entered_time = Time.parse(context.state["EnteredTime"])
62
72
 
63
73
  context.state["FinishedTime"] ||= finished_time.iso8601
64
74
  context.state["Duration"] = finished_time - entered_time
@@ -69,6 +79,14 @@ module Floe
69
79
  0
70
80
  end
71
81
 
82
+ def mark_error(context, exception)
83
+ # InputPath or OutputPath were bad.
84
+ context.next_state = nil
85
+ context.output = {"Error" => "States.Runtime", "Cause" => exception.message}
86
+ # Since finish threw an exception, super was never called. Calling that now.
87
+ mark_finished(context)
88
+ end
89
+
72
90
  def ready?(context)
73
91
  !context.state_started? || !running?(context)
74
92
  end
data/lib/floe.rb CHANGED
@@ -43,6 +43,7 @@ module Floe
43
43
  class Error < StandardError; end
44
44
  class InvalidWorkflowError < Error; end
45
45
  class InvalidExecutionInput < Error; end
46
+ class PathError < Error; end
46
47
 
47
48
  def self.logger
48
49
  @logger ||= NullLogger.new
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ManageIQ Developers
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-01 00:00:00.000000000 Z
11
+ date: 2024-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_spawn