cel 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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)