cel 0.1.2 → 0.2.1

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: d81d1083943f4f6d5bc415963b01c2f9be140074ae63493601b1613422fe29dd
4
- data.tar.gz: 4ea24204656323c4bc8313293e64186973f28f84c4878b94804c5cb32b0cc047
3
+ metadata.gz: 54879dbe064eebebe6597acd098c6b10b8376f7b2333d2f822ca02c24a0815bb
4
+ data.tar.gz: 95e2fed808711b64aedc8ed6aff2f8bc8eb9e5f9562907c0d794df186a35f035
5
5
  SHA512:
6
- metadata.gz: 22d25e42ca7b497e2dccbaf239a649a1a66af83001d9580faabf511cc71d8db76eca1d9ededd0fbfb3ba9bde25834750809022983364b6591bf8da1c99c6673c
7
- data.tar.gz: 4526e92a70b428e512654fccc94adbe9dd4a0a32534aa1792b5633a72e55cc95e67c4ef99b5ab5b272cf822e7a7175ef28e73ada9f2e61ca5613699d715a78ca
6
+ metadata.gz: 59349c9633015f0a63c297573e30e33d459de63f1af8ff032f808b045c069716c19142e2208f653864bd4a2363009dd7cb54c8feed3160e6c792c260853cc79f
7
+ data.tar.gz: 2070807c5f7a01217117d96373372e111697727069b915a665acfc6fd2a58c6ff4adfb4fd747522585d7b03733cfb77ee2b7b726b6c36edb3f6d828a93ffed10
data/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.1] - 2023-07-26
4
+
5
+
6
+ ### Improvements
7
+
8
+ Collection type declarations are now better supported, i.e. declaring "an array of strings" is now possible:
9
+
10
+ ```ruby
11
+ Cel::Types[:list, :string] # array of strings
12
+ Cel::Environment.new(
13
+ names: Cel::Types[:list, :string],
14
+ owned_by: Cel::Types[:map, :string] # hash map of string keys
15
+ )
16
+ ```
17
+
18
+ ### Bugfixes
19
+
20
+ Collections passed as variables of an expression are now correctly cast to Cel types:
21
+
22
+ ```ruby
23
+ env.evaluate("a", { a: [1, 2, 3] }) #=> now returns a Cel::List
24
+ ```
25
+
26
+ Conversely, custom functions now expose ruby types in the blocks, instead of its Cel counterparts:
27
+
28
+ ```ruby
29
+ func = Cel::Function(:list, :list, return_type: :list) do |a, b|
30
+ # a is now a ruby array, b as well
31
+ a & b
32
+ # function returns a ruby array, will be converted to a Cel::List for use in the expression
33
+ end
34
+ ```
35
+
36
+ ## [0.2.0] - 2023-01-17
37
+
38
+ ### Features
39
+
40
+ #### Timestamp/Duration types
41
+
42
+ Implementing `Cel::Timestamp` and `Cel::Duration` CEL types, along with support for functions for those types (such as `getDate`, `getDayOfYear`).
43
+
44
+ ```ruby
45
+ Cel::Environment.new.evaluate("timestamp('2022-12-25T00:00:00Z').getDate()") #=> 25
46
+ Cel::Environment.new.evaluate("duration('3600s10ms').getHours()") #=> 1
47
+ ```
48
+
49
+ #### Protobuf-to-CEL conversion
50
+
51
+ Support for auto-conversion of certain protobuf messages in the `google.protobf` package to CEL types.
52
+
53
+ https://github.com/google/cel-spec/blob/master/doc/langdef.md#dynamic-values
54
+
55
+ ```ruby
56
+ Cel::Environment.new.evaluate("google.protobuf.BoolValue{value: true}") #=> true
57
+ Cel::Environment.new.evaluate("google.protobuf.Value{string_value: 'bla'}") #=> "bla"
58
+ ```
59
+
60
+ #### Custom Functions
61
+
62
+ `cel` supports the definition of custom functions in the environment, to be used in expressions:
63
+
64
+ ```ruby
65
+ Cel::Environment.new(foo: Cel::Function(:int, :int, return_type: :int) { |a, b| a + b }).evaluate("foo(2, 2)") #=> 4
66
+ ```
67
+
68
+ ### Expression encoding/decoding
69
+
70
+ Expressions can now be encoded and decoded, to improve storage / reparsing to and from JSON, for example.
71
+
72
+ ```ruby
73
+ enc = Cel::Environment.new.encode("1 == 2") #=> ["op", ...
74
+ store_to_db(JSON.dump(enc))
75
+
76
+ # then, somewhere else
77
+ env = Cel::Environment.new
78
+ ast = env.decode(JSON.parse(read_from_db))
79
+ env.evaluate(ast) #=> 3
80
+ ```
81
+
82
+ **NOTE**: This feature is only available in ruby 3.1 .
83
+
84
+ ### Bugfixes
85
+
86
+ * fixed parser bug disallowing identifiers composed "true" or "false" (such as "true_name").
87
+
3
88
  ## [0.1.2] - 2022-11-10
