cel 0.1.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.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cel
4
+ class Type
5
+ def initialize(type)
6
+ @type = type
7
+ end
8
+
9
+ def ==(other)
10
+ other == @type || super
11
+ end
12
+
13
+ def to_str
14
+ @type.to_s
15
+ end
16
+
17
+ def type
18
+ TYPES[:type]
19
+ end
20
+
21
+ def cast(value)
22
+ case @type
23
+ when :int
24
+ Number.new(:int, Integer(value))
25
+ when :uint
26
+ Number.new(:uint, Integer(value).abs)
27
+ when :double
28
+ Number.new(:double, Float(value))
29
+ when :string
30
+ String.new(String(value))
31
+ when :bytes
32
+ Bytes.new(value.bytes)
33
+ when :bool
34
+ Bool.new(value)
35
+ when :any
36
+ value
37
+ else
38
+ raise Error, "unsupported cast operation to #{@type}"
39
+ end
40
+ end
41
+ end
42
+
43
+ class ListType < Type
44
+ attr_accessor :element_type
45
+
46
+ def initialize(type_list)
47
+ super(:list)
48
+ @type_list = type_list
49
+ @element_type = @type_list.empty? ? TYPES[:any] : @type_list.sample.type
50
+ end
51
+
52
+ def get(idx)
53
+ @type_list[idx]
54
+ end
55
+ end
56
+
57
+ class MapType < Type
58
+ attr_accessor :element_type
59
+
60
+ def initialize(type_map)
61
+ super(:map)
62
+ @type_map = type_map
63
+ @element_type = @type_map.empty? ? TYPES[:any] : @type_map.keys.sample.type
64
+ end
65
+
66
+ def get(attrib)
67
+ _, value = @type_map.find { |k, _| k == attrib.to_s }
68
+ value
69
+ end
70
+ end
71
+
72
+ # Primitive Cel Types
73
+
74
+ PRIMITIVE_TYPES = %i[int uint double bool string bytes list map null_type type].freeze
75
+ TYPES = PRIMITIVE_TYPES.map { |typ| [typ, Type.new(typ)] }.to_h
76
+ TYPES[:type] = Type.new(:type)
77
+ TYPES[:any] = Type.new(:any)
78
+ end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cel
4
+ class Checker
5
+ def initialize(declarations)
6
+ @declarations = declarations
7
+ end
8
+
9
+ def check(ast)
10
+ case ast
11
+ when Group
12
+ check(ast.value)
13
+ when Invoke
14
+ check_invoke(ast)
15
+ when Operation
16
+ check_operation(ast)
17
+ when Literal
18
+ ast.type
19
+ when Identifier
20
+ check_identifier(ast)
21
+ when Condition
22
+ check_condition(ast)
23
+ end
24
+ end
25
+
26
+ alias_method :call, :check
27
+
28
+ private
29
+
30
+ def merge(declarations)
31
+ Checker.new(@declarations ? @declarations.merge(declarations) : declarations)
32
+ end
33
+
34
+ # TODO: add protobuf timestamp and duration
35
+ LOGICAL_EXPECTED_TYPES = %i[bool int uint double string bytes].freeze
36
+ ADD_EXPECTED_TYPES = %i[int uint double string bytes list].freeze
37
+ SUB_EXPECTED_TYPES = %i[int uint double].freeze
38
+ MULTIDIV_EXPECTED_TYPES = %i[int uint double].freeze
39
+ REMAINDER_EXPECTED_TYPES = %i[int uint].freeze
40
+
41
+ def check_operation(operation)
42
+ type = infer_operation_type(operation)
43
+ operation.type = type
44
+ type
45
+ end
46
+
47
+ BOOLABLE_OPERATORS = %w[&& || == != < <= >= >].freeze
48
+
49
+ def infer_operation_type(operation)
50
+ op = operation.op
51
+
52
+ values = operation.operands.map do |operand|
53
+ ev_operand = call(operand)
54
+
55
+ return TYPES[:any] if ev_operand == :any && !BOOLABLE_OPERATORS.include?(op)
56
+
57
+ ev_operand
58
+ end
59
+
60
+ if values.size == 1
61
+ # unary ops
62
+ type = values.first
63
+ case op
64
+ when "!"
65
+ return type if type == :bool
66
+
67
+ when "-"
68
+ return type if type == :int || type == :double # rubocop:disable Style/MultipleComparison
69
+
70
+ else
71
+ unsupported_type(operation)
72
+ end
73
+ else
74
+
75
+ case op
76
+ when "&&", "||", "==", "!=", "<", "<=", ">=", ">"
77
+ return TYPES[:bool]
78
+ when "in"
79
+ return TYPES[:bool] if find_match_all_types(%i[list map], values.last)
80
+ when "+"
81
+ if (type = find_match_all_types(ADD_EXPECTED_TYPES, values))
82
+ return type
83
+ end
84
+ when "-"
85
+ if (type = find_match_all_types(SUB_EXPECTED_TYPES, values))
86
+ return type
87
+ end
88
+ when "*", "/"
89
+ if (type = find_match_all_types(MULTIDIV_EXPECTED_TYPES, values))
90
+ return type
91
+ end
92
+ when "%"
93
+ if (type = find_match_all_types(REMAINDER_EXPECTED_TYPES, values))
94
+ return type
95
+ end
96
+ else
97
+ unsupported_type(operation)
98
+ end
99
+ end
100
+ unsupported_type(operation)
101
+ end
102
+
103
+ def check_invoke(funcall, var_type = nil)
104
+ var = funcall.var
105
+ func = funcall.func
106
+ args = funcall.args
107
+
108
+ return check_standard_func(funcall) unless var
109
+
110
+ var_type ||= case var
111
+ when Identifier
112
+ check_identifier(var)
113
+ when Invoke
114
+ check_invoke(var)
115
+ else
116
+ var.type
117
+ end
118
+
119
+ case var_type
120
+ when MapType
121
+ # A field selection expression, e.f, can be applied both to messages and
122
+ # to maps. For maps, selection is interpreted as the field being a string key.
123
+ case func
124
+ when :[]
125
+ attribute = var_type.get(args)
126
+ unsupported_operation(funcall) unless attribute
127
+ when :all, :exists, :exists_one
128
+ check_arity(funcall, args, 2)
129
+ identifier, predicate = args
130
+
131
+ unsupported_type(funcall) unless identifier.is_a?(Identifier)
132
+
133
+ element_checker = merge(identifier.to_sym => var_type.element_type)
134
+
135
+ unsupported_type(funcall) if element_checker.check(predicate) != :bool
136
+
137
+ return TYPES[:bool]
138
+ else
139
+ attribute = var_type.get(func)
140
+ unsupported_operation(funcall) unless attribute
141
+ end
142
+
143
+ call(attribute)
144
+ when ListType
145
+ case func
146
+ when :[]
147
+ attribute = var_type.get(args)
148
+ unsupported_operation(funcall) unless attribute
149
+ call(attribute)
150
+ when :all, :exists, :exists_one
151
+ check_arity(funcall, args, 2)
152
+ identifier, predicate = args
153
+
154
+ unsupported_type(funcall) unless identifier.is_a?(Identifier)
155
+
156
+ element_checker = merge(identifier.to_sym => var_type.element_type)
157
+
158
+ unsupported_type(funcall) if element_checker.check(predicate) != :bool
159
+
160
+ TYPES[:bool]
161
+ when :filter
162
+ check_arity(funcall, args, 2)
163
+ identifier, predicate = args
164
+
165
+ unsupported_type(funcall) unless identifier.is_a?(Identifier)
166
+
167
+ element_checker = merge(identifier.to_sym => var_type.element_type)
168
+
169
+ unsupported_type(funcall) if element_checker.check(predicate) != :bool
170
+
171
+ var_type
172
+ when :map
173
+ check_arity(funcall, args, 2)
174
+ identifier, predicate = args
175
+
176
+ unsupported_type(funcall) unless identifier.is_a?(Identifier)
177
+
178
+ element_checker = merge(identifier.to_sym => var_type.element_type)
179
+
180
+ var_type.element_type = element_checker.check(predicate)
181
+ var_type
182
+ else
183
+ unsupported_operation(funcall)
184
+ end
185
+ when TYPES[:string]
186
+ case func
187
+ when :contains, :endsWith, :startsWith
188
+ check_arity(funcall, args, 1)
189
+ return TYPES[:bool] if find_match_all_types(%i[string], call(args.first))
190
+ when :matches # rubocop:disable Lint/DuplicateBranch
191
+ check_arity(funcall, args, 1)
192
+ # TODO: verify if string can be transformed into a regex
193
+ return TYPES[:bool] if find_match_all_types(%i[string], call(args.first))
194
+ else
195
+ unsupported_type(funcall)
196
+ end
197
+ unsupported_operation(funcall)
198
+ else
199
+ TYPES[:any]
200
+ end
201
+ end
202
+
203
+ CAST_ALLOWED_TYPES = {
204
+ int: %i[uint double string], # TODO: enum, timestamp
205
+ uint: %i[int double string],
206
+ string: %i[int uint double bytes], # TODO: timestamp, duration
207
+ double: %i[int uint string],
208
+ bytes: %i[string],
209
+ }.freeze
210
+
211
+ def check_standard_func(funcall)
212
+ func = funcall.func
213
+ args = funcall.args
214
+
215
+ case func
216
+ when :type
217
+ check_arity(func, args, 1)
218
+ return TYPES[:type]
219
+ when :has
220
+ check_arity(func, args, 1)
221
+ unsupported_type(funcall) unless args.first.is_a?(Invoke)
222
+
223
+ return TYPES[:bool]
224
+ when :size
225
+ check_arity(func, args, 1)
226
+ return TYPES[:int] if find_match_all_types(%i[string bytes list map], call(args.first))
227
+ when :int, :uint, :string, :double, :bytes # :duration, :timestamp
228
+ check_arity(func, args, 1)
229
+ allowed_types = CAST_ALLOWED_TYPES[func]
230
+
231
+ return TYPES[func] if find_match_all_types(allowed_types, call(args.first))
232
+ when :matches
233
+ check_arity(func, args, 2)
234
+ return TYPES[:bool] if find_match_all_types(%i[string], args.map { |arg| call(arg) })
235
+ when :dyn
236
+ check_arity(func, args, 1)
237
+ arg_type = call(args.first)
238
+ case arg_type
239
+ when ListType, MapType
240
+ arg_type.element_type = TYPES[:any]
241
+ end
242
+ return arg_type
243
+ else
244
+ unsupported_type(funcall)
245
+ end
246
+
247
+ unsupported_operation(funcall)
248
+ end
249
+
250
+ def check_identifier(identifier)
251
+ return unless identifier.type == :any
252
+
253
+ return TYPES[:type] if Cel::PRIMITIVE_TYPES.include?(identifier.to_sym)
254
+
255
+ id_type = infer_dec_type(identifier.id)
256
+
257
+ return TYPES[:any] unless id_type
258
+
259
+ identifier.type = id_type
260
+
261
+ id_type
262
+ end
263
+
264
+ def check_condition(condition)
265
+ then_type = call(condition.then)
266
+ else_type = call(condition.else)
267
+
268
+ return then_type if then_type == else_type
269
+
270
+ TYPES[:any]
271
+ end
272
+
273
+ def infer_dec_type(id)
274
+ return unless @declarations
275
+
276
+ var_name, *id_call_chain = id.split(".").map(&:to_sym)
277
+
278
+ typ = @declarations[var_name]
279
+
280
+ return unless typ
281
+
282
+ return convert(typ) if id_call_chain.empty?
283
+ end
284
+
285
+ def convert(typ)
286
+ case typ
287
+ when Symbol
288
+ TYPES[typ] or
289
+ raise CheckError, "#{typ} is not aa valid type"
290
+ else
291
+ typ
292
+ end
293
+ end
294
+
295
+ def find_match_all_types(expected, types)
296
+ # at least an expected type must match all values
297
+ type = expected.find do |expected_type|
298
+ case types
299
+ when Array
300
+ types.all? { |typ| typ == expected_type }
301
+ else
302
+ types == expected_type
303
+ end
304
+ end
305
+
306
+ TYPES[type]
307
+ end
308
+
309
+ def check_arity(func, args, arity)
310
+ return if args.size == arity
311
+
312
+ raise CheckError, "`#{func}` invoked with wrong number of arguments (should be #{arity})"
313
+ end
314
+
315
+ def unsupported_type(op)
316
+ raise NoMatchingOverloadError, op
317
+ end
318
+
319
+ def unsupported_operation(op)
320
+ raise CheckError, "unsupported operation (#{op})"
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cel
4
+ class Context
5
+ def initialize(bindings)
6
+ @bindings = bindings.dup
7
+
8
+ return unless @bindings
9
+
10
+ @bindings.each do |k, v|
11
+ @bindings[k] = to_cel_type(v)
12
+ end
13
+ end
14
+
15
+ def lookup(identifier)
16
+ raise EvaluateError, "no value in context for #{identifier}" unless @bindings
17
+
18
+ id = identifier.id
19
+ val = @bindings.dig(*id.split(".").map(&:to_sym))
20
+
21
+ raise EvaluateError, "no value in context for #{identifier}" unless val
22
+
23
+ val
24
+ end
25
+
26
+ def merge(bindings)
27
+ Context.new(@bindings ? @bindings.merge(bindings) : bindings)
28
+ end
29
+
30
+ private
31
+
32
+ def to_cel_type(v)
33
+ Literal.to_cel_type(v)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cel
4
+ class Environment
5
+ def initialize(declarations = nil)
6
+ @declarations = declarations
7
+ @parser = Parser.new
8
+ @checker = Checker.new(@declarations)
9
+ end
10
+
11
+ def compile(expr)
12
+ ast = @parser.parse(expr)
13
+ @checker.check(ast)
14
+ ast
15
+ end
16
+
17
+ def check(expr)
18
+ ast = @parser.parse(expr)
19
+ @checker.check(ast)
20
+ end
21
+
22
+ def program(expr)
23
+ expr = compile(expr) if expr.is_a?(::String)
24
+ Runner.new(expr)
25
+ end
26
+
27
+ def evaluate(expr, bindings = nil)
28
+ context = Context.new(bindings)
29
+ expr = compile(expr) if expr.is_a?(::String)
30
+ Program.new(context).evaluate(expr)
31
+ end
32
+
33
+ private
34
+
35
+ def validate(ast, structs); end
36
+ end
37
+
38
+ class Runner
39
+ def initialize(ast)
40
+ @ast = ast
41
+ end
42
+
43
+ def evaluate(bindings = nil)
44
+ context = Context.new(bindings)
45
+ Program.new(context).evaluate(@ast)
46
+ end
47
+ end
48
+ end
data/lib/cel/errors.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cel
4
+ class Error < StandardError; end
5
+
6
+ class ParseError < Error; end
7
+
8
+ class CheckError < Error; end
9
+
10
+ class EvaluateError < Error; end
11
+
12
+ class NoSuchFieldError < EvaluateError
13
+ attr_reader :code
14
+
15
+ def initialize(var, attrib)
16
+ super("No such field: #{var}.#{attrib}")
17
+ @code = :no_such_field
18
+ end
19
+ end
20
+
21
+ class NoMatchingOverloadError < CheckError
22
+ attr_reader :code
23
+
24
+ def initialize(op)
25
+ super("No matching overload: #{op}")
26
+ @code = :no_matching_overload
27
+ end
28
+ end
29
+
30
+ class BindingError < EvaluateError; end
31
+ end
data/lib/cel/macro.rb ADDED
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cel
4
+ module Macro
5
+ module_function
6
+
7
+ # If e evaluates to a protocol buffers version 2 message and f is a defined field:
8
+ # If f is a repeated field or map field, has(e.f) indicates whether the field is non-empty.
9
+ # If f is a singular or oneof field, has(e.f) indicates whether the field is set.
10
+ # If e evaluates to a protocol buffers version 3 message and f is a defined field:
11
+ # If f is a repeated field or map field, has(e.f) indicates whether the field is non-empty.
12
+ # If f is a oneof or singular message field, has(e.f) indicates whether the field is set.
13
+ # If f is some other singular field, has(e.f) indicates whether the field's value is its default
14
+ # value (zero for numeric fields, false for booleans, empty for strings and bytes).
15
+ def has(invoke)
16
+ var = invoke.var
17
+ func = invoke.func
18
+
19
+ case var
20
+ when Message
21
+ # If e evaluates to a message and f is not a declared field for the message,
22
+ # has(e.f) raises a no_such_field error.
23
+ raise NoSuchFieldError.new(var, func) unless var.field?(func)
24
+
25
+ Bool.new(!var.public_send(func).nil?)
26
+ when Map
27
+ # If e evaluates to a map, then has(e.f) indicates whether the string f
28
+ # is a key in the map (note that f must syntactically be an identifier).
29
+ Bool.new(var.respond_to?(func))
30
+ else
31
+ # In all other cases, has(e.f) evaluates to an error.
32
+ raise EvaluateError, "#{invoke} is not supported"
33
+ end
34
+ end
35
+
36
+ def size(literal)
37
+ literal.size
38
+ end
39
+
40
+ def matches(string, pattern)
41
+ pattern = Regexp.new(pattern)
42
+ Bool.new(pattern.match?(string))
43
+ end
44
+
45
+ def all(collection, identifier, predicate, context:)
46
+ collection.all? do |element, *|
47
+ Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
48
+ end
49
+ end
50
+
51
+ def exists(collection, identifier, predicate, context:)
52
+ collection.any? do |element, *|
53
+ Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
54
+ end
55
+ end
56
+
57
+ def exists_one(collection, identifier, predicate, context:)
58
+ collection.one? do |element, *|
59
+ Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
60
+ end
61
+ end
62
+
63
+ def filter(collection, identifier, predicate, context:)
64
+ collection.select do |element, *|
65
+ Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
66
+ end
67
+ end
68
+
69
+ def map(collection, identifier, predicate, context:)
70
+ collection.map do |element, *|
71
+ Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate)
72
+ end
73
+ end
74
+ end
75
+ end