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.
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cel
4
+ module Extensions
5
+ module String
6
+ module_function
7
+
8
+ def __check(funcall, checker:)
9
+ var = funcall.var
10
+ func = funcall.func
11
+ args = funcall.args
12
+
13
+ case func
14
+ when :quote
15
+ checker.check_arity(func, args, 1)
16
+ arg = checker.call(args.first)
17
+ return TYPES[:string] if checker.find_match_all_types(%i[string], arg)
18
+ when :indexOf, :lastIndexOf
19
+ if checker.call(var) == TYPES[:string]
20
+ checker.check_arity(func, args, 2, :>=)
21
+ arg, pos = args
22
+ arg = checker.call(arg)
23
+ if checker.find_match_all_types(%i[string], arg)
24
+ return TYPES[:int] if pos.nil?
25
+
26
+ pos = checker.call(pos)
27
+ return TYPES[:int] if checker.find_match_all_types(%i[int], pos)
28
+
29
+ end
30
+ end
31
+ when :split
32
+ if checker.call(var) == TYPES[:string]
33
+ checker.check_arity(func, args, 2, :>=)
34
+ arg, pos = args
35
+ arg = checker.call(arg)
36
+ if checker.find_match_all_types(%i[string], arg)
37
+ return TYPES[:list, :string] if pos.nil?
38
+
39
+ pos = checker.call(pos)
40
+ return TYPES[:list, :string] if checker.find_match_all_types(%i[int], pos)
41
+
42
+ end
43
+ end
44
+ when :substring
45
+ if checker.call(var) == TYPES[:string]
46
+ checker.check_arity(func, args, 2, :>=)
47
+ arg, pos = args
48
+ arg = checker.call(arg)
49
+ if checker.find_match_all_types(%i[int], arg)
50
+ return TYPES[:string] if pos.nil?
51
+
52
+ pos = checker.call(pos)
53
+ return TYPES[:string] if checker.find_match_all_types(%i[int], pos)
54
+
55
+ end
56
+ end
57
+ when :trim, :lowerAscii, :upperAscii, :reverse
58
+ if checker.call(var) == TYPES[:string]
59
+ checker.check_arity(func, args, 0)
60
+ return TYPES[:string]
61
+ end
62
+ when :format
63
+ if checker.call(var) == TYPES[:string]
64
+ checker.check_arity(func, args, 1)
65
+ arg = checker.call(args.first)
66
+ return TYPES[:string] if checker.find_match_all_types(%i[list], arg)
67
+ end
68
+ when :charAt
69
+ if checker.call(var) == TYPES[:string]
70
+ checker.check_arity(func, args, 1)
71
+ arg = checker.call(args.first)
72
+ return TYPES[:string] if checker.find_match_all_types(%i[int], arg)
73
+ end
74
+ when :replace
75
+ if checker.call(var) == TYPES[:string]
76
+ checker.check_arity(func, args, 2..3, :include?)
77
+ p, r, limit = args.map(&checker.method(:call))
78
+ if limit
79
+ return TYPES[:string] if checker.find_match_all_types(%i[string], [p, r]) &&
80
+ checker.find_match_all_types(%i[int], limit)
81
+ elsif checker.find_match_all_types(%i[string], [p, r])
82
+ return TYPES[:string]
83
+ end
84
+ end
85
+ when :join
86
+ if checker.call(var) == TYPES[:list, :string] || checker.call(var) == TYPES[:list, :any]
87
+ checker.check_arity(func, args, 1, :>=)
88
+ arg = checker.call(args.first)
89
+ return TYPES[:string] unless arg
90
+ return TYPES[:string] if checker.find_match_all_types(%i[string], arg)
91
+
92
+ end
93
+ end
94
+
95
+ checker.unsupported_operation(funcall)
96
+ end
97
+
98
+ def quote(str, program:)
99
+ value = program.call(str).value
100
+
101
+ # this should use String#dump, but this also replaces printable UTF8 characters
102
+
103
+ # replace quotes
104
+ value = value.gsub(/[\\'"\t\r\a\b\v\f\n]/) { |m| m.dump[1..-2] }
105
+
106
+ # replace invalid utf-8 chars
107
+ value = value.encode(Encoding::UTF_8, invalid: :replace)
108
+
109
+ # adding leading quotes if not quoted already
110
+ value = "\"#{value}\"" unless value.start_with?("\"") && value.end_with?("\"")
111
+
112
+ Cel::String.new(value)
113
+ end
114
+ end
115
+ end
116
+
117
+ class String
118
+ define_cel_method(:charAt) do |idx|
119
+ raise EvaluateError, "index out of range: #{idx}" unless idx.between?(0, @value.size)
120
+
121
+ String.new(@value[idx] || "")
122
+ end
123
+
124
+ define_cel_method(:indexOf) do |substr, *args|
125
+ idx, = args
126
+
127
+ raise EvaluateError, "index out of range: #{idx}" if idx && !idx.between?(0, @value.size)
128
+
129
+ Number.new(:int, @value.index(substr, *args) || -1)
130
+ end
131
+
132
+ define_cel_method(:lastIndexOf) do |substr, *args|
133
+ idx, = args
134
+
135
+ raise EvaluateError, "index out of range: #{idx}" if idx && !idx.between?(0, @value.size)
136
+
137
+ Number.new(:int, @value.rindex(substr, *args) || -1)
138
+ end
139
+
140
+ define_cel_method(:lowerAscii) do
141
+ String.new(@value.downcase(:ascii))
142
+ end
143
+
144
+ define_cel_method(:upperAscii) do
145
+ String.new(@value.upcase(:ascii))
146
+ end
147
+
148
+ UNICODE_WHITESPACE = /[ \t\r\n\u000C\u000D\u0020\u0085\u00A0\u1680\u2000-\u200A\u2028-\u2029\u202F\u205F\u3000]+/
149
+
150
+ define_cel_method(:trim) do
151
+ # why this? because String#split only takes null, horizontal tab, line feed, vertical tab, form feed,
152
+ # carriage return, space into account. We need the unicode definition of whitespace.
153
+ value = @value.gsub(/\A#{UNICODE_WHITESPACE}/o, "")
154
+ .gsub(/#{UNICODE_WHITESPACE}\z/o, "")
155
+
156
+ String.new(value.strip)
157
+ end
158
+
159
+ define_cel_method(:reverse) do
160
+ String.new(@value.reverse)
161
+ end
162
+
163
+ define_cel_method(:split) do |pattern, *args|
164
+ raise NoOverloadError unless pattern.is_a?(String) && args.size.between?(0, 1)
165
+
166
+ limit, = args
167
+
168
+ raise NoOverloadError if limit && !limit.is_a?(Number)
169
+
170
+ if limit && limit.zero?
171
+ List.new([])
172
+ else
173
+ List.new(@value.split(pattern, *args))
174
+ end
175
+ end
176
+
177
+ define_cel_method(:substring) do |*args|
178
+ raise NoOverloadError unless args.size.between?(1, 2)
179
+
180
+ start_idx, end_idx = *args
181
+ end_idx ||= @value.size
182
+
183
+ raise EvaluateError, "index out of range: #{start_idx}" unless start_idx.between?(0, @value.size)
184
+
185
+ raise EvaluateError, "index out of range: #{end_idx}" if end_idx >= @value.size + 1
186
+
187
+ raise EvaluateError, "end index can't be higher than start index" if end_idx < start_idx
188
+
189
+ String.new(@value[start_idx..(end_idx - 1)])
190
+ end
191
+
192
+ define_cel_method(:replace) do |pattern, replacement, *args|
193
+ raise NoOverloadError unless args.size.between?(0, 1)
194
+
195
+ limit, = args
196
+
197
+ raise NoOverloadError unless pattern.is_a?(String) && replacement.is_a?(String)
198
+
199
+ pattern = pattern.value
200
+ replacement = replacement.value
201
+
202
+ value = if limit
203
+ raise NoOverloadError unless limit.is_a?(Number)
204
+
205
+ @value.gsub(pattern) do |m|
206
+ if limit.zero?
207
+ m
208
+ else
209
+ limit -= 1
210
+ replacement
211
+ end
212
+ end
213
+ else
214
+ @value.gsub(pattern, replacement)
215
+ end
216
+
217
+ String.new(value)
218
+ end
219
+
220
+ define_cel_method(:format) do |mappings|
221
+ String.new(format(@value, *mappings))
222
+ end
223
+ end
224
+
225
+ class List
226
+ define_cel_method(:join) do |*args|
227
+ String.new(super(*args))
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "extensions/math"
4
+ require_relative "extensions/string"
5
+ require_relative "extensions/bind"
6
+ require_relative "extensions/encoders"
7
+
8
+ module Cel
9
+ EXTENSIONS = {
10
+ math: Extensions::Math,
11
+ strings: Extensions::String,
12
+ base64: Extensions::Encoders::Base64,
13
+ cel: Extensions::Bind,
14
+ }.freeze
15
+ end
data/lib/cel/parser.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  #
2
3
  # DO NOT MODIFY!!!!
3
4
  # This file is automatically generated by Racc 1.8.1
@@ -32,7 +33,7 @@ else
32
33
  }
33
34
  end.freeze
34
35
 
35
- OPERATORS_RE = Regexp.union(*OPERATORS.keys)
36
+ OPERATORS_RE = Regexp.union(*OPERATORS.keys).freeze
36
37
 
37
38
  BACKSLASH = "\\\\" # Must be literally two backslashes for proper interpolation
38
39
  DIGIT = "[0-9]"
@@ -46,24 +47,24 @@ ESC_BYTE_SEQ = "#{BACKSLASH}[xX]#{HEXDIGIT}{2}"
46
47
  ESC_UNI_SEQ = "#{BACKSLASH}u#{HEXDIGIT}{4}|#{BACKSLASH}U#{HEXDIGIT}{8}"
47
48
  ESC_SEQ = "#{ESC_CHAR_SEQ}|#{ESC_BYTE_SEQ}|#{ESC_UNI_SEQ}|#{ESC_OCT_SEQ}"
48
49
 
49
- WHITESPACE_REGEX = /[ \t\r\n\u000C]+/
50
- COMMENT_REGEX = %r{//[^\n]*}
50
+ WHITESPACE_REGEX = /[ \t\r\n\u000C]+/.freeze
51
+ COMMENT_REGEX = %r{//[^\n]*}.freeze
51
52
 
52
53
  NUM_FLOAT_REGEX = Regexp.union(
53
54
  /#{DIGIT}+\.#{DIGIT}+#{EXPONENT}?/,
54
55
  /#{DIGIT}+#{EXPONENT}/,
55
56
  /\.#{DIGIT}+#{EXPONENT}?/
56
- )
57
+ ).freeze
57
58
 
58
59
  NUM_INT_REGEX = Regexp.union(
59
60
  /0x(?<hex>#{HEXDIGIT}+)/,
60
61
  /(?<dec>#{DIGIT}+)/
61
- )
62
+ ).freeze
62
63
 
63
64
  NUM_UINT_REGEX = Regexp.union(
64
65
  /0x(?<hex>#{HEXDIGIT}+)[uU]/,
65
66
  /(?<dec>#{DIGIT}+)[uU]/
66
- )
67
+ ).freeze
67
68
 
68
69
  STRING_REGEX = Regexp.union(
69
70
  /"""(?<str>(?:#{ESC_SEQ}|[^\\])*)"""/,
@@ -74,20 +75,20 @@ STRING_REGEX = Regexp.union(
74
75
  /#{RAW}'''(?<str>.*?)'''/m,
75
76
  /#{RAW}"(?<str>[^"\n\r]*)"/,
76
77
  /#{RAW}'(?<str>[^'\n\r]*)'/,
77
- )
78
+ ).freeze
78
79
 
79
- QUOTED_MAP_FIELD_REGEX = /`/
80
+ QUOTED_MAP_FIELD_REGEX = /`/.freeze
80
81
 
81
- BYTES_REGEX = /[bB]#{STRING_REGEX}/
82
+ BYTES_REGEX = /[bB]#{STRING_REGEX}/.freeze
82
83
 
83
84
  RESERVED = %W[
84
85
  as break const continue else
85
86
  for function if import let
86
87
  loop package namespace return
87
88
  var void while
88
- ].freeze
89
+ ].each(&:freeze).freeze
89
90
 
90
- IDENTIFIER_REGEX = /[_a-zA-Z][_a-zA-Z0-9]*/
91
+ IDENTIFIER_REGEX = /[_a-zA-Z][_a-zA-Z0-9]*/.freeze
91
92
 
92
93
  def initialize(
93
94
  package = nil,
@@ -404,9 +405,9 @@ def math_operation(op, operands)
404
405
  when "*", "/", "%"
405
406
  depth = lhs.depth + 1
406
407
  end
407
- when "+", "+"
408
+ when "+"
408
409
  case lhs.op
409
- when "+", "+"
410
+ when "+"
410
411
  depth = lhs.depth + 1
411
412
  end
412
413
  end
data/lib/cel/program.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Cel
4
4
  class Program
5
- attr_reader :context
5
+ attr_reader :environment, :context
6
6
 
7
7
  def initialize(context, environment)
8
8
  @context = context
@@ -55,7 +55,11 @@ module Cel
55
55
  end
56
56
 
57
57
  def evaluate_identifier(identifier)
58
- if Cel::PRIMITIVE_TYPES.include?(identifier.to_sym)
58
+ id_sym = identifier.to_sym
59
+
60
+ if Cel::EXTENSIONS.key?(id_sym)
61
+ Cel::EXTENSIONS[id_sym]
62
+ elsif Cel::PRIMITIVE_TYPES.include?(id_sym)
59
63
  TYPES[identifier.to_sym]
60
64
  else
61
65
  val, func = @context.lookup(identifier)
@@ -179,12 +183,12 @@ module Cel
179
183
  t.is_a?(Class) && t < Protobuf.base_class ? 1 : 0
180
184
  end
181
185
 
182
- val = lhs.public_send(op, rhs)
186
+ val = lhs.cel_send(op, rhs)
183
187
 
184
188
  Literal.to_cel_type(val)
185
189
  else
186
190
  op_value, *values = operands.map(&method(:call))
187
- val = op_value.public_send(op, *values)
191
+ val = op_value.cel_send(op, *values)
188
192
 
189
193
  Literal.to_cel_type(val)
190
194
  end
@@ -193,7 +197,8 @@ module Cel
193
197
  def evaluate_invoke(invoke, var = invoke.var)
194
198
  return evaluate_standard_func(invoke) unless var
195
199
 
196
- func = invoke.func
200
+ func = call(invoke.func) || invoke.func
201
+
197
202
  args = invoke.args
198
203
 
199
204
  var =
@@ -225,7 +230,7 @@ module Cel
225
230
  when String
226
231
  raise EvaluateError, "#{invoke} is not supported" unless String.method_defined?(func, false)
227
232
 
228
- var.public_send(func, *args.map(&method(:call)))
233
+ var.cel_send(func, *args.map(&method(:call)))
229
234
  when Map
230
235
  return Macro.__send__(func, var, *args, program: self) if !Module.respond_to?(func) && Macro.respond_to?(func)
231
236
 
@@ -234,9 +239,13 @@ module Cel
234
239
  # to the expression x['foo'] when x evaluates to a map).
235
240
 
236
241
  if args
237
- var.public_send(func, *args)
242
+ var.cel_send(func, *args)
238
243
  else
244
+ # this should only be a data accessor, calling functions is disallowed
245
+ raise EvaluateError, "#{invoke} is not supported" unless var.key?(func)
246
+
239
247
  begin
248
+ # TODO: use explicit lookup method to circumvent the need to method_missing it
240
249
  var.public_send(func)
241
250
  rescue NoMethodError
242
251
  raise NoSuchKeyError.new(var, func)
@@ -250,8 +259,8 @@ module Cel
250
259
  # to the expression x['foo'] when x evaluates to a map).
251
260
 
252
261
  args ?
253
- var.public_send(func, *Array(args).map(&method(:call))) :
254
- var.public_send(func)
262
+ var.cel_send(func, *Array(args).map(&method(:call))) :
263
+ var.cel_send(func)
255
264
  when Timestamp, Duration
256
265
  raise EvaluateError, "#{invoke} is not supported" unless var.class.method_defined?(func, false)
257
266
 
@@ -262,7 +271,7 @@ module Cel
262
271
  # runtime error no_such_field is raised.
263
272
  raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
264
273
 
265
- var.public_send(func, *args)
274
+ var.cel_send(func, *args)
266
275
  when Literal
267
276
  # eliminate ruby API from the lookup
268
277
  raise NoMatchingOverloadError.new(var, func) if Literal.instance_methods.include?(func)
@@ -271,7 +280,7 @@ module Cel
271
280
  # runtime error no_such_field is raised.
272
281
  raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
273
282
 
274
- var.public_send(func)
283
+ var.cel_send(func)
275
284
  when Protobuf.base_class
276
285
  # eliminate protobuf API from the lookup
277
286
  raise NoMatchingOverloadError.new(var, func) if Protobuf.base_class.instance_methods.include?(func)
@@ -283,8 +292,10 @@ module Cel
283
292
  value = Protobuf.lookup(var, func)
284
293
 
285
294
  Literal.to_cel_type(value)
286
- else
287
- if var.is_a?(Module) && var.const_defined?(func)
295
+ when Module
296
+ if var.respond_to?(func) && !Module.respond_to?(func)
297
+ return var.__send__(func, *args, program: self)
298
+ elsif var.const_defined?(func) && !Module.const_defined?(func)
288
299
  # this block assumes a message based call on a protobuf message, either to a
289
300
  # subclass/namespace (Foo.Bar), or an enum (Foo.BAR)
290
301
  # protobuf accessing enum module
@@ -313,8 +324,12 @@ module Cel
313
324
  return Literal.to_cel_type(value)
314
325
  end
315
326
 
327
+ raise EvaluateError, "#{invoke} is not supported"
328
+ else
316
329
  raise EvaluateError, "#{invoke} is not supported"
317
330
  end
331
+ rescue NoCelMethodError
332
+ raise EvaluateError, "#{invoke} is not supported"
318
333
  end
319
334
 
320
335
  def evaluate_condition(condition)
data/lib/cel/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cel
4
4
  module Ruby
5
- VERSION = "0.3.1"
5
+ VERSION = "0.4.1"
6
6
  end
7
7
  end
data/lib/cel.rb CHANGED
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bigdecimal"
4
- require "cel/version"
5
- require "cel/errors"
6
- require "cel/ast/types"
7
- require "cel/ast/elements"
8
- require "cel/parser"
9
- require "cel/macro"
10
- require "cel/context"
11
- require "cel/encoder" if RUBY_VERSION >= "3.1.0"
12
- require "cel/checker"
13
- require "cel/program"
14
- require "cel/environment"
4
+ require_relative "cel/version"
5
+ require_relative "cel/errors"
6
+ require_relative "cel/ast/cel_methods"
7
+ require_relative "cel/ast/types"
8
+ require_relative "cel/ast/elements"
9
+ require_relative "cel/extensions"
10
+ require_relative "cel/parser"
11
+ require_relative "cel/macro"
12
+ require_relative "cel/context"
13
+ require_relative "cel/encoder" if RUBY_VERSION >= "3.1.0"
14
+ require_relative "cel/checker"
15
+ require_relative "cel/program"
16
+ require_relative "cel/environment"
15
17
 
16
18
  begin
17
19
  require "tzinfo"
@@ -19,7 +21,7 @@ rescue LoadError # rubocop:disable Lint/SuppressedException
19
21
  end
20
22
 
21
23
  begin
22
- require "cel/ast/elements/protobuf"
24
+ require_relative "cel/ast/elements/protobuf"
23
25
  rescue LoadError => e
24
26
  puts e
25
27
  module Cel
@@ -252,4 +254,17 @@ module Cel
252
254
  /[[:upper:]]/.match?(typ[0]) ? typ : typ.capitalize
253
255
  end.join("::")
254
256
  end
257
+
258
+ # freeze constants to make cel ractor-friendly
259
+ Number.freeze
260
+ Bool.freeze
261
+ Null.freeze
262
+ String.freeze
263
+ Bytes.freeze
264
+ List.freeze
265
+ Map.freeze
266
+ Timestamp.freeze
267
+ Duration.freeze
268
+ Type.freeze
269
+ TYPES.each_value(&:freeze).freeze
255
270
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: tzinfo-data
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
54
68
  description: Pure Ruby implementation of Google Common Expression Language, https://opensource.google/projects/cel.
55
69
  email:
56
70
  - cardoso_tiago@hotmail.com
@@ -65,14 +79,37 @@ files:
65
79
  - LICENSE.txt
66
80
  - README.md
67
81
  - lib/cel.rb
82
+ - lib/cel/ast/cel_methods.rb
68
83
  - lib/cel/ast/elements.rb
84
+ - lib/cel/ast/elements/bool.rb
85
+ - lib/cel/ast/elements/bytes.rb
86
+ - lib/cel/ast/elements/condition.rb
87
+ - lib/cel/ast/elements/duration.rb
88
+ - lib/cel/ast/elements/function.rb
89
+ - lib/cel/ast/elements/group.rb
90
+ - lib/cel/ast/elements/identifier.rb
91
+ - lib/cel/ast/elements/invoke.rb
92
+ - lib/cel/ast/elements/list.rb
93
+ - lib/cel/ast/elements/literal.rb
94
+ - lib/cel/ast/elements/map.rb
95
+ - lib/cel/ast/elements/message.rb
96
+ - lib/cel/ast/elements/null.rb
97
+ - lib/cel/ast/elements/number.rb
98
+ - lib/cel/ast/elements/operation.rb
69
99
  - lib/cel/ast/elements/protobuf.rb
100
+ - lib/cel/ast/elements/string.rb
101
+ - lib/cel/ast/elements/timestamp.rb
70
102
  - lib/cel/ast/types.rb
71
103
  - lib/cel/checker.rb
72
104
  - lib/cel/context.rb
73
105
  - lib/cel/encoder.rb
74
106
  - lib/cel/environment.rb
75
107
  - lib/cel/errors.rb
108
+ - lib/cel/extensions.rb
109
+ - lib/cel/extensions/bind.rb
110
+ - lib/cel/extensions/encoders.rb
111
+ - lib/cel/extensions/math.rb
112
+ - lib/cel/extensions/string.rb
76
113
  - lib/cel/macro.rb
77
114
  - lib/cel/parser.rb
78
115
  - lib/cel/program.rb
@@ -99,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
136
  - !ruby/object:Gem::Version
100
137
  version: '0'
101
138
  requirements: []
102
- rubygems_version: 3.6.7
139
+ rubygems_version: 3.6.9
103
140
  specification_version: 4
104
141
  summary: Pure Ruby implementation of Google Common Expression Language, https://opensource.google/projects/cel.
105
142
  test_files: []