4
89
 
5
90
  point release to update links in rubygems.
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,12 +67,26 @@ 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.
72
87
 
73
88
  ```ruby
74
- # gem "google-protobuf" in your Gemfile
89
+ require "google/protobuf"
75
90
  require "cel"
76
91
 
77
92
  env = Cel::Environment.new
@@ -79,6 +94,18 @@ env = Cel::Environment.new
79
94
  env.evaluate("google.protobuf.Duration{seconds: 123}.seconds == 123") #=> true
80
95
  ```
81
96
 
97
+ ### Custom functions
98
+
99
+ `cel-ruby` allows you to define custom functions to be used insde CEL expressions. While we **strongly** recommend usage of `Cel::Function` for defining them (due to the ability of them being used for checking), the only requirement is that the function object responds to `.call`:
100
+
101
+ ```ruby
102
+ env = environment(foo: Cel::Function(:int, :int, return_type: :int) { |a, b| a + b})
103
+ env.evaluate("foo(2, 2)") #=> 4
104
+
105
+ # this is also possible, just not as type-safe
106
+ env2 = environment(foo: -> (a, b) { a + b})
107
+ env2.evaluate("foo(2, 2)") #=> 4
108
+ ```
82
109
 
83
110
  ## Supported Rubies
84
111
 
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "google/protobuf/struct_pb"
4
+ require "google/protobuf/wrappers_pb"
5
+ require "google/protobuf/any_pb"
6
+ require "google/protobuf/well_known_types"
7
+
8
+ module Cel
9
+ module Protobuf
10
+ module_function
11
+
12
+ def convert_from_protobuf(msg)
13
+ case msg
14
+ when Google::Protobuf::Any
15
+
16
+ any.unpack(type_msg).to_ruby
17
+ when Google::Protobuf::ListValue
18
+ msg.to_a
19
+ when Google::Protobuf::Struct
20
+ msg.to_h
21
+ when Google::Protobuf::Value
22
+ msg.to_ruby
23
+ when Google::Protobuf::BoolValue,
24
+ Google::Protobuf::BytesValue,
25
+ Google::Protobuf::DoubleValue,
26
+ Google::Protobuf::FloatValue,
27
+ Google::Protobuf::Int32Value,
28
+ Google::Protobuf::Int64Value,
29
+ Google::Protobuf::UInt32Value,
30
+ Google::Protobuf::UInt64Value,
31
+ Google::Protobuf::NullValue,
32
+ Google::Protobuf::StringValue
33
+ msg.value
34
+ when Google::Protobuf::Timestamp
35
+ msg.to_time
36
+ when Google::Protobuf::Duration
37
+ Cel::Duration.new(seconds: msg.seconds, nanos: msg.nanos)
38
+ else
39
+ raise Error, "#{msg.class}: protobuf to cel unsupported"
40
+ end
41
+ end
42
+
43
+ def convert_from_type(type, value)
44
+ case type
45
+ when "Any", "google.protobuf.Any"
46
+ type_url = value[Identifier.new("type_url")].value
47
+ _, type_msg = type_url.split("/", 2)
48
+ type_msg = const_get(type_msg.split(".").map(&:capitalize).join("::"))
49
+ encoded_msg = value[Identifier.new("value")].value.pack("C*")
50
+ any = Google::Protobuf::Any.new(type_url: type_url, value: encoded_msg)
51
+ value = Literal.to_cel_type(any.unpack(type_msg).to_ruby)
52
+ when "ListValue", "google.protobuf.ListValue"
53
+ value = value.nil? ? List.new([]) : value[Identifier.new("values")]
54
+ when "Struct", "google.protobuf.Struct"
55
+ value = value.nil? ? Map.new({}) : value[Identifier.new("fields")]
56
+ when "Value", "google.protobuf.Value"
57
+ return Null.new if value.nil?
58
+
59
+ key = value.keys.first
60
+
61
+ value = value.fetch(key, value)
62
+
63
+ value = Number.new(:double, value) if key == "number_value"
64
+
65
+ value
66
+ when "BoolValue", "google.protobuf.BoolValue"
67
+ value = value.nil? ? Bool.new(false) : value[Identifier.new("value")]
68
+ when "BytesValue", "google.protobuf.BytesValue"
69
+ value = value[Identifier.new("value")]
70
+ when "DoubleValue", "google.protobuf.DoubleValue",
71
+ "FloatValue", "google.protobuf.FloatValue"
72
+ value = value.nil? ? Number.new(:double, 0.0) : value[Identifier.new("value")]
73
+ value.type = TYPES[:double]
74
+ when "Int32Value", "google.protobuf.Int32Value",
75
+ "Int64Value", "google.protobuf.Int64Value"
76
+ value = value.nil? ? Number.new(:int, 0) : value[Identifier.new("value")]
77
+ when "Uint32Value", "google.protobuf.UInt32Value",
78
+ "Uint64Value", "google.protobuf.UInt64Value"
79
+ value = value.nil? ? Number.new(:uint, 0) : value[Identifier.new("value")]
80
+ when "NullValue", "google.protobuf.NullValue"
81
+ value = Null.new
82
+ when "StringValue", "google.protobuf.StringValue"
83
+ value = value.nil? ? String.new(+"") : value[Identifier.new("value")]
84
+ when "Timestamp", "google.protobuf.Timestamp"
85
+ seconds = value.fetch(Identifier.new("seconds"), 0)
86
+ nanos = value.fetch(Identifier.new("nanos"), 0)
87
+ value = Timestamp.new(Time.at(seconds, nanos, :nanosecond))
88
+ when "Duration", "google.protobuf.Duration"
89
+ seconds = value.fetch(Identifier.new("seconds"), 0)
90
+ nanos = value.fetch(Identifier.new("nanos"), 0)
91
+ value = Duration.new(seconds: seconds, nanos: nanos)
92
+ end
93
+ value
94
+ end
95
+
96
+ def try_invoke_from(var, func, args)
97
+ case var
98
+ when "Any", "google.protobuf.Any",
99
+ "ListValue", "google.protobuf.ListValue",
100
+ "Struct", "google.protobuf.Struct",
101
+ "Value", "google.protobuf.Value",
102
+ "BoolValue", "google.protobuf.BoolValue",
103
+ "BytesValue", "google.protobuf.BytesValue",
104
+ "DoubleValue", "google.protobuf.DoubleValue",
105
+ "FloatValue", "google.protobuf.FloatValue",
106
+ "Int32Value", "google.protobuf.Int32Value",
107
+ "Int64Value", "google.protobuf.Int64Value",
108
+ "Uint32Value", "google.protobuf.Uint32Value",
109
+ "Uint64Value", "google.protobuf.Uint64Value",
110
+ "NullValue", "google.protobuf.NullValue",
111
+ "StringValue", "google.protobuf.StringValue",
112
+ "Timestamp", "google.protobuf.Timestamp",
113
+ "Duration", "google.protobuf.Duration"
114
+ protoclass = var.split(".").last
115
+ protoclass = Google::Protobuf.const_get(protoclass)
116
+
117
+ value = if args.nil? && protoclass.constants.include?(func.to_sym)
118
+ protoclass.const_get(func)
119
+ else
120
+ protoclass.__send__(func, *args)
121
+ end
122
+
123
+ Literal.to_cel_type(value)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
3
4
  require "delegate"
