floe 0.12.0 → 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 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