lib/cel/ast/elements.rb in cel-0.1.2 vs lib/cel/ast/elements.rb in cel-0.2.0

- old
+ new

@@ -1,8 +1,10 @@ # frozen_string_literal: true +require "time" require "delegate" +require_relative "elements/protobuf" module Cel LOGICAL_OPERATORS = %w[< <= >= > == != in].freeze MULTI_OPERATORS = %w[* / %].freeze @@ -18,15 +20,26 @@ end def ==(other) super || other.to_s == @id.to_s end + + def to_s + @id.to_s + end end class Message < SimpleDelegator attr_reader :type, :struct + def self.new(type, struct) + value = convert_from_type(type, struct) + return value if value.is_a?(Null) || value != struct + + super + end + def initialize(type, struct) @struct = Struct.new(*struct.keys.map(&:to_sym)).new(*struct.values) @type = type.is_a?(Type) ? type : MapType.new(struct.to_h do |k, v| [Literal.to_cel_type(k), Literal.to_cel_type(v)] end) @@ -34,26 +47,51 @@ end def field?(key) !@type.get(key).nil? end + + def self.convert_from_type(type, value) + case type + when Invoke, Identifier + spread_type = type.to_s + Protobuf.convert_from_type(spread_type, value) + when Type + [type, value] + else + [ + MapType.new(struct.to_h do |k, v| + [Literal.to_cel_type(k), Literal.to_cel_type(v)] + end), + Struct.new(*struct.keys.map(&:to_sym)).new(*struct.values), + ] + end + end end class Invoke attr_reader :var, :func, :args + def self.new(func:, var: nil, args: nil) + Protobuf.try_invoke_from(var, func, args) || super + end + def initialize(func:, var: nil, args: nil) @var = var @func = func.to_sym @args = args end def ==(other) - super || ( - other.respond_to?(:to_ary) && + case other + when Invoke + @var == other.var && @func == other.func && @args == other.args + when Array [@var, @func, @args].compact == other - ) + else + super + end end def to_s if var if func == :[] @@ -65,10 +103,37 @@ "#{func}#{"(#{args.map(&:to_s).join(", ")})" if args}" end end end + class Function + attr_reader :types, :type + + def initialize(*types, return_type: nil, &func) + unless func.nil? + types = Array.new(func.arity) { TYPES[:any] } if types.empty? + raise(Error, "number of arg types does not match number of yielded args") unless types.size == func.arity + end + @types = types.map { |typ| typ.is_a?(Type) ? typ : TYPES[typ] } + @type = if return_type.nil? + TYPES[:any] + else + return_type.is_a?(Type) ? return_type : TYPES[return_type] + end + @func = func + end + + def call(*args) + Literal.to_cel_type(@func.call(*args)) + end + end + + mod = self + mod.define_singleton_method(:Function) do |*args, **kwargs, &blk| + mod::Function.new(*args, **kwargs, &blk) + end + class Literal < SimpleDelegator attr_reader :type, :value def initialize(type, value) @type = type.is_a?(Type) ? type : TYPES[type] @@ -80,10 +145,12 @@ def ==(other) @value == other || super end def self.to_cel_type(val) + val = Protobuf.convert_from_protobuf(val) if val.is_a?(Google::Protobuf::MessageExts) + case val when Literal, Identifier val # TODO: should support byte streams? when ::String @@ -100,10 +167,12 @@ List.new(val) when true, false Bool.new(val) when nil Null.new + when Time + Timestamp.new(val) else raise BindingError, "can't convert #{val} to CEL type" end end @@ -272,16 +341,182 @@ raise CheckError, "#{self} is invalid (keys must be of an allowed type (int, uint, bool, or string)" end end + class Timestamp < Literal + def initialize(value) + value = case value + when String then Time.parse(value) + when Numeric then Time.at(value) + else value + end + super(:timestamp, value) + end + + def +(other) + Timestamp.new(@value + other.to_f) + end + + def -(other) + case other + when Timestamp + Duration.new(@value - other.value) + when Duration + Timestamp.new(@value - other.to_f) + end + end + + LOGICAL_OPERATORS.each do |op| + class_eval(<<-OUT, __FILE__, __LINE__ + 1) + def #{op}(other) + other.is_a?(Cel::Literal) ? Bool.new(super) : super + end + OUT + end + + # Cel Functions + + def getDate(tz = nil) + to_local_time(tz).day + end + + def getDayOfMonth(tz = nil) + getDate(tz) - 1 + end + + def getDayOfWeek(tz = nil) + to_local_time(tz).wday + end + + def getDayOfYear(tz = nil) + to_local_time(tz).yday - 1 + end + + def getMonth(tz = nil) + to_local_time(tz).month - 1 + end + + def getFullYear(tz = nil) + to_local_time(tz).year + end + + def getHours(tz = nil) + to_local_time(tz).hour + end + + def getMinutes(tz = nil) + to_local_time(tz).min + end + + def getSeconds(tz = nil) + to_local_time(tz).sec + end + + def getMilliseconds(tz = nil) + to_local_time(tz).nsec / 1_000_000 + end + + private + + def to_local_time(tz = nil) + time = @value + if tz + tz = TZInfo::Timezone.get(tz) unless tz.match?(/\A[+-]\d{2,}:\d{2,}\z/) + time = time.getlocal(tz) + end + time + end + end + + class Duration < Literal + def initialize(value) + value = case value + when String + init_from_string(value) + when Hash + seconds, nanos = value.values_at(:seconds, :nanos) + seconds ||= 0 + nanos ||= 0 + seconds + (nanos / 1_000_000_000.0) + else + value + end + super(:duration, value) + end + + LOGICAL_OPERATORS.each do |op| + class_eval(<<-OUT, __FILE__, __LINE__ + 1) + def #{op}(other) + case other + when Cel::Literal + Bool.new(super) + when Numeric + @value == other + + else + super + end + end + OUT + end + + # Cel Functions + + def getHours + (getMinutes / 60).to_i + end + + def getMinutes + (getSeconds / 60).to_i + end + + def getSeconds + @value.divmod(1).first + end + + def getMilliseconds + (@value.divmod(1).last * 1000).round + end + + private + + def init_from_string(value) + seconds = 0 + nanos = 0 + value.scan(/([0-9]*(?:\.[0-9]*)?)([a-z]+)/) do |duration, units| + case units + when "h" + seconds += Cel.to_numeric(duration) * 60 * 60 + when "m" + seconds += Cel.to_numeric(duration) * 60 + when "s" + seconds += Cel.to_numeric(duration) + when "ms" + nanos += Cel.to_numeric(duration) * 1000 * 1000 + when "us" + nanos += Cel.to_numeric(duration) * 1000 + when "ns" + nanos += Cel.to_numeric(duration) + else + raise EvaluateError, "#{units} is unsupported" + end + end + seconds + (nanos / 1_000_000_000.0) + end + end + class Group attr_reader :value def initialize(value) @value = value end + + def ==(other) + other.is_a?(Group) && @value == other.value + end end class Operation attr_reader :op, :operands @@ -292,14 +527,17 @@ @operands = operands @type = TYPES[:any] end def ==(other) - if other.is_a?(Array) + case other + when Array other.size == @operands.size + 1 && other.first == @op && other.slice(1..-1).zip(@operands).all? { |x1, x2| x1 == x2 } + when Operation + @op == other.op && @type == other.type && @operands == other.operands else super end end @@ -315,8 +553,12 @@ def initialize(if_, then_, else_) @if = if_ @then = then_ @else = else_ + end + + def ==(other) + other.is_a?(Condition) && @if == other.if && @then == other.then && @else == other.else end end end