5
+ require_relative "elements/protobuf"
4
6
 
5
7
  module Cel
6
8
  LOGICAL_OPERATORS = %w[< <= >= > == != in].freeze
@@ -20,11 +22,22 @@ module Cel
20
22
  def ==(other)
21
23
  super || other.to_s == @id.to_s
22
24
  end
25
+
26
+ def to_s
27
+ @id.to_s
28
+ end
23
29
  end
24
30
 
25
31
  class Message < SimpleDelegator
26
32
  attr_reader :type, :struct
27
33
 
34
+ def self.new(type, struct)
35
+ value = convert_from_type(type, struct)
36
+ return value if value.is_a?(Null) || value != struct
37
+
38
+ super
39
+ end
40
+
28
41
  def initialize(type, struct)
29
42
  @struct = Struct.new(*struct.keys.map(&:to_sym)).new(*struct.values)
30
43
  @type = type.is_a?(Type) ? type : MapType.new(struct.to_h do |k, v|
@@ -36,11 +49,32 @@ module Cel
36
49
  def field?(key)
37
50
  !@type.get(key).nil?
38
51
  end
52
+
53
+ def self.convert_from_type(type, value)
54
+ case type
55
+ when Invoke, Identifier
56
+ spread_type = type.to_s
57
+ Protobuf.convert_from_type(spread_type, value)
58
+ when Type
59
+ [type, value]
60
+ else
61
+ [
62
+ MapType.new(struct.to_h do |k, v|
63
+ [Literal.to_cel_type(k), Literal.to_cel_type(v)]
64
+ end),
65
+ Struct.new(*struct.keys.map(&:to_sym)).new(*struct.values),
66
+ ]
67
+ end
68
+ end
39
69
  end
40
70
 
41
71
  class Invoke
42
72
  attr_reader :var, :func, :args
43
73
 
74
+ def self.new(func:, var: nil, args: nil)
75
+ Protobuf.try_invoke_from(var, func, args) || super
76
+ end
77
+
44
78
  def initialize(func:, var: nil, args: nil)
45
79
  @var = var
46
80
  @func = func.to_sym
@@ -48,10 +82,14 @@ module Cel
48
82
  end
49
83
 
50
84
  def ==(other)
51
- super || (
52
- other.respond_to?(:to_ary) &&
85
+ case other
86
+ when Invoke
87
+ @var == other.var && @func == other.func && @args == other.args
88
+ when Array
53
89
  [@var, @func, @args].compact == other
54
- )
90
+ else
91
+ super
92
+ end
55
93
  end
56
94
 
57
95
  def to_s
@@ -67,6 +105,33 @@ module Cel
67
105
  end
68
106
  end
69
107
 
108
+ class Function
109
+ attr_reader :types, :type
110
+
111
+ def initialize(*types, return_type: nil, &func)
112
+ unless func.nil?
113
+ types = Array.new(func.arity) { TYPES[:any] } if types.empty?
114
+ raise(Error, "number of arg types does not match number of yielded args") unless types.size == func.arity
115
+ end
116
+ @types = types.map { |typ| typ.is_a?(Type) ? typ : TYPES[typ] }
117
+ @type = if return_type.nil?
118
+ TYPES[:any]
119
+ else
120
+ return_type.is_a?(Type) ? return_type : TYPES[return_type]
121
+ end
122
+ @func = func
123
+ end
124
+
125
+ def call(*args)
126
+ Literal.to_cel_type(@func.call(*args))
127
+ end
128
+ end
129
+
130
+ mod = self
131
+ mod.define_singleton_method(:Function) do |*args, **kwargs, &blk|
132
+ mod::Function.new(*args, **kwargs, &blk)
133
+ end
134
+
70
135
  class Literal < SimpleDelegator
71
136
  attr_reader :type, :value
72
137
 
@@ -82,6 +147,8 @@ module Cel
82
147
  end
83
148
 
84
149
  def self.to_cel_type(val)
150
+ val = Protobuf.convert_from_protobuf(val) if val.is_a?(Google::Protobuf::MessageExts)
151
+
85
152
  case val
86
153
  when Literal, Identifier
87
154
  val
@@ -102,11 +169,17 @@ module Cel
102
169
  Bool.new(val)
103
170
  when nil
104
171
  Null.new
172
+ when Time
173
+ Timestamp.new(val)
105
174
  else
106
175
  raise BindingError, "can't convert #{val} to CEL type"
107
176
  end
108
177
  end
109
178
 
179
+ def to_ruby_type
180
+ @value
181
+ end
182
+
110
183
  private
111
184
 
112
185
  def check; end
@@ -227,6 +300,10 @@ module Cel
227
300
  def to_ary
228
301
  [self]
229
302
  end
303
+
304
+ def to_ruby_type
305
+ value.map(&:to_ruby_type)
306
+ end
230
307
  end
231
308
 
232
309
  class Map < Literal
@@ -249,6 +326,10 @@ module Cel
249
326
  [self]
250
327
  end
251
328
 
329
+ def to_ruby_type
330
+ value.to_h { |*args| args.map(&:to_ruby_type) }
331
+ end
332
+
252
333
  def respond_to_missing?(meth, *args)
253
334
  super || (@value && @value.keys.any? { |k| k.to_s == meth.to_s })
254
335
  end
@@ -274,12 +355,178 @@ module Cel
274
355
  end
275
356
  end
276
357
 
358
+ class Timestamp < Literal
359
+ def initialize(value)
360
+ value = case value
361
+ when String then Time.parse(value)
362
+ when Numeric then Time.at(value)
363
+ else value
364
+ end
365
+ super(:timestamp, value)
366
+ end
367
+
368
+ def +(other)
369
+ Timestamp.new(@value + other.to_f)
370
+ end
371
+
372
+ def -(other)
373
+ case other
374
+ when Timestamp
375
+ Duration.new(@value - other.value)
376
+ when Duration
377
+ Timestamp.new(@value - other.to_f)
378
+ end
379
+ end
380
+
381
+ LOGICAL_OPERATORS.each do |op|
382
+ class_eval(<<-OUT, __FILE__, __LINE__ + 1)
383
+ def #{op}(other)
384
+ other.is_a?(Cel::Literal) ? Bool.new(super) : super
385
+ end
386
+ OUT
387
+ end
388
+
389
+ # Cel Functions
390
+
391
+ def getDate(tz = nil)
392
+ to_local_time(tz).day
393
+ end
394
+
395
+ def getDayOfMonth(tz = nil)
396
+ getDate(tz) - 1
397
+ end
398
+
399
+ def getDayOfWeek(tz = nil)
400
+ to_local_time(tz).wday
401
+ end
402
+
403
+ def getDayOfYear(tz = nil)
404
+ to_local_time(tz).yday - 1
405
+ end
406
+
407
+ def getMonth(tz = nil)
408
+ to_local_time(tz).month - 1
409
+ end
410
+
411
+ def getFullYear(tz = nil)
412
+ to_local_time(tz).year
413
+ end
414
+
415
+ def getHours(tz = nil)
416
+ to_local_time(tz).hour
417
+ end
418
+
419
+ def getMinutes(tz = nil)
420
+ to_local_time(tz).min
421
+ end
422
+
423
+ def getSeconds(tz = nil)
424
+ to_local_time(tz).sec
425
+ end
426
+
427
+ def getMilliseconds(tz = nil)
428
+ to_local_time(tz).nsec / 1_000_000
429
+ end
430
+
431
+ private
432
+
433
+ def to_local_time(tz = nil)
434
+ time = @value
435
+ if tz
436
+ tz = TZInfo::Timezone.get(tz) unless tz.match?(/\A[+-]\d{2,}:\d{2,}\z/)
437
+ time = time.getlocal(tz)
438
+ end
439
+ time
440
+ end
441
+ end
442
+
443
+ class Duration < Literal
444
+ def initialize(value)
445
+ value = case value
446
+ when String
447
+ init_from_string(value)
448
+ when Hash
449
+ seconds, nanos = value.values_at(:seconds, :nanos)
450
+ seconds ||= 0
451
+ nanos ||= 0
452
+ seconds + (nanos / 1_000_000_000.0)
453
+ else
454
+ value
455
+ end
456
+ super(:duration, value)
457
+ end
458
+
459
+ LOGICAL_OPERATORS.each do |op|
460
+ class_eval(<<-OUT, __FILE__, __LINE__ + 1)
461
+ def #{op}(other)
462
+ case other
463
+ when Cel::Literal
464
+ Bool.new(super)
465
+ when Numeric
466
+ @value == other
467
+
468
+ else
469
+ super
470
+ end
471
+ end
472
+ OUT
473
+ end
474
+
475
+ # Cel Functions
476
+
477
+ def getHours
478
+ (getMinutes / 60).to_i
479
+ end
480
+
481
+ def getMinutes
482
+ (getSeconds / 60).to_i
483
+ end
484
+
485
+ def getSeconds
486
+ @value.divmod(1).first
487
+ end
488
+
489
+ def getMilliseconds
490
+ (@value.divmod(1).last * 1000).round
491
+ end
492
+
493
+ private
494
+
495
+ def init_from_string(value)
496
+ seconds = 0
497
+ nanos = 0
498
+ value.scan(/([0-9]*(?:\.[0-9]*)?)([a-z]+)/) do |duration, units|
499
+ case units
500
+ when "h"
501
+ seconds += Cel.to_numeric(duration) * 60 * 60
502
+ when "m"
503
+ seconds += Cel.to_numeric(duration) * 60
504
+ when "s"
505
+ seconds += Cel.to_numeric(duration)
506
+ when "ms"
507
+ nanos += Cel.to_numeric(duration) * 1000 * 1000
508
+ when "us"
509
+ nanos += Cel.to_numeric(duration) * 1000
510
+ when "ns"
511
+ nanos += Cel.to_numeric(duration)
512
+ else
513
+ raise EvaluateError, "#{units} is unsupported"
514
+ end
515
+ end
516
+ seconds + (nanos / 1_000_000_000.0)
517
+ end
518
+ end
519
+
277
520
  class Group
278
521
  attr_reader :value
279
522
 
280
523
  def initialize(value)
281
524
  @value = value
282
525
  end
526
+
527
+ def ==(other)
528
+ other.is_a?(Group) && @value == other.value
529
+ end
283
530
  end
284
531
 
285
532
  class Operation
@@ -294,10 +541,13 @@ module Cel
294
541
  end
295
542
 
296
543
  def ==(other)
297
- if other.is_a?(Array)
544
+ case other
545
+ when Array
298
546
  other.size == @operands.size + 1 &&
299
547
  other.first == @op &&
300
548
  other.slice(1..-1).zip(@operands).all? { |x1, x2| x1 == x2 }
549
+ when Operation
550
+ @op == other.op && @type == other.type && @operands == other.operands
301
551
  else
302
552
  super
303
553
  end
@@ -318,5 +568,9 @@ module Cel
318
568
  @then = then_
319
569
  @else = else_
320
570
  end
571
+
572
+ def ==(other)
573
+ other.is_a?(Condition) && @if == other.if && @then == other.then && @else == other.else
574
+ end
321
575
  end
322
576
  end