cel 0.1.2 → 0.2.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: d81d1083943f4f6d5bc415963b01c2f9be140074ae63493601b1613422fe29dd
4
- data.tar.gz: 4ea24204656323c4bc8313293e64186973f28f84c4878b94804c5cb32b0cc047
3
+ metadata.gz: 868e7e643aad1444aa54da0dab45e583a49abf2e6af0dcbbd1512b88af209e88
4
+ data.tar.gz: 2f174f88813b63181f1c16e701d1baf961eda0d9ef18c575b29b5afdc53d450c
5
5
  SHA512:
6
- metadata.gz: 22d25e42ca7b497e2dccbaf239a649a1a66af83001d9580faabf511cc71d8db76eca1d9ededd0fbfb3ba9bde25834750809022983364b6591bf8da1c99c6673c
7
- data.tar.gz: 4526e92a70b428e512654fccc94adbe9dd4a0a32534aa1792b5633a72e55cc95e67c4ef99b5ab5b272cf822e7a7175ef28e73ada9f2e61ca5613699d715a78ca
6
+ metadata.gz: 26c83e074c1729c00f5b8c3ca271865bb009f562326c6b6ddca3d5c127cf7c7f33ab93b3c964d4db0eafbe442e07dc1413abf2dc5b5ac487950da2d02fc8cd69
7
+ data.tar.gz: 3aa55062c1693eb5c3d3ad45bb09d95028c5115a9428d8bf2fdd3189b5c8869c4b6d92c6aa3e864b515f555fe78d3015fbeaf03799d64f0ef9f8365ce95a87d0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2023-01-17
4
+
5
+ ### Features
6
+
7
+ #### Timestamp/Duration types
8
+
9
+ Implementing `Cel::Timestamp` and `Cel::Duration` CEL types, along with support for functions for those types (such as `getDate`, `getDayOfYear`).
10
+
11
+ ```ruby
12
+ Cel::Environment.new.evaluate("timestamp('2022-12-25T00:00:00Z').getDate()") #=> 25
13
+ Cel::Environment.new.evaluate("duration('3600s10ms').getHours()") #=> 1
14
+ ```
15
+
16
+ #### Protobuf-to-CEL conversion
17
+
18
+ Support for auto-conversion of certain protobuf messages in the `google.protobf` package to CEL types.
19
+
20
+ https://github.com/google/cel-spec/blob/master/doc/langdef.md#dynamic-values
21
+
22
+ ```ruby
23
+ Cel::Environment.new.evaluate("google.protobuf.BoolValue{value: true}") #=> true
24
+ Cel::Environment.new.evaluate("google.protobuf.Value{string_value: 'bla'}") #=> "bla"
25
+ ```
26
+
27
+ #### Custom Functions
28
+
29
+ `cel` supports the definition of custom functions in the environment, to be used in expressions:
30
+
31
+ ```ruby
32
+ Cel::Environment.new(foo: Cel::Function(:int, :int, return_type: :int) { |a, b| a + b }).evaluate("foo(2, 2)") #=> 4
33
+ ```
34
+
35
+ ### Expression encoding/decoding
36
+
37
+ Expressions can now be encoded and decoded, to improve storage / reparsing to and from JSON, for example.
38
+
39
+ ```ruby
40
+ enc = Cel::Environment.new.encode("1 == 2") #=> ["op", ...
41
+ store_to_db(JSON.dump(enc))
42
+
43
+ # then, somewhere else
44
+ env = Cel::Environment.new
45
+ ast = env.decode(JSON.parse(read_from_db))
46
+ env.evaluate(ast) #=> 3
47
+ ```
48
+
49
+ **NOTE**: This feature is only available in ruby 3.1 .
50
+
51
+ ### Bugfixes
52
+
53
+ * fixed parser bug disallowing identifiers composed "true" or "false" (such as "true_name").
54
+
3
55
  ## [0.1.2] - 2022-11-10
4
56
 
5
57
  point release to update links in rubygems.
data/README.md CHANGED
@@ -71,7 +71,7 @@ puts return_value #=> true
71
71
  If `google/protobuf` is available in the environment, `cel-ruby` will also be able to integrate with protobuf declarations in CEL expressions.
72
72
 
73
73
  ```ruby
74
- # gem "google-protobuf" in your Gemfile
74
+ require "google/protobuf"
75
75
  require "cel"
76
76
 
77
77
  env = Cel::Environment.new
@@ -79,6 +79,18 @@ env = Cel::Environment.new
79
79
  env.evaluate("google.protobuf.Duration{seconds: 123}.seconds == 123") #=> true
80
80
  ```
81
81
 
