cel 0.2.0 → 0.2.2

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: 868e7e643aad1444aa54da0dab45e583a49abf2e6af0dcbbd1512b88af209e88
4
- data.tar.gz: 2f174f88813b63181f1c16e701d1baf961eda0d9ef18c575b29b5afdc53d450c
3
+ metadata.gz: ed190ec0ddbda3e8c5229160dff580efa6cf1175f8b5ed5245e8de40f1feb800
4
+ data.tar.gz: 5f5d64c6a0199a0d47c7c29a3ef0c022f666e752147cadefbe6fc7090685e819
5
5
  SHA512:
6
- metadata.gz: 26c83e074c1729c00f5b8c3ca271865bb009f562326c6b6ddca3d5c127cf7c7f33ab93b3c964d4db0eafbe442e07dc1413abf2dc5b5ac487950da2d02fc8cd69
7
- data.tar.gz: 3aa55062c1693eb5c3d3ad45bb09d95028c5115a9428d8bf2fdd3189b5c8869c4b6d92c6aa3e864b515f555fe78d3015fbeaf03799d64f0ef9f8365ce95a87d0
6
+ metadata.gz: c1c1aec4b0d8e54cf52a18057933c1ed214cddd79671ba6280f4e46f852c595f62259b4a10e2d8d0187d0b296f726b6dcae0b07aaccded157769b55f44d88900
7
+ data.tar.gz: 06c5d2653edca32b4cfb411a788734a0de1c9dcd6368f95b4ad4dcaeffe0b7fb82504c8e861d515acd19dbadb514804c5c6c108aa09e1600da0b9c1a60aec519
data/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.2] - 2023-08-17
4
+
5
+ * Reimplements `Cel::Literal#==` in a simpler way, to avoid several comparison issues arising from the previous implementation.
6
+ * fix initialization of `Cel::Duration` and `Cel::Timestamp` from string notation.
7
+ * fix protobuf-to-cel parsing of negative literals.
8
+ * more consistent usage of Cel types internally.
9
+ * fix condition clause evaluation.
10
+
11
+
12
+ ## [0.2.1] - 2023-07-26
13
+
14
+
15
+ ### Improvements
16
+
17
+ Collection type declarations are now better supported, i.e. declaring "an array of strings" is now possible:
18
+
19
+ ```ruby
20
+ Cel::Types[:list, :string] # array of strings
21
+ Cel::Environment.new(
22
+ names: Cel::Types[:list, :string],
23
+ owned_by: Cel::Types[:map, :string] # hash map of string keys
24
+ )
25
+ ```
26
+
27
+ ### Bugfixes
28
+
29
+ Collections passed as variables of an expression are now correctly cast to Cel types:
30
+
31
+ ```ruby
32
+ env.evaluate("a", { a: [1, 2, 3] }) #=> now returns a Cel::List
33
+ ```
34
+
35
+ Conversely, custom functions now expose ruby types in the blocks, instead of its Cel counterparts:
36
+
37
+ ```ruby
38
+ func = Cel::Function(:list, :list, return_type: :list) do |a, b|
39
+ # a is now a ruby array, b as well
40
+ a & b
41
+ # function returns a ruby array, will be converted to a Cel::List for use in the expression
42
+ end
43
+ ```
44
+
3
45
  ## [0.2.0] - 2023-01-17
4
46
 
