datacaster 3.0.0 → 3.1.0

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: 3b77dec970a054162cb38f106f29f74a505d5c0e3084d46b8c578c2657b63078
4
- data.tar.gz: ea0b92d48730865ed975c5354e304a6f2baaafd3672af83796763ea968aafe73
3
+ metadata.gz: c42f42112214378de25b126640a7bb578532c1db0ac9fbb73914ef3c08db76e2
4
+ data.tar.gz: 0abba877a155d717b0aa2cb605c9214789c839b381492984cb277a1523c5a7a1
5
5
  SHA512:
6
- metadata.gz: 196c625a37388f5debe9ae393f2abcddbedef154e874ce37bc7b5c392d19cfe67921f5aca62321c38fb4d7e7ca8cc825f903e69ebc9e36c62780c9f545116b15
7
- data.tar.gz: f2e4dbe9fa520f51080ad6140fefed7ca87dab429f7af9d720582ee6378be5279607340d444ccdd8125ea0b8f23eff9da7b2c0e5c0055faff92bb10b3235887c
6
+ metadata.gz: 4b07733981e971af666cf9a7688cd35f4a32b80eb2e4a53505620425027f5fdd963c7c63717f6888fe700df4ef6c5eb175a07aac7183cb0df29dd1e8b97fe446
7
+ data.tar.gz: a596b40d1ebff4e8abc455bdde09c01b0f1499a35cee44daca329885277448cbe6f66268d568a091fa78e83e0da7adf9d9cb09df0c5711d4bc3243d3b909a886
data/README.md CHANGED
@@ -15,9 +15,10 @@ It is currently used in production in several projects (mainly as request parame
15
15
  - [Result value](#result-value)
16
16
  - [Hash schema](#hash-schema)
17
17
  - [Logical operators](#logical-operators)
18
- - [*AND operator*:](#and-operator)
19
- - [*OR operator*:](#or-operator)
20
- - [*IF... THEN... ELSE operator*:](#if-then-else-operator)
18
+ - [*AND operator*](#and-operator)
19
+ - [*OR operator*](#or-operator)
20
+ - [*IF... THEN... ELSE operator*](#if-then-else-operator)
21
+ - [*SWITCH... ON... ELSE operator*](#switch-on-else-operator)
21
22
  - [Built-in types](#built-in-types)
22
23
  - [Basic types](#basic-types)
23
24
  - [`array(error_key = nil)`](#arrayerror_key--nil)
@@ -38,6 +39,7 @@ It is currently used in production in several projects (mainly as request parame
38
39
  - [`must_be(klass, error_key = nil)`](#must_beklass-error_key--nil)
39
40
  - [`optional(base)`](#optionalbase)
40
41
  - [`pass`](#pass)
42
+ - [`pass_if(base)`](#pass_ifbase)
41
43
  - [`pick(key)`](#pickkey)
42
44
  - [`remove`](#remove)
43
45
  - [`responds_to(method, error_key = nil)`](#responds_tomethod-error_key--nil)
@@ -266,7 +268,7 @@ And one special: AND with error aggregation (`*`).
266
268
 
267
269
  The former 3 is described immediately below, and the latter is described in the section on hash schemas further in this file.
268
270
 
269
- #### *AND operator*:
271
+ #### *AND operator*
270
272
 
271
273
  ```ruby
272
274
  even_number = Datacaster.schema { integer & check { |x| x.even? } }
@@ -282,7 +284,7 @@ even_number.("test")
282
284
 
283
285
  If left-hand validation of AND operator passes, *its result* (not the original value) is passed to the right-hand validation. See below in this file section on transformations where this might be relevant.
284
286
 
285
- #### *OR operator*:
287
+ #### *OR operator*
286
288
 
287
289
  ```ruby
288
290
  # 'compare' custom type returns ValidResult if and only if validated value == compare's argument
@@ -296,12 +298,57 @@ person_or_entity.(:ngo) # => Datacaster::ErrorResult(["does not equal :entity
296
298
 
297
299
  Notice that OR operator, if left-hand validation fails, passes the original value to the right-hand validation. As you see in the example above resultant error messages are not always convenient (i.e. to show something like "value must be :person or :entity" is preferable to showing somewhat misleading "must be equal to :entity"). See the next section on "IF... THEN... ELSE" for closer to the real world example.
298
300
 
299
- #### *IF... THEN... ELSE operator*:
301
+ #### *IF... THEN... ELSE operator*
300
302
 
301
- Let's suppose we want to validate that incoming hash is either 'person' or 'entity', where
303
+ Let's support we want to run different validations depending on some value, e.g.:
302
304
 
303
- - 'person' is a hash with 3 keys (kind: `:person`, name: string, salary: integer),
304
- - 'entity' is a hash with 4 keys (kind: `:entity`, title: string, form: string, revenue: integer).
305
+ * if 'salary' is more than 100_000, check for the additional key, 'passport'
306
+ * otherwise, ensure 'passport' key is absent
307
+ * in any case, check that 'name' key is present and is a string
308
+
309
+ ```ruby
310
+ applicant =
311
+ Datacaster.schema do
312
+ base = hash_schema(
313
+ name: string,
314
+ salary: integer
315
+ )
316
+
317
+ large_salary = check { |x| x[:salary] > 100_000 }
318
+
319
+ base &
320
+ large_salary.
321
+ then(passport: string).
322
+ else(passport: absent)
323
+ end
324
+
325
+ applicant.(name: 'John', salary: 50_000)
326
+ # => Datacaster::ValidResult({:name=>"John", :salary=>50000})
327
+
328
+ applicant.(name: 'Jane', salary: 101_000, passport: 'AB123CD')
329
+ # => Datacaster::ValidResult({:name=>"Jane", :salary=>101000, :passport=>"AB123CD"})
330
+
331
+ applicant.(name: 'George', salary: 101_000)
332
+ # => Datacaster::ErrorResult({:passport=>["is not a string"])
333
+ ```
334
+
335
+ Formally, with `a.then(b).else(c)`:
336
+
337
+ * if `a` returns `ValidResult`, then `b` is called *with the result of `a`* (not the original value) and whatever `b` returns is returned;
338
+ * otherwise, `c` is called with the original value, and whatever `c` returns is returned.
339
+
340
+ `else`-part is required and could not be omitted.
341
+
342
+ Note: this construct is *not* an equivalent of `a & b | c`.
343
+
344
+ With `a.then(b).else(c)` if `a` and `b` fails, then `b`'s error is returned. With `a & b | c`, instead, `c`'s result would be returned.
345
+
346
+ #### *SWITCH... ON... ELSE operator*
347
+
348
+ Let's suppose we want to validate that incoming hash is either 'person' or 'entity', where:
349
+
350
+ * 'person' is a hash with 3 keys (kind: `:person`, name: string, salary: integer),
351
+ * 'entity' is a hash with 4 keys (kind: `:entity`, title: string, form: string, revenue: integer).
305
352
 
306
353
  ```ruby
307
354
  person_or_entity =
@@ -317,7 +364,25 @@ person_or_entity =
317
364
  # separate entity validator (excluding validation of 'kind' field)
318
365
  entity = hash_schema(title: string, form: string, revenue: integer)
319
366
 
320
- kind_is_valid & hash_schema(kind: compare(:person)).then(person).else(entity)
367
+
368
+ # 1. First option, explicit definition
369
+
370
+ kind_is_valid &
371
+ switch(pick(:kind)).
372
+ on(compare(:person), person).
373
+ on(compare(:entity), entity)
374
+
375
+ # 2. Second option, shortcut definiton
376
+
377
+ kind_is_valid &
378
+ switch(:kind).
379
+ on(:person, person).
380
+ on(:entity, entity)
381
+
382
+ # 3. Third option, using keywords args and Ruby 3.1 value omission in hash literals
383
+
384
+ kind_is_valid &
385
+ switch(:kind, person:, entity:)
321
386
  end
322
387
 
323
388
  person_or_entity.(
@@ -341,22 +406,27 @@ person_or_entity.(
341
406
  # => Datacaster::ErrorResult({:kind=>["is invalid"]})
342
407
  ```
343
408
 
344
- See below documentation on 'check' custom type to know how to provide custom error message instead of 'is invalid'.
409
+ In our opinion the above example shows most laconic way to express underlying 'business-logic' (including elaborate error reporting on all kinds of failures) among all available competitor approaches/gems.
345
410
 
346
- Schema, defined above, behaves in all aspects (shown in the example and in other practical applications which might come to your mind) just as you might expect it to, after reading previous examples and the code above.
411
+ Notice that shortcut definitions are available (illustrated in the example above) for the switch caster:
347
412
 
348
- In our opinion the above example shows most laconic way to express underlying 'business-logic' (including elaborate error reporting on all kinds of failures) among all available competitor approaches/gems.
413
+ * `switch(:key)` is exactly the same as `switch(pick(:key))` (works for a string, a symbol, or an array thereof)
414
+ * `on(:key, ...)` is exactly the same as `on(compare(:key), ...)` (works for a string or a symbol)
415
+ * `switch([caster], on_check => on_caster, ...)` is exactly the same as `switch([caster]).on(on_check, on_caster).on(...)`
349
416
 
350
- Formally, with `a.then(b).else(c)`:
417
+ `switch()` without a `base` argument will pass the incoming value to the `.on(...)` casters.
351
418
 
352
- * if `a` returns `ValidResult`, then `b` is called *with the result of `a`* (not the original value) and whatever `b` returns is returned;
353
- * otherwise, `c` is called with the original value, and whatever `c` returns is returned.
419
+ Formally, with `switch(a).on(on_check, on_caster).else(c)`:
354
420
 
355
- `else`-part is required and could not be omitted.
421
+ * if `a` returns ErrorResult, it is the result of the switch
422
+ * otherwise, all `on_check` casters from the `.on` blocks are called with the result of `a`, until the first one which returns ValidResult is found – corresponding `on_caster` is called with the original value and its result is the result of the switch
423
+ * if all `on_check`-s returned ErrorResult
424
+ * and there is an `.else` block, `c` is called with the original value and its result is the result of the switch
425
+ * if there is no `.else` block, `ErrorResult(['is invalid'])` is returned from the switch
356
426
 
357
- Note: this construct is *not* an equivalent of `a & b | c`.
427
+ I18n keys:
358
428
 
359
- With `a.then(b).else(c)` if `a` and `b` fails, then `b`'s error is returned. With `a & b | c`, instead, `c`'s result would be returned.
429
+ * all `.on` checks resulted in an error and there is no `.else`: `'.switch'`, `'datacaster.errors.switch'`
360
430
 
361
431
  ## Built-in types
362
432
 
@@ -450,10 +520,10 @@ Always returns ValidResult.
450
520
 
451
521
  Returns `default_value` in the following cases:
452
522
 
453
- * if value is `Datacaster.absent` (`on` is disregarded in such case)
454
- * if `on` is set to method name to which the value responds and yields truthy
523
+ * if the value is `Datacaster.absent` (`on` is disregarded in such case)
524
+ * if `on` is set to a method name to which the value responds and yields truthy
455
525
 
456
- Returns initial value otherwise.
526
+ Returns the initial value otherwise.
457
527
 
458
528
  Set `on` to `:nil?`, `:empty?` or similar method names.
459
529
 
@@ -594,6 +664,12 @@ Always returns ValidResult. Doesn't transform the value.
594
664
 
595
665
  Useful to "mark" the value as validated (see section below on hash schemas, where this could be applied).
596
666
 
667
+ #### `pass_if(base)`
668
+
669
+ Returns ValidResult if and only if base returns ValidResult. Returns base's error result otherwise.
670
+
671
+ Doesn't transform the value: if base succeeds returns the original value (not the one that base returned).
672
+
597
673
  #### `pick(key)`
598
674
 
599
675
  Returns ValidResult if and only if the value `#is_a?(Enumerable)`.
@@ -642,15 +718,7 @@ I18n keys: `error_key`, `'.responds_to'`, `'datacaster.errors.responds_to'`. Add
642
718
 
643
719
  #### `transform_to_value(value)`
644
720
 
645
- Always returns ValidResult. The value is transformed to provided argument. Is used to provide default values, e.g.:
646
-
647
- ```ruby
648
- max_concurrent_connections = Datacaster.schema { compare(nil).then(transform_to_value(5)).else(integer) }
649
-
650
- max_concurrent_connections.(9) # => Datacaster::ValidResult(9)
651
- max_concurrent_connections.("9") # => Datacaster::ErrorResult(["is not an integer"])
652
- max_concurrent_connections.(nil) # => Datacaster::ValidResult(5)
653
- ```
721
+ Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). See also [`default`](#defaultdefault_value-on-nil).
654
722
 
655
723
  ### "Web-form" types
656
724
 
@@ -1375,7 +1443,7 @@ en:
1375
1443
  wrong_format: wrong format
1376
1444
  ```
1377
1445
 
1378
- Let's gradually reduce the boilerplate, starting with the most explicit example. Notice that all relative keys (i.e. keys which will be scoped during the execution) starts with `'.'`:
1446
+ Let's gradually reduce the boilerplate, starting with the most explicit example. Notice that all relative keys (i.e. keys which will be scoped during the execution) start with `'.'`:
1379
1447
 
1380
1448
  ```ruby
1381
1449
  schema =
@@ -1436,7 +1504,6 @@ schema =
1436
1504
  Datacaster.schema(i18n_scope: 'user') do
1437
1505
  check { |v| v[:id] == 1 } &
1438
1506
  hash_schema(
1439
- # '.wrong_format' inferred to be '.name.wrong_format'
1440
1507
  name: check { false }
1441
1508
  )
1442
1509
  end
@@ -1470,9 +1537,9 @@ Every caster will automatically provide `value` variable for i18n interpolation.
1470
1537
 
1471
1538
  All keyword arguments of `#i18n_key`, `#i18n_scope` and designed for that sole purpose `#i18n_vars` are provided as interpolation variables on i18n.
1472
1539
 
1473
- It is possible to add i18n variables at the runtime (e.g. inside `check { ... }` block) by calling `i18n_vars!(variable: 'value')` (or `i18n_var!(:variable, 'value')`.
1540
+ It is possible to add i18n variables at the runtime (e.g. inside `check { ... }` block) by calling `i18n_vars!(variable: 'value')` or `i18n_var!(:variable, 'value')`.
1474
1541
 
1475
- Outer calls of `#i18n_key` (`#i18n_scope`, `#i18n_vars`) have presedence before the inner if variable names collide. However, runtime calls of `#i18n_vars!` and `#i18n_var!` overwrites compile-time variables from the next nearest key, scope or vars on collision.
1542
+ Outer calls of `#i18n_key` (`#i18n_scope`, `#i18n_vars`) have presedence before the inner if variable names collide. However, runtime calls of `#i18n_vars!` and `#i18n_var!` overwrite compile-time variables from the next nearest key, scope or vars on collision.
1476
1543
 
1477
1544
  ## Registering custom 'predefined' types
1478
1545
 
@@ -21,4 +21,5 @@ en:
21
21
  to_float: does not look like a float
22
22
  to_integer: does not look like an integer
23
23
  non_empty_string: should be non-empty string
24
+ switch: is invalid
24
25
  try: raised an error
data/datacaster.gemspec CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency 'rake', '~> 12.0'
27
27
  spec.add_development_dependency 'rspec', '~> 3.0'
28
28
  spec.add_development_dependency 'i18n', '~> 1.14'
29
+ spec.add_development_dependency 'dry-monads', '>= 1.3', '< 1.4'
29
30
 
30
- spec.add_runtime_dependency 'dry-monads', '>= 1.3', '< 1.4'
31
31
  spec.add_runtime_dependency 'zeitwerk', '>= 2', '< 3'
32
32
  end
@@ -46,7 +46,7 @@ module Datacaster
46
46
  end
47
47
 
48
48
  def then(other)
49
- ThenNode.new(self, other)
49
+ ThenNode.new(self, DefinitionDSL.expand(other))
50
50
  end
51
51
 
52
52
  def with_context(context)
@@ -18,7 +18,7 @@ module Datacaster
18
18
  end
19
19
 
20
20
  def inspect
21
- "#<Datacaster::Comparator>"
21
+ "#<Datacaster::Comparator(#{@value.inspect})>"
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,11 @@
1
+ module Datacaster
2
+ module ContextNodes
3
+ class PassIf < Datacaster::ContextNode
4
+ def cast(object, runtime:)
5
+ @runtime = create_runtime(runtime)
6
+ result = @base.with_runtime(@runtime).call(object)
7
+ result.valid? ? Datacaster::ValidResult(object) : result
8
+ end
9
+ end
10
+ end
11
+ end
@@ -4,7 +4,6 @@ require 'date'
4
4
  module Datacaster
5
5
  class DefinitionDSL
6
6
  include Datacaster::Predefined
7
- include Dry::Monads[:result]
8
7
 
9
8
  # Translates hashes like {a: <IntegerChecker>} to <HashSchema {a: <IntegerChecker>}>
10
9
  # and arrays like [<IntegerChecker>] to <ArraySchema <IntegerChecker>>
@@ -53,6 +53,18 @@ module Datacaster
53
53
  Validator.new(active_model_validations)
54
54
  end
55
55
 
56
+ def schema(base)
57
+ ContextNodes::StructureCleaner.new(base, strategy: :fail)
58
+ end
59
+
60
+ def choosy_schema(base)
61
+ ContextNodes::StructureCleaner.new(base, strategy: :remove)
62
+ end
63
+
64
+ def partial_schema(base)
65
+ ContextNodes::StructureCleaner.new(base, strategy: :pass)
66
+ end
67
+
56
68
  # 'Meta' types
57
69
 
58
70
  def absent(error_key = nil)
@@ -90,7 +102,25 @@ module Datacaster
90
102
  transform(&:itself)
91
103
  end
92
104
 
93
- def pick(*keys)
105
+ def switch(*base, **on_clauses)
106
+ switch =
107
+ if base.length == 0
108
+ SwitchNode.new
109
+ else
110
+ SwitchNode.new(base)
111
+ end
112
+ on_clauses.reduce(switch) do |result, (k, v)|
113
+ result.on(k, v)
114
+ end
115
+ end
116
+
117
+ def pass_if(base)
118
+ ContextNodes::PassIf.new(base)
119
+ end
120
+
121
+ def pick(*keys, strict: false)
122
+ raise RuntimeError.new("provide keys to pick, e.g. pick(:key)") if keys.empty?
123
+
94
124
  must_be(Enumerable) & transform { |value|
95
125
  result =
96
126
  keys.map do |key|
@@ -209,7 +239,7 @@ module Datacaster
209
239
  elsif ['false', '0', false].include?(x)
210
240
  Datacaster.ValidResult(false)
211
241
  else
212
- Datacaster.ErrorResult(Datacaster::I18nValues::Key.new(error_keys, value: x))
242
+ Datacaster.ErrorResult(I18nValues::Key.new(error_keys, value: x))
213
243
  end
214
244
  end
215
245
  end
@@ -1,9 +1,5 @@
1
- require 'dry/monads'
2
-
3
1
  module Datacaster
4
2
  class Result
5
- include Dry::Monads[:result]
6
-
7
3
  def initialize(valid, value_or_errors)
8
4
  @value_or_errors = value_or_errors
9
5
  if !valid && !@value_or_errors.is_a?(Hash) && !@value_or_errors.is_a?(Array)
@@ -43,7 +39,11 @@ module Datacaster
43
39
  end
44
40
 
45
41
  def to_dry_result
46
- @valid ? Success(@value_or_errors) : Failure(errors)
42
+ if @valid
43
+ Dry::Monads::Result::Success.new(@value_or_errors)
44
+ else
45
+ Dry::Monads::Result::Failure.new(errors)
46
+ end
47
47
  end
48
48
 
49
49
  private
@@ -0,0 +1,72 @@
1
+ module Datacaster
2
+ class SwitchNode < Base
3
+ def initialize(base = nil, on_casters = [], else_caster = nil)
4
+ base = base[0] if base.is_a?(Array) && base.length == 1
5
+
6
+ case base
7
+ when nil
8
+ @base = nil
9
+ when Datacaster::Base
10
+ @base = base
11
+ when String, Symbol, Array
12
+ @base = Datacaster::Predefined.pick(base)
13
+ else
14
+ raise RuntimeError, "provide a Datacaster::Base instance, a hash key, or an array of keys to switch(...) caster", caller
15
+ end
16
+
17
+ @ons = on_casters
18
+ @else = else_caster
19
+ end
20
+
21
+ def on(caster_or_value, clause)
22
+ caster =
23
+ case caster_or_value
24
+ when Datacaster::Base
25
+ caster_or_value
26
+ else
27
+ Datacaster::Predefined.compare(caster_or_value)
28
+ end
29
+
30
+ clause = DefinitionDSL.expand(clause)
31
+
32
+ self.class.new(@base, @ons + [[caster, clause]], @else)
33
+ end
34
+
35
+ def else(else_caster)
36
+ raise ArgumentError, "Datacaster: double else clause is not permitted", caller if @else
37
+ self.class.new(@base, @ons, else_caster)
38
+ end
39
+
40
+ def cast(object, runtime:)
41
+ if @ons.empty?
42
+ raise RuntimeError, "switch caster requires at least one 'on' statement: switch(...).on(condition, cast)", caller
43
+ end
44
+
45
+ if @base.nil?
46
+ switch_result = object
47
+ else
48
+ switch_result = @base.with_runtime(runtime).(object)
49
+ return switch_result unless switch_result.valid?
50
+ switch_result = switch_result.value
51
+ end
52
+
53
+ @ons.each do |check, clause|
54
+ result = check.with_runtime(runtime).(switch_result)
55
+ next unless result.valid?
56
+
57
+ return clause.with_runtime(runtime).(object)
58
+ end
59
+
60
+ # all 'on'-s have failed
61
+ return @else.with_runtime(runtime).(object) if @else
62
+
63
+ Datacaster.ErrorResult(
64
+ I18nValues::Key.new(['.switch', 'datacaster.errors.switch'], value: object)
65
+ )
66
+ end
67
+
68
+ def inspect
69
+ "#<Datacaster::SwitchNode base: #{@base.inspect} on: #{@ons.inspect} else: #{@else.inspect}>"
70
+ end
71
+ end
72
+ end
@@ -9,7 +9,7 @@ module Datacaster
9
9
  def else(else_caster)
10
10
  raise ArgumentError.new("Datacaster: double else clause is not permitted") if @else
11
11
 
12
- self.class.new(@left, @then, else_caster)
12
+ self.class.new(@left, @then, DefinitionDSL.expand(else_caster))
13
13
  end
14
14
 
15
15
  def cast(object, runtime:)
@@ -1,3 +1,3 @@
1
1
  module Datacaster
2
- VERSION = "3.0.0"
2
+ VERSION = "3.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datacaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eugene Zolotarev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-29 00:00:00.000000000 Z
11
+ date: 2023-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -76,7 +76,7 @@ dependencies:
76
76
  - - "<"
77
77
  - !ruby/object:Gem::Version
78
78
  version: '1.4'
79
- type: :runtime
79
+ type: :development
80
80
  prerelease: false
81
81
  version_requirements: !ruby/object:Gem::Requirement
82
82
  requirements:
@@ -140,6 +140,7 @@ files:
140
140
  - lib/datacaster/context_nodes/errors_caster.rb
141
141
  - lib/datacaster/context_nodes/i18n.rb
142
142
  - lib/datacaster/context_nodes/i18n_keys_mapper.rb
143
+ - lib/datacaster/context_nodes/pass_if.rb
143
144
  - lib/datacaster/context_nodes/structure_cleaner.rb
144
145
  - lib/datacaster/context_nodes/user_context.rb
145
146
  - lib/datacaster/definition_dsl.rb
@@ -157,6 +158,7 @@ files:
157
158
  - lib/datacaster/runtimes/structure_cleaner.rb
158
159
  - lib/datacaster/runtimes/user_context.rb
159
160
  - lib/datacaster/substitute_i18n.rb
161
+ - lib/datacaster/switch_node.rb
160
162
  - lib/datacaster/then_node.rb
161
163
  - lib/datacaster/transformer.rb
162
164
  - lib/datacaster/trier.rb