82
+ ### Custom functions
83
+
84
+ `cel-ruby` allows you to define custom functions to be used insde CEL expressions. While we **strongly** recommend usage of `Cel::Function` for defining them (due to the ability of them being used for checking), the only requirement is that the function object responds to `.call`:
85
+
86
+ ```ruby
87
+ env = environment(foo: Cel::Function(:int, :int, return_type: :int) { |a, b| a + b})
88
+ env.evaluate("foo(2, 2)") #=> 4
89
+
90
+ # this is also possible, just not as type-safe
91
+ env2 = environment(foo: -> (a, b) { a + b})
92
+ env2.evaluate("foo(2, 2)") #=> 4
93
+ ```
82
94
 
83
95
  ## Supported Rubies
84
96
 
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "google/protobuf/struct_pb"
4
+ require "google/protobuf/wrappers_pb"
5
+ require "google/protobuf/any_pb"
6
+ require "google/protobuf/well_known_types"
7
+
8
+ module Cel
9
+ module Protobuf
10
+ module_function
11
+
12
+ def convert_from_protobuf(msg)
13
+ case msg
14
+ when Google::Protobuf::Any
15
+
16
+ any.unpack(type_msg).to_ruby
17
+ when Google::Protobuf::ListValue
18
+ msg.to_a
19
+ when Google::Protobuf::Struct
20
+ msg.to_h
21
+ when Google::Protobuf::Value
22
+ msg.to_ruby
23
+ when Google::Protobuf::BoolValue,
24
+ Google::Protobuf::BytesValue,
25
+ Google::Protobuf::DoubleValue,
26
+ Google::Protobuf::FloatValue,
27
+ Google::Protobuf::Int32Value,
28
+ Google::Protobuf::Int64Value,
29
+ Google::Protobuf::UInt32Value,
30
+ Google::Protobuf::UInt64Value,
31
+ Google::Protobuf::NullValue,
32
+ Google::Protobuf::StringValue
33
+ msg.value
34
+ when Google::Protobuf::Timestamp
35
+ msg.to_time
36
+ when Google::Protobuf::Duration
37
+ Cel::Duration.new(seconds: msg.seconds, nanos: msg.nanos)
38
+ else
39
+ raise Error, "#{msg.class}: protobuf to cel unsupported"
40
+ end
41
+ end
42
+
43
+ def convert_from_type(type, value)
44
+ case type
45
+ when "Any", "google.protobuf.Any"
46
+ type_url = value[Identifier.new("type_url")].value
47
+ _, type_msg = type_url.split("/", 2)
48
+ type_msg = const_get(type_msg.split(".").map(&:capitalize).join("::"))
49
+ encoded_msg = value[Identifier.new("value")].value.pack("C*")
50
+ any = Google::Protobuf::Any.new(type_url: type_url, value: encoded_msg)
51
+ value = Literal.to_cel_type(any.unpack(type_msg).to_ruby)
52
+ when "ListValue", "google.protobuf.ListValue"
53
+ value = value.nil? ? List.new([]) : value[Identifier.new("values")]
54
+ when "Struct", "google.protobuf.Struct"
55
+ value = value.nil? ? Map.new({}) : value[Identifier.new("fields")]
56
+ when "Value", "google.protobuf.Value"
57
+ return Null.new if value.nil?
58
+
59
+ key = value.keys.first
60
+
61
+ value = value.fetch(key, value)
62
+
63
+ value = Number.new(:double, value) if key == "number_value"
64
+
65
+ value
66
+ when "BoolValue", "google.protobuf.BoolValue"
67
+ value = value.nil? ? Bool.new(false) : value[Identifier.new("value")]
68
+ when "BytesValue", "google.protobuf.BytesValue"
69
+ value = value[Identifier.new("value")]
70
+ when "DoubleValue", "google.protobuf.DoubleValue",
71
+ "FloatValue", "google.protobuf.FloatValue"
72
+ value = value.nil? ? Number.new(:double, 0.0) : value[Identifier.new("value")]
73
+ value.type = TYPES[:double]
74
+ when "Int32Value", "google.protobuf.Int32Value",
75
+ "Int64Value", "google.protobuf.Int64Value"
76
+ value = value.nil? ? Number.new(:int, 0) : value[Identifier.new("value")]
77
+ when "Uint32Value", "google.protobuf.UInt32Value",
78
+ "Uint64Value", "google.protobuf.UInt64Value"
79
+ value = value.nil? ? Number.new(:uint, 0) : value[Identifier.new("value")]
80
+ when "NullValue", "google.protobuf.NullValue"
81
+ value = Null.new
82
+ when "StringValue", "google.protobuf.StringValue"
83
+ value = value.nil? ? String.new(+"") : value[Identifier.new("value")]
84
+ when "Timestamp", "google.protobuf.Timestamp"
85
+ seconds = value.fetch(Identifier.new("seconds"), 0)
86
+ nanos = value.fetch(Identifier.new("nanos"), 0)
87
+ value = Timestamp.new(Time.at(seconds, nanos, :nanosecond))
88
+ when "Duration", "google.protobuf.Duration"
89
+ seconds = value.fetch(Identifier.new("seconds"), 0)
90
+ nanos = value.fetch(Identifier.new("nanos"), 0)
91
+ value = Duration.new(seconds: seconds, nanos: nanos)
92
+ end
93
+ value
94
+ end
95
+
96
+ def try_invoke_from(var, func, args)
97
+ case var
98
+ when "Any", "google.protobuf.Any",
99
+ "ListValue", "google.protobuf.ListValue",
100
+ "Struct", "google.protobuf.Struct",
101
+ "Value", "google.protobuf.Value",
102
+ "BoolValue", "google.protobuf.BoolValue",
103
+ "BytesValue", "google.protobuf.BytesValue",
104
+ "DoubleValue", "google.protobuf.DoubleValue",
105
+ "FloatValue", "google.protobuf.FloatValue",
106
+ "Int32Value", "google.protobuf.Int32Value",
107
+ "Int64Value", "google.protobuf.Int64Value",
108
+ "Uint32Value", "google.protobuf.Uint32Value",
109
+ "Uint64Value", "google.protobuf.Uint64Value",
110
+ "NullValue", "google.protobuf.NullValue",
111
+ "StringValue", "google.protobuf.StringValue",
112
+ "Timestamp", "google.protobuf.Timestamp",
113
+ "Duration", "google.protobuf.Duration"
114
+ protoclass = var.split(".").last
115
+ protoclass = Google::Protobuf.const_get(protoclass)
116
+
117
+ value = if args.nil? && protoclass.constants.include?(func.to_sym)
118
+ protoclass.const_get(func)
119
+ else
120
+ protoclass.__send__(func, *args)
121
+ end
122
+
123
+ Literal.to_cel_type(value)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
3
4
  require "delegate"
