cel 0.3.1 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +32 -1
- data/README.md +26 -3
- data/lib/cel/ast/cel_methods.rb +58 -0
- data/lib/cel/ast/elements/bool.rb +34 -0
- data/lib/cel/ast/elements/bytes.rb +34 -0
- data/lib/cel/ast/elements/condition.rb +22 -0
- data/lib/cel/ast/elements/duration.rb +114 -0
- data/lib/cel/ast/elements/function.rb +33 -0
- data/lib/cel/ast/elements/group.rb +19 -0
- data/lib/cel/ast/elements/identifier.rb +32 -0
- data/lib/cel/ast/elements/invoke.rb +64 -0
- data/lib/cel/ast/elements/list.rb +70 -0
- data/lib/cel/ast/elements/literal.rb +74 -0
- data/lib/cel/ast/elements/map.rb +120 -0
- data/lib/cel/ast/elements/message.rb +50 -0
- data/lib/cel/ast/elements/null.rb +11 -0
- data/lib/cel/ast/elements/number.rb +70 -0
- data/lib/cel/ast/elements/operation.rb +39 -0
- data/lib/cel/ast/elements/protobuf.rb +4 -0
- data/lib/cel/ast/elements/string.rb +54 -0
- data/lib/cel/ast/elements/timestamp.rb +109 -0
- data/lib/cel/ast/elements.rb +18 -828
- data/lib/cel/ast/types.rb +49 -4
- data/lib/cel/checker.rb +47 -31
- data/lib/cel/context.rb +1 -1
- data/lib/cel/environment.rb +15 -2
- data/lib/cel/errors.rb +11 -0
- data/lib/cel/extensions/bind.rb +35 -0
- data/lib/cel/extensions/encoders.rb +41 -0
- data/lib/cel/extensions/math.rb +211 -0
- data/lib/cel/extensions/string.rb +230 -0
- data/lib/cel/extensions.rb +15 -0
- data/lib/cel/parser.rb +14 -13
- data/lib/cel/program.rb +28 -13
- data/lib/cel/version.rb +1 -1
- data/lib/cel.rb +27 -12
- metadata +39 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
class Map < Literal
|
|
5
|
+
ALLOWED_TYPES = %i[int uint bool string].map { |typ| TYPES[typ] }.freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :depth
|
|
8
|
+
|
|
9
|
+
def initialize(value, depth: 1)
|
|
10
|
+
# store array internally to support repeated keys
|
|
11
|
+
value = value.map do |k, v|
|
|
12
|
+
[Literal.to_cel_type(k), Literal.to_cel_type(v)]
|
|
13
|
+
end
|
|
14
|
+
super(TYPES[:map], value)
|
|
15
|
+
@depth = depth
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ==(other)
|
|
19
|
+
case other
|
|
20
|
+
when Map
|
|
21
|
+
@value.all? do |args|
|
|
22
|
+
other.value.include?(args)
|
|
23
|
+
end
|
|
24
|
+
when Array
|
|
25
|
+
# calls to != will downgrade maps to arrays
|
|
26
|
+
@value.all? do |args|
|
|
27
|
+
other.include?(args)
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
(LOGICAL_OPERATORS - %w[== != in]).each do |op|
|
|
35
|
+
@cel_methods.delete(op.to_sym)
|
|
36
|
+
|
|
37
|
+
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
38
|
+
def #{op}(*)
|
|
39
|
+
raise NoMethodError
|
|
40
|
+
end
|
|
41
|
+
OUT
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def key?(key)
|
|
45
|
+
ukey =
|
|
46
|
+
case key
|
|
47
|
+
when Symbol
|
|
48
|
+
key.to_s
|
|
49
|
+
when String
|
|
50
|
+
key.delete_prefix('"').delete_suffix('"')
|
|
51
|
+
else
|
|
52
|
+
key
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@value.any? do |k, _v|
|
|
56
|
+
k == ukey
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
alias_method :include?, :key?
|
|
61
|
+
|
|
62
|
+
define_cel_method(:[]) do |key|
|
|
63
|
+
ukey =
|
|
64
|
+
case key
|
|
65
|
+
when Symbol
|
|
66
|
+
key.to_s
|
|
67
|
+
when String
|
|
68
|
+
key.delete_prefix('"').delete_suffix('"')
|
|
69
|
+
else
|
|
70
|
+
key
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
values = @value.filter_map do |k, v|
|
|
74
|
+
v if k == ukey
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
raise NoSuchKeyError.new(self, key) if values.empty?
|
|
78
|
+
|
|
79
|
+
raise EvaluateError, "Failed with repeated key" if values.size > 1
|
|
80
|
+
|
|
81
|
+
values.first
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_ruby_type
|
|
85
|
+
value.to_h { |args| args.map(&:to_ruby_type) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def respond_to_missing?(meth, *args)
|
|
89
|
+
return true if super
|
|
90
|
+
return false unless @value
|
|
91
|
+
|
|
92
|
+
meth_s = meth.to_s
|
|
93
|
+
|
|
94
|
+
@value.any? { |k, _| k == meth_s }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def method_missing(meth, *args)
|
|
98
|
+
return super unless @value
|
|
99
|
+
|
|
100
|
+
meth_s = meth.to_s
|
|
101
|
+
values = @value.filter_map { |k, v| v if k == meth_s }
|
|
102
|
+
|
|
103
|
+
return super if values.empty?
|
|
104
|
+
|
|
105
|
+
values.first
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# For a map, the entry keys are sub-expressions that must evaluate to values
|
|
111
|
+
# of an allowed type (int, uint, bool, or string)
|
|
112
|
+
def check
|
|
113
|
+
return if @value.all? do |key, _|
|
|
114
|
+
key.is_a?(Identifier) || ALLOWED_TYPES.include?(key.type)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
raise CheckError, "#{self} is invalid (keys must be of an allowed type (int, uint, bool, or string)"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
class Message
|
|
5
|
+
EMPTY_STRUCT = {}.freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :name, :struct, :package, :depth
|
|
8
|
+
|
|
9
|
+
def initialize(name, struct, package: nil, depth: 1)
|
|
10
|
+
@struct = struct ? struct.transform_keys(&:to_sym) : EMPTY_STRUCT
|
|
11
|
+
@name = name
|
|
12
|
+
@package = package
|
|
13
|
+
@depth = depth
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def type
|
|
17
|
+
@type ||= if proto_type
|
|
18
|
+
Protobuf.convert_to_cel_type(proto_type, @package) || proto_type
|
|
19
|
+
else
|
|
20
|
+
Protobuf.try_convert_from_wrapper_type(@name.to_s)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def message_type
|
|
25
|
+
@message_type ||= proto_type || container
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def proto_type
|
|
29
|
+
@proto_type ||= Protobuf.convert_to_proto_type(@name.to_s, @package)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ==(other)
|
|
33
|
+
other.is_a?(Cel::Message) && other.name == @name && other.struct == @struct
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
if proto_type
|
|
38
|
+
proto_type.descriptor.name
|
|
39
|
+
else
|
|
40
|
+
message_type.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def container
|
|
47
|
+
@container ||= Cel.message_container(@name, @struct)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
class Number < Literal
|
|
5
|
+
def ==(other)
|
|
6
|
+
case other
|
|
7
|
+
when Number
|
|
8
|
+
@value == other.value
|
|
9
|
+
else
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
([:+, :-, *(MULTI_OPERATORS - %w[%])] - %i[%]).each do |op|
|
|
15
|
+
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
16
|
+
define_cel_method(:#{op}) do |other|
|
|
17
|
+
value = super(other)
|
|
18
|
+
check_overflow(value)
|
|
19
|
+
Number.new(@type, value)
|
|
20
|
+
end
|
|
21
|
+
OUT
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
(LOGICAL_OPERATORS - %w[== != in]).each do |op|
|
|
25
|
+
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
26
|
+
define_cel_method(:#{op}) do |other|
|
|
27
|
+
super(other)
|
|
28
|
+
end
|
|
29
|
+
OUT
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
define_cel_method(:%) do |other|
|
|
33
|
+
raise EvaluateError, "no matching overload" if @type == :double && other.type == :double
|
|
34
|
+
|
|
35
|
+
value = if negative?
|
|
36
|
+
if other.negative?
|
|
37
|
+
-(abs % other.abs)
|
|
38
|
+
else
|
|
39
|
+
-(abs % other)
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
@value % other.abs
|
|
43
|
+
end
|
|
44
|
+
check_overflow(value)
|
|
45
|
+
Number.new(@type, value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
%i[+ -].each do |op|
|
|
49
|
+
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
50
|
+
def #{op}@
|
|
51
|
+
raise NoOverloadError if @type == TYPES[:uint]
|
|
52
|
+
value = super
|
|
53
|
+
Number.new(@type, value)
|
|
54
|
+
end
|
|
55
|
+
OUT
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def check_overflow(value = @value)
|
|
59
|
+
case @type
|
|
60
|
+
when TYPES[:double]
|
|
61
|
+
return if value.nan? || value.infinite?
|
|
62
|
+
raise EvaluateError, "return error for overflow" unless (-(MAX_FLOAT - 1)...MAX_FLOAT).cover?(value)
|
|
63
|
+
when TYPES[:uint]
|
|
64
|
+
raise EvaluateError, "return error for overflow" unless (0...MAX_INT).cover?(value)
|
|
65
|
+
else
|
|
66
|
+
raise EvaluateError, "return error for overflow" unless (-(MAX_INT - 1)...MAX_INT).cover?(value)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
class Operation
|
|
5
|
+
attr_reader :op, :operands, :depth
|
|
6
|
+
|
|
7
|
+
attr_accessor :type
|
|
8
|
+
|
|
9
|
+
def initialize(op, operands, depth: 1)
|
|
10
|
+
@op = op
|
|
11
|
+
@operands = operands
|
|
12
|
+
@type = TYPES[:any]
|
|
13
|
+
@depth = depth
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ==(other)
|
|
17
|
+
case other
|
|
18
|
+
when Array
|
|
19
|
+
other.size == @operands.size + 1 &&
|
|
20
|
+
other.first == @op &&
|
|
21
|
+
other.slice(1..-1).zip(@operands).all? { |x1, x2| x1 == x2 }
|
|
22
|
+
when Operation
|
|
23
|
+
@op == other.op && @type == other.type && @operands == other.operands
|
|
24
|
+
else
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def unary?
|
|
30
|
+
@operands.size == 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
return "#{@op}#{@operands.first}" if @operands.size == 1
|
|
35
|
+
|
|
36
|
+
@operands.join(" #{@op} ")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -400,6 +400,10 @@ module Cel
|
|
|
400
400
|
end
|
|
401
401
|
|
|
402
402
|
module CelComparisonMode
|
|
403
|
+
# TODO: this does not isolate the scope of methods callable under cel.
|
|
404
|
+
# we'll need a different version of the CelMethods mixin for this.
|
|
405
|
+
alias_method :cel_send, :public_send
|
|
406
|
+
|
|
403
407
|
def ==(other)
|
|
404
408
|
super || begin
|
|
405
409
|
return false unless other.is_a?(Protobuf.base_class)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
class String < Literal
|
|
5
|
+
def initialize(value)
|
|
6
|
+
super(:string, value)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# override base String#%
|
|
10
|
+
@cel_methods.delete(:%)
|
|
11
|
+
|
|
12
|
+
def %(_other)
|
|
13
|
+
raise NoMethodError, "undefined method '%' for #{self}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
(LOGICAL_OPERATORS - %w[== != in]).each do |op|
|
|
17
|
+
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
18
|
+
define_cel_method(:#{op}) do |other|
|
|
19
|
+
super(other)
|
|
20
|
+
end
|
|
21
|
+
OUT
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# CEL string functions
|
|
25
|
+
|
|
26
|
+
define_cel_method(:contains) do |string|
|
|
27
|
+
Bool.cast(@value.include?(string))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
define_cel_method(:endsWith) do |string|
|
|
31
|
+
Bool.cast(@value.end_with?(string))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
define_cel_method(:startsWith) do |string|
|
|
35
|
+
Bool.cast(@value.start_with?(string))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
define_cel_method(:matches) do |pattern|
|
|
39
|
+
Macro.matches(self, pattern)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
%i[+ -].each do |op|
|
|
43
|
+
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
44
|
+
define_cel_method(:#{op}) do |other|
|
|
45
|
+
String.new(super(other))
|
|
46
|
+
end
|
|
47
|
+
OUT
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
inspect
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Cel
|
|
6
|
+
class Timestamp < Literal
|
|
7
|
+
TIMESTAMP_RANGE = (Time.parse("0001-01-01T00:00:00Z")..Time.parse("9999-12-31T23:59:59.999999999Z"))
|
|
8
|
+
|
|
9
|
+
def initialize(value)
|
|
10
|
+
value = case value
|
|
11
|
+
when ::String then Time.parse(value)
|
|
12
|
+
when Numeric then Time.at(value)
|
|
13
|
+
else value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
raise EvaluateError, "out of range" unless TIMESTAMP_RANGE.include?(value)
|
|
17
|
+
|
|
18
|
+
super(:timestamp, value.round(9))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
define_cel_method(:+) do |other|
|
|
22
|
+
Timestamp.new(@value + other.to_f)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
define_cel_method(:-) do |other|
|
|
26
|
+
case other
|
|
27
|
+
when Timestamp
|
|
28
|
+
Duration.new(@value - other.value)
|
|
29
|
+
when Duration
|
|
30
|
+
Timestamp.new(@value - other.to_f)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
LOGICAL_OPERATORS.each do |op|
|
|
35
|
+
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
36
|
+
define_cel_method(:#{op}) do |other|
|
|
37
|
+
val = super(other)
|
|
38
|
+
other.is_a?(Cel::Literal) ? Bool.cast(val) : val
|
|
39
|
+
end
|
|
40
|
+
OUT
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Cel Functions
|
|
44
|
+
|
|
45
|
+
define_cel_method(:getDate) do |tz = nil|
|
|
46
|
+
Number.new(:int, to_local_time(tz).day)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
define_cel_method(:getDayOfMonth) do |tz = nil|
|
|
50
|
+
getDate(tz) - 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
define_cel_method(:getDayOfWeek) do |tz = nil|
|
|
54
|
+
Number.new(:int, to_local_time(tz).wday)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
define_cel_method(:getDayOfYear) do |tz = nil|
|
|
58
|
+
Number.new(:int, to_local_time(tz).yday - 1)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
define_cel_method(:getMonth) do |tz = nil|
|
|
62
|
+
Number.new(:int, to_local_time(tz).month - 1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
define_cel_method(:getFullYear) do |tz = nil|
|
|
66
|
+
Number.new(:int, to_local_time(tz).year)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
define_cel_method(:getHours) do |tz = nil|
|
|
70
|
+
Number.new(:int, to_local_time(tz).hour)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
define_cel_method(:getMinutes) do |tz = nil|
|
|
74
|
+
Number.new(:int, to_local_time(tz).min)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
define_cel_method(:getSeconds) do |tz = nil|
|
|
78
|
+
Number.new(:int, to_local_time(tz).sec)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
define_cel_method(:getMilliseconds) do |tz = nil|
|
|
82
|
+
Number.new(:int, to_local_time(tz).nsec / 1_000_000)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_ruby_type
|
|
86
|
+
Protobuf.timestamp_class.from_time(@value)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_s
|
|
90
|
+
@value.utc.iso8601
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def to_local_time(tz = nil)
|
|
96
|
+
time = @value
|
|
97
|
+
if tz
|
|
98
|
+
tz = tz.value
|
|
99
|
+
if tz.match?(/\A[+-]?\d{2,}:\d{2,}\z/)
|
|
100
|
+
tz.prepend("+") unless tz.start_with?("+", "-")
|
|
101
|
+
else
|
|
102
|
+
tz = TZInfo::Timezone.get(tz)
|
|
103
|
+
end
|
|
104
|
+
time = time.getlocal(tz)
|
|
105
|
+
end
|
|
106
|
+
time
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|