cel 0.1.0

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