5
+ require_relative "elements/protobuf"
4
6
 
5
7
  module Cel
6
8
  LOGICAL_OPERATORS = %w[< <= >= > == != in].freeze
@@ -20,11 +22,22 @@ module Cel
20
22
  def ==(other)
21
23
  super || other.to_s == @id.to_s
22
24
  end
25
+
26
+ def to_s
27
+ @id.to_s
28
+ end
23
29
  end
24
30
 
25
31
  class Message < SimpleDelegator
26
32
  attr_reader :type, :struct
27
33
 
34
+ def self.new(type, struct)
35
+ value = convert_from_type(type, struct)
36
+ return value if value.is_a?(Null) || value != struct
37
+
38
+ super
39
+ end
40
+
28
41
  def initialize(type, struct)
29
42
  @struct = Struct.new(*struct.keys.map(&:to_sym)).new(*struct.values)
30
43
  @type = type.is_a?(Type) ? type : MapType.new(struct.to_h do |k, v|
@@ -36,11 +49,32 @@ module Cel
36
49
  def field?(key)
37
50
  !@type.get(key).nil?
38
51
  end
52
+
53
+ def self.convert_from_type(type, value)
54
+ case type
55
+ when Invoke, Identifier
56
+ spread_type = type.to_s
57
+ Protobuf.convert_from_type(spread_type, value)
58
+ when Type
59
+ [type, value]
60
+ else
61
+ [
62
+ MapType.new(struct.to_h do |k, v|
63
+ [Literal.to_cel_type(k), Literal.to_cel_type(v)]
64
+ end),
65
+ Struct.new(*struct.keys.map(&:to_sym)).new(*struct.values),
66
+ ]
67
+ end
68
+ end
39
69
  end
40
70
 
41
71
  class Invoke
42
72
  attr_reader :var, :func, :args
43
73
 
74
+ def self.new(func:, var: nil, args: nil)
75
+ Protobuf.try_invoke_from(var, func, args) || super
76
+ end
77
+
44
78
  def initialize(func:, var: nil, args: nil)
45
79
  @var = var
46
80
  @func = func.to_sym
@@ -48,10 +82,14 @@ module Cel
48
82
  end
49
83
 
50
84
  def ==(other)
51
- super || (
52
- other.respond_to?(:to_ary) &&
85
+ case other
86
+ when Invoke
87
+ @var == other.var && @func == other.func && @args == other.args
88
+ when Array
53
89
  [@var, @func, @args].compact == other
54
- )
90
+ else
91
+ super
92
+ end
55
93
  end
56
94
 
57
95
  def to_s
@@ -67,6 +105,33 @@ module Cel
67
105
  end
68
106
  end
69
107
 
108
+ class Function
109
+ attr_reader :types, :type
110
+
111
+ def initialize(*types, return_type: nil, &func)
112
+ unless func.nil?
113
+ types = Array.new(func.arity) { TYPES[:any] } if types.empty?
114
+ raise(Error, "number of arg types does not match number of yielded args") unless types.size == func.arity
115
+ end
116
+ @types = types.map { |typ| typ.is_a?(Type) ? typ : TYPES[typ] }
117
+ @type = if return_type.nil?
118
+ TYPES[:any]
119
+ else
120
+ return_type.is_a?(Type) ? return_type : TYPES[return_type]
121
+ end
122
+ @func = func
123
+ end
124
+
125
+ def call(*args)
126
+ Literal.to_cel_type(@func.call(*args))
127
+ end
128
+ end
129
+
130
+ mod = self
131
+ mod.define_singleton_method(:Function) do |*args, **kwargs, &blk|
132
+ mod::Function.new(*args, **kwargs, &blk)
133
+ end
134
+
70
135
  class Literal < SimpleDelegator
71
136
  attr_reader :type, :value
72
137
 
@@ -82,6 +147,8 @@ module Cel
82
147
  end
83
148
 
84
149
  def self.to_cel_type(val)
150
+ val = Protobuf.convert_from_protobuf(val) if val.is_a?(Google::Protobuf::MessageExts)
151
+
85
152
  case val
86
153
  when Literal, Identifier
87
154
  val
@@ -102,6 +169,8 @@ module Cel
102
169
  Bool.new(val)
103
170
  when nil
104
171
  Null.new
172
+ when Time
173
+ Timestamp.new(val)
105
174
  else
106
175
  raise BindingError, "can't convert #{val} to CEL type"
107
176
  end
@@ -274,12 +343,178 @@ module Cel
274
343
  end
275
344
  end
276
345
 
346
+ class Timestamp < Literal
347
+ def initialize(value)
348
+ value = case value
349
+ when String then Time.parse(value)
350
+ when Numeric then Time.at(value)
351
+ else value
352
+ end
353
+ super(:timestamp, value)
354
+ end
355
+
356
+ def +(other)
357
+ Timestamp.new(@value + other.to_f)
358
+ end
359
+
360
+ def -(other)
361
+ case other
362
+ when Timestamp
363
+ Duration.new(@value - other.value)
364
+ when Duration
365
+ Timestamp.new(@value - other.to_f)
366
+ end
367
+ end
368
+
369
+ LOGICAL_OPERATORS.each do |op|
370
+ class_eval(<<-OUT, __FILE__, __LINE__ + 1)
371
+ def #{op}(other)
372
+ other.is_a?(Cel::Literal) ? Bool.new(super) : super
373
+ end
374
+ OUT
375
+ end
376
+
377
+ # Cel Functions
378
+
379
+ def getDate(tz = nil)
380
+ to_local_time(tz).day
381
+ end
382
+
383
+ def getDayOfMonth(tz = nil)
384
+ getDate(tz) - 1
385
+ end
386
+
387
+ def getDayOfWeek(tz = nil)
388
+ to_local_time(tz).wday
389
+ end
390
+
391
+ def getDayOfYear(tz = nil)
392
+ to_local_time(tz).yday - 1
393
+ end
394
+
395
+ def getMonth(tz = nil)
396
+ to_local_time(tz).month - 1
397
+ end
398
+
399
+ def getFullYear(tz = nil)
400
+ to_local_time(tz).year
401
+ end
402
+
403
+ def getHours(tz = nil)
404
+ to_local_time(tz).hour
405
+ end
406
+
407
+ def getMinutes(tz = nil)
408
+ to_local_time(tz).min
409
+ end
410
+
411
+ def getSeconds(tz = nil)
412
+ to_local_time(tz).sec
413
+ end
414
+
415
+ def getMilliseconds(tz = nil)
416
+ to_local_time(tz).nsec / 1_000_000
417
+ end
418
+
419
+ private
420
+
421
+ def to_local_time(tz = nil)
422
+ time = @value
423
+ if tz
424
+ tz = TZInfo::Timezone.get(tz) unless tz.match?(/\A[+-]\d{2,}:\d{2,}\z/)
425
+ time = time.getlocal(tz)
426
+ end
427
+ time
428
+ end
429
+ end
430
+
431
+ class Duration < Literal
432
+ def initialize(value)
433
+ value = case value
434
+ when String
435
+ init_from_string(value)
436
+ when Hash
437
+ seconds, nanos = value.values_at(:seconds, :nanos)
438
+ seconds ||= 0
439
+ nanos ||= 0
440
+ seconds + (nanos / 1_000_000_000.0)
441
+ else
442
+ value
443
+ end
444
+ super(:duration, value)
445
+ end
446
+
447
+ LOGICAL_OPERATORS.each do |op|
448
+ class_eval(<<-OUT, __FILE__, __LINE__ + 1)
449
+ def #{op}(other)
450
+ case other
451
+ when Cel::Literal
452
+ Bool.new(super)
453
+ when Numeric
454
+ @value == other
455
+
456
+ else
457
+ super
458
+ end
459
+ end
460
+ OUT
461
+ end
462
+
463
+ # Cel Functions
464
+
465
+ def getHours
466
+ (getMinutes / 60).to_i
467
+ end
468
+
469
+ def getMinutes
470
+ (getSeconds / 60).to_i
471
+ end
472
+
473
+ def getSeconds
474
+ @value.divmod(1).first
475
+ end
476
+
477
+ def getMilliseconds
478
+ (@value.divmod(1).last * 1000).round
479
+ end
480
+
481
+ private
482
+
483
+ def init_from_string(value)
484
+ seconds = 0
485
+ nanos = 0
486
+ value.scan(/([0-9]*(?:\.[0-9]*)?)([a-z]+)/) do |duration, units|
487
+ case units
488
+ when "h"
489
+ seconds += Cel.to_numeric(duration) * 60 * 60
490
+ when "m"
491
+ seconds += Cel.to_numeric(duration) * 60
492
+ when "s"
493
+ seconds += Cel.to_numeric(duration)
494
+ when "ms"
495
+ nanos += Cel.to_numeric(duration) * 1000 * 1000
496
+ when "us"
497
+ nanos += Cel.to_numeric(duration) * 1000
498
+ when "ns"
499
+ nanos += Cel.to_numeric(duration)
500
+ else
501
+ raise EvaluateError, "#{units} is unsupported"
502
+ end
503
+ end
504
+ seconds + (nanos / 1_000_000_000.0)
505
+ end
506
+ end
507
+
277
508
  class Group
278
509
  attr_reader :value
279
510
 
280
511
  def initialize(value)
281
512
  @value = value
282
513
  end
514
+
515
+ def ==(other)
516
+ other.is_a?(Group) && @value == other.value
517
+ end
283
518
  end
284
519
 
285
520
  class Operation
@@ -294,10 +529,13 @@ module Cel
294
529
  end
295
530
 
296
531
  def ==(other)
297
- if other.is_a?(Array)
532
+ case other
533
+ when Array
298
534
  other.size == @operands.size + 1 &&
299
535
  other.first == @op &&
300
536
  other.slice(1..-1).zip(@operands).all? { |x1, x2| x1 == x2 }
537
+ when Operation
538
+ @op == other.op && @type == other.type && @operands == other.operands
301
539
  else
302
540
  super
303
541
  end
@@ -318,5 +556,9 @@ module Cel
318
556
  @then = then_
319
557
  @else = else_
320
558
  end
559
+
560
+ def ==(other)
561
+ other.is_a?(Condition) && @if == other.if && @then == other.then && @else == other.else
562
+ end
321
563
  end
322
564
  end
data/lib/cel/ast/types.rb CHANGED
@@ -14,6 +14,8 @@ module Cel
14
14
  @type.to_s
15
15
  end
16
16
 
17
+ alias_method :to_s, :to_str
18
+
17
19
  def type
18
20
  TYPES[:type]
19
21
  end
@@ -32,6 +34,10 @@ module Cel
32
34
  Bytes.new(value.bytes)
33
35
  when :bool
34
36
  Bool.new(value)
37
+ when :timestamp
38
+ Timestamp.new(value)
39
+ when :duration
40
+ Duration.new(value)
35
41
  when :any
36
42
  value
37
43
  else
@@ -71,7 +77,7 @@ module Cel
71
77
 
72
78
  # Primitive Cel Types
73
79
 
74
- PRIMITIVE_TYPES = %i[int uint double bool string bytes list map null_type type].freeze
80
+ PRIMITIVE_TYPES = %i[int uint double bool string bytes list map timestamp duration null_type type].freeze
75
81
  TYPES = PRIMITIVE_TYPES.to_h { |typ| [typ, Type.new(typ)] }
76
82
  TYPES[:type] = Type.new(:type)
77
83
  TYPES[:any] = Type.new(:any)