cel 0.2.0 → 0.2.2

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: 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.