lib/floe/workflow/intrinsic_function/transformer.rb in floe-0.12.0 vs lib/floe/workflow/intrinsic_function/transformer.rb in floe-0.13.0

- old
+ new

@@ -7,10 +7,11 @@ module Floe class Workflow class IntrinsicFunction class Transformer < Parslet::Transform OptionalArg = Struct.new(:type) + VariadicArgs = Struct.new(:type) class << self def process_args(args, function, signature = nil) args = resolve_args(args) if signature @@ -35,24 +36,37 @@ end end def check_arity(args, function, signature) if signature.any?(OptionalArg) - signature_without_optional = signature.reject { |a| a.kind_of?(OptionalArg) } - signature_size = (signature_without_optional.size..signature.size) + signature_required = signature.reject { |a| a.kind_of?(OptionalArg) } + signature_size = (signature_required.size..signature.size) raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature_size})" unless signature_size.include?(args.size) + elsif signature.any?(VariadicArgs) + signature_required = signature.reject { |a| a.kind_of?(VariadicArgs) } + + raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected at least #{signature_required.size})" unless args.size >= signature_required.size else raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature.size})" unless signature.size == args.size end end def check_types(args, function, signature) + # Adjust the signature for VariadicArgs to create a copy of the expected type for each given arg + if signature.last.kind_of?(VariadicArgs) + signature = signature[0..-2] + Array.new(args.size - signature.size + 1, signature.last.type) + end + args.zip(signature).each_with_index do |(arg, type), index| type = type.type if type.kind_of?(OptionalArg) - raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type) + if type.kind_of?(Array) + 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) } + else + raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type) + end end end end rule(:null_literal => simple(:v)) { nil } @@ -61,10 +75,54 @@ rule(:string => simple(:v)) { v.to_s[1..-2] } rule(:number => simple(:v)) { v.match(/[eE.]/) ? Float(v) : Integer(v) } rule(:jsonpath => simple(:v)) { Floe::Workflow::Path.value(v.to_s, context, input) } + STATES_FORMAT_PLACEHOLDER = /(?<!\\)\{\}/.freeze + + rule(:states_format => {:args => subtree(:args)}) do + args = Transformer.process_args(args(), "States.Format", [String, VariadicArgs[[String, TrueClass, FalseClass, Integer, Float, NilClass]]]) + str, *rest = *args + + # TODO: Handle templates with escaped characters, including invalid templates + # See https://states-language.net/#intrinsic-functions (number 6) + + expected_args = str.scan(STATES_FORMAT_PLACEHOLDER).size + actual_args = rest.size + if expected_args != actual_args + raise ArgumentError, "number of arguments to States.Format do not match the occurrences of {} (given #{actual_args}, expected #{expected_args})" + end + + rest.each do |arg| + str = str.sub(STATES_FORMAT_PLACEHOLDER, arg.nil? ? "null" : arg.to_s) + end + + # TODO: Handle arguments that have escape characters within them but are interpolated + str.gsub!("\\'", "'") + str.gsub!("\\{", "{") + str.gsub!("\\}", "}") + str.gsub!("\\\\", "\\") + + str + end + + rule(:states_json_to_string => {:args => subtree(:args)}) do + args = Transformer.process_args(args(), "States.JsonToString", [Object]) + json = args.first + + JSON.generate(json) + end + + rule(:states_string_to_json => {:args => subtree(:args)}) do + args = Transformer.process_args(args(), "States.StringToJson", [String]) + str = args.first + + JSON.parse(str) + rescue JSON::ParserError => e + raise ArgumentError, "invalid value for argument 1 to States.StringToJson (invalid json: #{e.message})" + end + rule(:states_array => {:args => subtree(:args)}) do Transformer.process_args(args, "States.Array") end rule(:states_array_partition => {:args => subtree(:args)}) do @@ -133,11 +191,11 @@ end rule(:states_hash => {:args => subtree(:args)}) do args = Transformer.process_args(args(), "States.Hash", [Object, String]) data, algorithm = *args - raise NotImplementedError if data.kind_of?(Hash) + if data.nil? raise ArgumentError, "invalid value for argument 1 to States.Hash (given null, expected non-null)" end algorithms = %w[MD5 SHA-1 SHA-256 SHA-384 SHA-512] @@ -145,11 +203,23 @@ raise ArgumentError, "invalid value for argument 2 to States.Hash (given #{algorithm.inspect}, expected one of #{algorithms.map(&:inspect).join(", ")})" end require "openssl" algorithm = algorithm.sub("-", "") - data = data.to_json unless data.kind_of?(String) - OpenSSL::Digest.hexdigest(algorithm, data) + data = JSON.generate(data) unless data.kind_of?(String) + OpenSSL::Digest.hexdigest(algorithm, data).force_encoding("UTF-8") + end + + rule(:states_json_merge => {:args => subtree(:args)}) do + args = Transformer.process_args(args(), "States.JsonMerge", [Hash, Hash, [TrueClass, FalseClass]]) + left, right, deep = *args + + if deep + # NOTE: not implemented by AWS Step Functions and nuances not defined in docs + left.merge(right) { |_key, l, r| l.kind_of?(Hash) && r.kind_of?(Hash) ? l.merge(r) : r } + else + left.merge(right) + end end rule(:states_math_random => {:args => subtree(:args)}) do args = Transformer.process_args(args(), "States.MathRandom", [Integer, Integer, OptionalArg[Integer]]) range_start, range_end, seed = *args