cel 0.1.2 → 0.2.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.
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