5
47
  ### Features
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Cel::Ruby
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/cel.svg)](http://rubygems.org/gems/cel)
4
- [![pipeline status](https://gitlab.com/honeyryderchuck/cel-ruby/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/cel-ruby/pipelines?page=1&scope=all&ref=master)
5
- [![coverage report](https://gitlab.com/honeyryderchuck/cel-ruby/badges/master/coverage.svg?job=coverage)](https://honeyryderchuck.gitlab.io/cel-ruby/coverage/#_AllFiles)
4
+ [![pipeline status](https://gitlab.com/os85/cel-ruby/badges/master/pipeline.svg)](https://gitlab.com/os85/cel-ruby/pipelines?page=1&scope=all&ref=master)
5
+ [![coverage report](https://gitlab.com/os85/cel-ruby/badges/master/coverage.svg?job=coverage)](https://os85.gitlab.io/cel-ruby/coverage/#_AllFiles)
6
6
 
7
7
  Pure Ruby implementation of Google Common Expression Language, https://opensource.google/projects/cel.
8
8
 
@@ -45,19 +45,20 @@ end
45
45
  prg = env.program(ast)
46
46
  # 1.3 evaluate
47
47
  return_value = prg.evaluate(name: Cel::String.new("/groups/acme.co/documents/secret-stuff"),
48
- group: Cel::String.new("acme.co")))
48
+ group: Cel::String.new("acme.co"))
49
49
 
50
50
  # 2.1 parse and check
51
51
  prg = env.program('name.startsWith("/groups/" + group)')
52
52
  # 2.2 then evaluate
53
53
  return_value = prg.evaluate(name: Cel::String.new("/groups/acme.co/documents/secret-stuff"),
54
- group: Cel::String.new("acme.co")))
54
+ group: Cel::String.new("acme.co"))
55
55
 
56
56
  # 3. or parse, check and evaluate
57
57
  begin
58
58
  return_value = env.evaluate(ast,
59
59
  name: Cel::String.new("/groups/acme.co/documents/secret-stuff"),
60
- group: Cel::String.new("acme.co"))
60
+ group: Cel::String.new("acme.co")
61
+ )
61
62
  rescue Cel::Error => e
62
63
  STDERR.puts("evaluation error: #{e.message}")
63
64
  raise e
@@ -66,6 +67,20 @@ end
66
67
  puts return_value #=> true
67
68
  ```
68
69
 
70
+ ### types
71
+
72
+ `cel-ruby` supports declaring the types of variables in the environment, which allows for expression checking:
73
+
74
+ ```ruby
75
+ env = Cel::Environment.new(
76
+ first_name: :string, # shortcut for Cel::Types[:string]
77
+ middle_names: Cel::Types[:list, :string], # list of strings
78
+ last_name: :string
79
+ )
80
+
81
+ # you can use Cel::Types to access any type of primitive type, i.e. Cel::Types[:bytes]
82
+ ```
83
+
69
84
  ### protobuf
70
85
 
71
86
  If `google/protobuf` is available in the environment, `cel-ruby` will also be able to integrate with protobuf declarations in CEL expressions.
@@ -60,9 +60,15 @@ module Cel
60
60
 
61
61
  value = value.fetch(key, value)
62
62
 
63
- value = Number.new(:double, value) if key == "number_value"
64
-
65
- value
63
+ value = case key
64
+ when "null_value"
65
+ Null.new if value.zero?
66
+ when "number_value"
67
+ value = -value.operands.first if value.is_a?(Operation) && value.op == "-" && value.unary?
68
+ Number.new(:double, value)
69
+ else
70
+ value
71
+ end
66
72
  when "BoolValue", "google.protobuf.BoolValue"
67
73
  value = value.nil? ? Bool.new(false) : value[Identifier.new("value")]
68
74
  when "BytesValue", "google.protobuf.BytesValue"
@@ -143,6 +143,7 @@ module Cel
143
143
  end
144
144
 
145
145
  def ==(other)
146
+ other = other.value if other.is_a?(Literal)
146
147
  @value == other || super
147
148
  end
148
149
 
@@ -176,6 +177,10 @@ module Cel
176
177
  end
177
178
  end
178
179
 
180
+ def to_ruby_type
181
+ @value
182
+ end
183
+
179
184
  private
180
185
 
181
186
  def check; end
@@ -190,7 +195,7 @@ module Cel
190
195
  OUT
191
196
  end
192
197
 
193
- LOGICAL_OPERATORS.each do |op|
198
+ (LOGICAL_OPERATORS - %w[==]).each do |op|
194
199
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
195
200
  def #{op}(other)
196
201
  Bool.new(super)
@@ -204,13 +209,17 @@ module Cel
204
209
  super(:bool, value)
205
210
  end
206
211
 
207
- LOGICAL_OPERATORS.each do |op|
212
+ (LOGICAL_OPERATORS - %w[==]).each do |op|
208
213
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
209
214
  def #{op}(other)
210
215
  Bool.new(super)
211
216
  end
212
217
  OUT
213
218
  end
219
+
220
+ def !
221
+ Bool.new(super)
222
+ end
214
223
  end
215
224
 
216
225
  class Null < Literal
@@ -242,14 +251,6 @@ module Cel
242
251
  Macro.matches(self, pattern)
243
252
  end
244
253
 
245
- LOGICAL_OPERATORS.each do |op|
246
- class_eval(<<-OUT, __FILE__, __LINE__ + 1)
247
- def #{op}(other)
248
- other.is_a?(Cel::Literal) ? Bool.new(super) : super
249
- end
250
- OUT
251
- end
252
-
253
254
  %i[+ -].each do |op|
254
255
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
255
256
  def #{op}(other)
@@ -268,7 +269,7 @@ module Cel
268
269
  [self]
269
270
  end
270
271
 
271
- LOGICAL_OPERATORS.each do |op|
272
+ (LOGICAL_OPERATORS - %w[==]).each do |op|
272
273
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
273
274
  def #{op}(other)
274
275
  Bool.new(super)
@@ -296,6 +297,10 @@ module Cel
296
297
  def to_ary
297
298
  [self]
298
299
  end
300
+
301
+ def to_ruby_type
302
+ value.map(&:to_ruby_type)
303
+ end
299
304
  end
300
305
 
301
306
  class Map < Literal
@@ -318,6 +323,10 @@ module Cel
318
323
  [self]
319
324
  end
320
325
 
326
+ def to_ruby_type
327
+ value.to_h { |*args| args.map(&:to_ruby_type) }
328
+ end
329
+
321
330
  def respond_to_missing?(meth, *args)
322
331
  super || (@value && @value.keys.any? { |k| k.to_s == meth.to_s })
323
332
  end
@@ -346,7 +355,7 @@ module Cel
346
355
  class Timestamp < Literal
347
356
  def initialize(value)
348
357
  value = case value
349
- when String then Time.parse(value)
358
+ when ::String then Time.parse(value)
350
359
  when Numeric then Time.at(value)
351
360
  else value
352
361
  end
@@ -431,7 +440,7 @@ module Cel
431
440
  class Duration < Literal
432
441
  def initialize(value)
433
442
  value = case value
434
- when String
443
+ when ::String
435
444
  init_from_string(value)
436
445
  when Hash
437
446
  seconds, nanos = value.values_at(:seconds, :nanos)
@@ -541,6 +550,10 @@ module Cel
541
550
  end
542
551
  end
543
552
 
553
+ def unary?
554
+ @operands.size == 1
555
+ end
556
+
544
557
  def to_s
545
558
  return "#{@op}#{@operands.first}" if @operands.size == 1
546
559
 
data/lib/cel/ast/types.rb CHANGED
@@ -21,6 +21,8 @@ module Cel
21
21
  end
22
22
 
23
23
  def cast(value)
24
+ value = value.value if value.is_a?(Literal)
25
+
24
26
  case @type
25
27
  when :int
26
28
  Number.new(:int, Integer(value))
@@ -58,6 +60,14 @@ module Cel
58
60
  def get(idx)
59
61
  @type_list[idx]
60
62
  end
63
+
64
+ def ==(other)
65
+ other == :list || super
66
+ end
67
+
68
+ def cast(value)
69
+ List.new(value)
70
+ end
61
71
  end
62
72
 
63
73
  class MapType < Type
@@ -73,12 +83,45 @@ module Cel
73
83
  _, value = @type_map.find { |k, _| k == attrib.to_s }
74
84
  value
75
85
  end
86
+
87
+ def ==(other)
88
+ other == :map || super
89
+ end
90
+
91
+ def cast(value)
92
+ Map.new(value)
93
+ end
76
94
  end
77
95
 
78
96
  # Primitive Cel Types
79
97
 
80
98
  PRIMITIVE_TYPES = %i[int uint double bool string bytes list map timestamp duration null_type type].freeze
81
- TYPES = PRIMITIVE_TYPES.to_h { |typ| [typ, Type.new(typ)] }
99
+ COLTYPES = %i[list map].freeze
100
+ TYPES = (PRIMITIVE_TYPES - COLTYPES).to_h { |typ| [typ, Type.new(typ)] }
82
101
  TYPES[:type] = Type.new(:type)
83
102
  TYPES[:any] = Type.new(:any)
103
+
104
+ module CollectionTypeFetch
105
+ def [](*args)
106
+ col_type, elem_type = args
107
+
108
+ return super unless COLTYPES.include?(col_type)
109
+
110
+ return super if args.size > 2
111
+
112
+ elem_type ||= :any
113
+
114
+ type = case col_type
115
+ when :list
116
+ ListType.new([])
117
+ when :map
118
+ MapType.new({})
119
+ end
120
+
121
+ type.element_type = super(*elem_type)
122
+ type
123
+ end
124
+ end
125
+
126
+ TYPES.singleton_class.prepend(CollectionTypeFetch)
84
127
  end
data/lib/cel/checker.rb CHANGED
@@ -148,7 +148,7 @@ module Cel
148
148
  case func
149
149
  when :[]
150
150
  attribute = var_type.get(args)
151
- unsupported_operation(funcall) unless attribute
151
+ return TYPES[:any] unless attribute
152
152
  when :all, :exists, :exists_one
153
153
  check_arity(funcall, args, 2)
154
154
  identifier, predicate = args
@@ -162,7 +162,7 @@ module Cel
162
162
  return TYPES[:bool]
163
163
  else
164
164
  attribute = var_type.get(func)
165
- unsupported_operation(funcall) unless attribute
165
+ return TYPES[:any] unless attribute
166
166
  end
167
167
 
168
168
  call(attribute)
@@ -357,7 +357,7 @@ module Cel
357
357
 
358
358
  return unless typ
359
359
 
360
- return convert(typ) if id_call_chain.empty?
360
+ convert(typ) if id_call_chain.empty?
361
361
  end
362
362
 
363
363
  def convert(typ)
@@ -381,7 +381,7 @@ module Cel
381
381
  end
382
382
  end
383
383
 
384
- TYPES[type]
384
+ type && types.is_a?(Type) ? types : TYPES[type]
385
385
  end
386
386
 
387
387
  def check_arity(func, args, arity)
data/lib/cel/macro.rb CHANGED
@@ -43,21 +43,24 @@ module Cel
43
43
  end
44
44
 
45
45
  def all(collection, identifier, predicate, context:)
46
- collection.all? do |element, *|
46
+ return_value = collection.all? do |element, *|
47
47
  Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
48
48
  end
49
+ Bool.new(return_value)
49
50
  end
50
51
 
51
52
  def exists(collection, identifier, predicate, context:)
52
- collection.any? do |element, *|
53
+ return_value = collection.any? do |element, *|
53
54
  Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
54
55
  end
56
+ Bool.new(return_value)
55
57
  end
56
58
 
57
59
  def exists_one(collection, identifier, predicate, context:)
58
- collection.one? do |element, *|
60
+ return_value = collection.one? do |element, *|
59
61
  Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
60
62
  end
63
+ Bool.new(return_value)
61
64
  end
62
65
 
63
66
  def filter(collection, identifier, predicate, context:)
data/lib/cel/program.rb CHANGED
@@ -57,20 +57,22 @@ module Cel
57
57
  ev_operand
58
58
  end
59
59
 
60
- if values.size == 1 &&
60
+ if operation.unary? &&
61
61
  op != "!" # https://bugs.ruby-lang.org/issues/18246
62
62
  # unary operations
63
- values.first.__send__(:"#{op}@")
63
+ Literal.to_cel_type(values.first.__send__(:"#{op}@"))
64
64
  elsif op == "&&"
65
- Bool.new(values.all? { |x| true == x }) # rubocop:disable Style/YodaCondition
65
+ Bool.new(values.all? { |x| true == x.value }) # rubocop:disable Style/YodaCondition
66
66
  elsif op == "||"
67
- Bool.new(values.any? { |x| true == x }) # rubocop:disable Style/YodaCondition
67
+ Bool.new(values.any? { |x| true == x.value }) # rubocop:disable Style/YodaCondition
68
68
  elsif op == "in"
69
69
  element, collection = values
70
70
  Bool.new(collection.include?(element))
71
71
  else
72
72
  op_value, *values = values
73
- op_value.public_send(op, *values)
73
+ val = op_value.public_send(op, *values)
74
+
75
+ Literal.to_cel_type(val)
74
76
  end
75
77
  end
76
78
 
@@ -93,7 +95,7 @@ module Cel
93
95
  when String
94
96
  raise EvaluateError, "#{invoke} is not supported" unless String.method_defined?(func, false)
95
97
 
96
- var.public_send(func, *args)
98
+ var.public_send(func, *args.map(&method(:call)))
97
99
  when Message
98
100
  # If e evaluates to a message and f is not declared in this message, the
99
101
  # runtime error no_such_field is raised.
@@ -120,7 +122,7 @@ module Cel
120
122
  end
121
123
 
122
124
  def evaluate_condition(condition)
123
- call(condition.if) ? call(condition.then) : call(condition.else)
125
+ call(condition.if).value ? call(condition.then) : call(condition.else)
124
126
  end
125
127
 
126
128
  def evaluate_standard_func(funcall)
@@ -134,8 +136,10 @@ module Cel
134
136
 
135
137
  val.class
136
138
  # MACROS
137
- when :has, :size
139
+ when :has
138
140
  Macro.__send__(func, *args)
141
+ when :size
142
+ Cel::Number.new(:int, Macro.__send__(func, *args))
139
143
  when :matches
140
144
  Macro.__send__(func, *args.map(&method(:call)))
141
145
  when :int, :uint, :string, :double, :bytes, :duration, :timestamp
@@ -153,7 +157,7 @@ module Cel
153
157
  def evaluate_custom_func(func, funcall)
154
158
  args = funcall.args
155
159
 
156
- func.call(*args.map(&method(:call)))
160
+ func.call(*args.map(&method(:call)).map(&:to_ruby_type))
157
161
  end
158
162
  end
159
163
  end
data/lib/cel/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cel
4
4
  module Ruby
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-17 00:00:00.000000000 Z
11
+ date: 2023-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -88,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  requirements: []
91
- rubygems_version: 3.3.7
91
+ rubygems_version: 3.4.10
92
92
  signing_key:
93
93
  specification_version: 4
94
94
  summary: Pure Ruby implementation of Google Common Expression Language, https://opensource.google/projects/cel.