datacaster 2.0.2 → 3.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +604 -287
  3. data/config/locales/en.yml +25 -0
  4. data/datacaster.gemspec +3 -1
  5. data/lib/datacaster/absent.rb +4 -0
  6. data/lib/datacaster/and_node.rb +3 -5
  7. data/lib/datacaster/and_with_error_aggregation_node.rb +5 -6
  8. data/lib/datacaster/array_schema.rb +18 -16
  9. data/lib/datacaster/base.rb +33 -44
  10. data/lib/datacaster/caster.rb +4 -8
  11. data/lib/datacaster/checker.rb +8 -10
  12. data/lib/datacaster/comparator.rb +9 -9
  13. data/lib/datacaster/config.rb +28 -0
  14. data/lib/datacaster/context_node.rb +43 -0
  15. data/lib/datacaster/context_nodes/errors_caster.rb +21 -0
  16. data/lib/datacaster/context_nodes/i18n.rb +20 -0
  17. data/lib/datacaster/context_nodes/i18n_keys_mapper.rb +27 -0
  18. data/lib/datacaster/context_nodes/pass_if.rb +11 -0
  19. data/lib/datacaster/context_nodes/structure_cleaner.rb +103 -0
  20. data/lib/datacaster/context_nodes/user_context.rb +20 -0
  21. data/lib/datacaster/definition_dsl.rb +36 -0
  22. data/lib/datacaster/hash_mapper.rb +13 -16
  23. data/lib/datacaster/hash_schema.rb +14 -15
  24. data/lib/datacaster/i18n_values/base.rb +87 -0
  25. data/lib/datacaster/i18n_values/key.rb +34 -0
  26. data/lib/datacaster/i18n_values/scope.rb +28 -0
  27. data/lib/datacaster/message_keys_merger.rb +8 -15
  28. data/lib/datacaster/or_node.rb +3 -4
  29. data/lib/datacaster/predefined.rb +150 -65
  30. data/lib/datacaster/result.rb +39 -18
  31. data/lib/datacaster/runtimes/base.rb +47 -0
  32. data/lib/datacaster/runtimes/i18n.rb +20 -0
  33. data/lib/datacaster/runtimes/structure_cleaner.rb +47 -0
  34. data/lib/datacaster/runtimes/user_context.rb +39 -0
  35. data/lib/datacaster/substitute_i18n.rb +48 -0
  36. data/lib/datacaster/switch_node.rb +72 -0
  37. data/lib/datacaster/then_node.rb +7 -8
  38. data/lib/datacaster/transformer.rb +4 -8
  39. data/lib/datacaster/trier.rb +9 -11
  40. data/lib/datacaster/validator.rb +8 -9
  41. data/lib/datacaster/version.rb +1 -1
  42. data/lib/datacaster.rb +15 -35
  43. metadata +60 -10
  44. data/lib/datacaster/definition_context.rb +0 -20
  45. data/lib/datacaster/terminator.rb +0 -98
@@ -1,14 +1,12 @@
1
- require 'dry/monads'
2
-
3
1
  module Datacaster
4
2
  class Result
5
- attr_accessor :meta
6
- include Dry::Monads[:result]
7
-
8
- def initialize(valid, value_or_errors, meta: nil)
3
+ def initialize(valid, value_or_errors)
9
4
  @value_or_errors = value_or_errors
5
+ if !valid && !@value_or_errors.is_a?(Hash) && !@value_or_errors.is_a?(Array)
6
+ @value_or_errors = Array(@value_or_errors)
7
+ end
8
+
10
9
  @valid = !!valid
11
- @meta = meta || {}
12
10
  end
13
11
 
14
12
  def valid?
@@ -19,13 +17,19 @@ module Datacaster
19
17
  @valid ? @value_or_errors : nil
20
18
  end
21
19
 
22
- def errors
23
- unless @value_or_errors.is_a?(Hash) || @value_or_errors.is_a?(Array)
24
- @value_or_errors = Array(@value_or_errors)
25
- end
20
+ def value!
21
+ raise "Tried to unwrap value of error result: #{inspect}" unless valid?
22
+ value
23
+ end
24
+
25
+ def raw_errors
26
26
  @valid ? nil : @value_or_errors
27
27
  end
28
28
 
29
+ def errors
30
+ @errors ||= @valid ? nil : resolve_i18n(raw_errors)
31
+ end
32
+
29
33
  def inspect
30
34
  if @valid
31
35
  "#<Datacaster::ValidResult(#{@value_or_errors.inspect})>"
@@ -35,27 +39,44 @@ module Datacaster
35
39
  end
36
40
 
37
41
  def to_dry_result
38
- @valid ? Success(@value_or_errors) : Failure(@value_or_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
+ end
48
+
49
+ private
50
+
51
+ def resolve_i18n(o)
52
+ case o
53
+ when Array
54
+ o.map { |x| resolve_i18n(x) }
55
+ when Hash
56
+ o.transform_values { |x| resolve_i18n(x) }
57
+ when I18nValues::Base
58
+ o.resolve
59
+ else
60
+ o
61
+ end
39
62
  end
40
63
  end
41
64
 
42
- def self.ValidResult(object, meta: nil)
65
+ def self.ValidResult(object)
43
66
  if object.is_a?(Result)
44
67
  raise "Can't create valid result from error #{object.inspect}" unless object.valid?
45
- object.meta = meta if meta
46
68
  object
47
69
  else
48
- Result.new(true, object, meta: meta)
70
+ Result.new(true, object)
49
71
  end
50
72
  end
51
73
 
52
- def self.ErrorResult(object, meta: nil)
74
+ def self.ErrorResult(object)
53
75
  if object.is_a?(Result)
54
76
  raise "Can't create error result from valid #{object.inspect}" if object.valid?
55
- object.meta = meta if meta
56
77
  object
57
78
  else
58
- Result.new(false, object, meta: meta)
79
+ Result.new(false, object)
59
80
  end
60
81
  end
61
82
  end
@@ -0,0 +1,47 @@
1
+ module Datacaster
2
+ module Runtimes
3
+ class Base
4
+ def self.call(r, proc, *args)
5
+ r.instance_exec(*args, &proc)
6
+ end
7
+
8
+ def self.send_to_parent(r, m, *args, &block)
9
+ parent = r.instance_variable_get(:@parent)
10
+ not_found!(m) if parent.nil?
11
+ call(parent, -> { public_send(m, *args, &block) })
12
+ end
13
+
14
+ def self.not_found!(m)
15
+ raise NoMethodError.new("Method #{m.inspect} is not available in current runtime context")
16
+ end
17
+
18
+ def initialize(parent = nil)
19
+ @parent = parent
20
+ end
21
+
22
+ def method_missing(m, *args, &block)
23
+ self.class.send_to_parent(self, m, *args, &block)
24
+ end
25
+
26
+ def respond_to_missing?(m, include_private = false)
27
+ !@parent.nil? && @parent.respond_to?(m, include_private)
28
+ end
29
+
30
+ def inspect
31
+ "#<#{self.class.name} parent: #{@parent.inspect}>"
32
+ end
33
+
34
+ def to_s
35
+ inspect
36
+ end
37
+
38
+ def Success(v)
39
+ Datacaster.ValidResult(v)
40
+ end
41
+
42
+ def Failure(v)
43
+ Datacaster.ErrorResult(v)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,20 @@
1
+ module Datacaster
2
+ module Runtimes
3
+ class I18n < Base
4
+ attr_reader :args
5
+
6
+ def initialize(*)
7
+ super
8
+ @args = {}
9
+ end
10
+
11
+ def i18n_var!(name, value)
12
+ @args[name] = value
13
+ end
14
+
15
+ def i18n_vars!(map)
16
+ @args.merge!(map)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,47 @@
1
+ module Datacaster
2
+ module Runtimes
3
+ class StructureCleaner < Base
4
+ attr_reader :checked_schema
5
+
6
+ def initialize(*)
7
+ super
8
+ @checked_schema = {}
9
+ @should_check_stack = [false]
10
+ @pointer_stack = [@checked_schema]
11
+ end
12
+
13
+ # Array checked schema are the same as hash one, where
14
+ # instead of keys there are array indicies
15
+ def checked_key!(key)
16
+ if @ignore
17
+ return yield if block_given?
18
+ return
19
+ end
20
+
21
+ @pointer_stack.last[key] ||= {}
22
+ @pointer_stack.push(@pointer_stack.last[key])
23
+ @should_check_stack.push(false)
24
+ result = yield if block_given?
25
+ was_checked = @should_check_stack.pop
26
+ @pointer_stack.pop
27
+ @pointer_stack.last[key] = true unless was_checked
28
+ result
29
+ end
30
+
31
+ def will_check!
32
+ @should_check_stack[-1] = true
33
+ end
34
+
35
+ def ignore_checks!(&block)
36
+ @ignore = true
37
+ result = yield
38
+ @ignore = false
39
+ result
40
+ end
41
+
42
+ def unchecked?
43
+ @should_check_stack == [false]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,39 @@
1
+ require 'ostruct'
2
+
3
+ module Datacaster
4
+ module Runtimes
5
+ class UserContext < Base
6
+ class ContextStruct
7
+ def initialize(context, node)
8
+ @context = context
9
+ @node = node
10
+ end
11
+
12
+ def method_missing(m, *args)
13
+ if !args.empty? || block_given?
14
+ return super
15
+ end
16
+
17
+ if @context.key?(m)
18
+ return @context[m]
19
+ end
20
+
21
+ begin
22
+ @node.class.send_to_parent(@node, :context).public_send(m)
23
+ rescue NoMethodError
24
+ raise NoMethodError.new("Key #{m.inspect} is not found in the context")
25
+ end
26
+ end
27
+ end
28
+
29
+ def initialize(parent, user_context)
30
+ super(parent)
31
+ @context_struct = ContextStruct.new(user_context, self)
32
+ end
33
+
34
+ def context
35
+ @context_struct
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ require 'yaml'
2
+
3
+ module Datacaster
4
+ module SubstituteI18n
5
+ @load_path = []
6
+
7
+ def self.exists?(key)
8
+ !fetch(key).nil?
9
+ end
10
+
11
+ def self.fetch(key)
12
+ keys = [locale] + key.split('.')
13
+
14
+ @translations.each do |hash|
15
+ result = hash.dig(*keys)
16
+ return result unless result.nil?
17
+ end
18
+ nil
19
+ end
20
+
21
+ def self.load_path
22
+ @load_path
23
+ end
24
+
25
+ def self.load_path=(array)
26
+ @load_path = array
27
+ @translations = array.map { |x| YAML.load_file(x) }
28
+ end
29
+
30
+ def self.locale
31
+ 'en'
32
+ end
33
+
34
+ def self.locale=(*)
35
+ raise NotImplementedError.new("Setting locale is not supported, use ruby-i18n instead of datacaster's built-in")
36
+ end
37
+
38
+ def self.t(key, **args)
39
+ string = fetch(key)
40
+ return "Translation missing #{key}" unless string
41
+
42
+ args.each do |from, to|
43
+ string = string.gsub("%{#{from}}", to.to_s)
44
+ end
45
+ string
46
+ end
47
+ end
48
+ end
@@ -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
@@ -1,29 +1,28 @@
1
1
  module Datacaster
2
2
  class ThenNode < Base
3
- def initialize(left, then_caster)
3
+ def initialize(left, then_caster, else_caster = nil)
4
4
  @left = left
5
5
  @then = then_caster
6
+ @else = else_caster
6
7
  end
7
8
 
8
9
  def else(else_caster)
9
10
  raise ArgumentError.new("Datacaster: double else clause is not permitted") if @else
10
11
 
11
- @else = else_caster
12
- self
12
+ self.class.new(@left, @then, DefinitionDSL.expand(else_caster))
13
13
  end
14
14
 
15
- def cast(object)
15
+ def cast(object, runtime:)
16
16
  unless @else
17
17
  raise ArgumentError.new('Datacaster: use "a & b" instead of "a.then(b)" when there is no else-clause')
18
18
  end
19
19
 
20
- object = super(object)
21
- left_result = @left.(object)
20
+ left_result = @left.with_runtime(runtime).(object)
22
21
 
23
22
  if left_result.valid?
24
- @then.(left_result)
23
+ @then.with_runtime(runtime).(left_result.value)
25
24
  else
26
- @else.(object)
25
+ @else.with_runtime(runtime).(object)
27
26
  end
28
27
  end
29
28
 
@@ -1,21 +1,17 @@
1
1
  module Datacaster
2
2
  class Transformer < Base
3
- def initialize(name, &block)
3
+ def initialize(&block)
4
4
  raise "Expected block" unless block_given?
5
5
 
6
- @name = name
7
6
  @transform = block
8
7
  end
9
8
 
10
- def cast(object)
11
- intermediary_result = super(object)
12
- object = intermediary_result.value
13
-
14
- Datacaster.ValidResult(@transform.(object))
9
+ def cast(object, runtime:)
10
+ Datacaster.ValidResult(Runtimes::Base.(runtime, @transform, object))
15
11
  end
16
12
 
17
13
  def inspect
18
- "#<Datacaster::#{@name}Transformer>"
14
+ "#<Datacaster::Transformer>"
19
15
  end
20
16
  end
21
17
  end
@@ -1,27 +1,25 @@
1
1
  module Datacaster
2
2
  class Trier < Base
3
- def initialize(name, error, catched_exception, &block)
3
+ def initialize(catched_exception, error_key = nil, &block)
4
4
  raise "Expected block" unless block_given?
5
5
 
6
- @name = name
7
- @error = error
8
6
  @catched_exception = Array(catched_exception)
9
- @transform = block
10
- end
7
+ @try = block
11
8
 
12
- def cast(object)
13
- intermediary_result = super(object)
14
- object = intermediary_result.value
9
+ @error_keys = ['.try', 'datacaster.errors.try']
10
+ @error_keys.unshift(error_key) if error_key
11
+ end
15
12
 
13
+ def cast(object, runtime:)
16
14
  begin
17
- Datacaster.ValidResult(@transform.(object))
15
+ Datacaster.ValidResult(Runtimes::Base.(runtime, @try, object))
18
16
  rescue *@catched_exception
19
- Datacaster.ErrorResult([@error])
17
+ Datacaster.ErrorResult(I18nValues::Key.new(@error_keys, value: object))
20
18
  end
21
19
  end
22
20
 
23
21
  def inspect
24
- "#<Datacaster::#{@name}Trier>"
22
+ "#<Datacaster::Trier>"
25
23
  end
26
24
  end
27
25
  end
@@ -1,5 +1,3 @@
1
- require 'active_model'
2
-
3
1
  module Datacaster
4
2
  class Validator < Base
5
3
  @@validations = {}
@@ -21,21 +19,22 @@ module Datacaster
21
19
  end.new
22
20
  end
23
21
 
24
- def initialize(validations, name)
25
- @name = name
22
+ def initialize(validations)
23
+ require 'active_model'
24
+
25
+ if Config.i18n_module == SubstituteI18n
26
+ raise NotImplementedError, "Using ActiveModel validations requires ruby-i18n or another i18n gem instead of datacaster's built-in", caller
27
+ end
26
28
  @validator = self.class.create_active_model(validations)
27
29
  end
28
30
 
29
- def cast(object)
30
- intermediary_result = super(object)
31
- object = intermediary_result.value
32
-
31
+ def cast(object, runtime:)
33
32
  @validator.value = object
34
33
  @validator.valid? ? Datacaster.ValidResult(object) : Datacaster.ErrorResult(@validator.errors[:value])
35
34
  end
36
35
 
37
36
  def inspect
38
- "#<Datacaster::#{@name}Validator>"
37
+ "#<Datacaster::Validator>"
39
38
  end
40
39
  end
41
40
  end
@@ -1,3 +1,3 @@
1
1
  module Datacaster
2
- VERSION = "2.0.2"
2
+ VERSION = "3.1.0"
3
3
  end
data/lib/datacaster.rb CHANGED
@@ -1,41 +1,23 @@
1
+ require 'zeitwerk'
2
+ loader = Zeitwerk::Loader.for_gem
3
+ loader.inflector.inflect('definition_dsl' => 'DefinitionDSL')
4
+ loader.setup
5
+
1
6
  require_relative 'datacaster/result'
2
- require_relative 'datacaster/version'
3
-
4
- require_relative 'datacaster/absent'
5
- require_relative 'datacaster/base'
6
- require_relative 'datacaster/predefined'
7
- require_relative 'datacaster/definition_context'
8
- require_relative 'datacaster/terminator'
9
- require_relative 'datacaster/config'
10
-
11
- require_relative 'datacaster/array_schema'
12
- require_relative 'datacaster/caster'
13
- require_relative 'datacaster/checker'
14
- require_relative 'datacaster/comparator'
15
- require_relative 'datacaster/hash_mapper'
16
- require_relative 'datacaster/hash_schema'
17
- require_relative 'datacaster/message_keys_merger'
18
- require_relative 'datacaster/transformer'
19
- require_relative 'datacaster/trier'
20
-
21
- require_relative 'datacaster/and_node'
22
- require_relative 'datacaster/and_with_error_aggregation_node'
23
- require_relative 'datacaster/or_node'
24
- require_relative 'datacaster/then_node'
25
7
 
26
8
  module Datacaster
27
9
  extend self
28
10
 
29
- def schema(&block)
30
- build_schema(Terminator::Raising.instance, &block)
11
+ def schema(i18n_scope: nil, &block)
12
+ ContextNodes::StructureCleaner.new(build_schema(i18n_scope: i18n_scope, &block), :fail)
31
13
  end
32
14
 
33
- def choosy_schema(&block)
34
- build_schema(Terminator::Sweeping.instance, &block)
15
+ def choosy_schema(i18n_scope: nil, &block)
16
+ ContextNodes::StructureCleaner.new(build_schema(i18n_scope: i18n_scope, &block), :remove)
35
17
  end
36
18
 
37
- def partial_schema(&block)
38
- build_schema(nil, &block)
19
+ def partial_schema(i18n_scope: nil, &block)
20
+ ContextNodes::StructureCleaner.new(build_schema(i18n_scope: i18n_scope, &block), :pass)
39
21
  end
40
22
 
41
23
  def absent
@@ -44,19 +26,17 @@ module Datacaster
44
26
 
45
27
  private
46
28
 
47
- def build_schema(terminator, &block)
29
+ def build_schema(i18n_scope: nil, &block)
48
30
  raise "Expected block" unless block
49
31
 
50
- definition_context = DefinitionContext.new
51
-
52
- datacaster = definition_context.instance_exec(&block)
32
+ datacaster = DefinitionDSL.eval(&block)
53
33
 
54
34
  unless datacaster.is_a?(Base)
55
35
  raise "Datacaster instance should be returned from a block (e.g. result of 'hash_schema(...)' call)"
56
36
  end
57
37
 
58
- datacaster = (datacaster & terminator) if terminator
59
- datacaster.set_definition_context(definition_context)
38
+ datacaster = datacaster.i18n_scope(i18n_scope) if i18n_scope
39
+
60
40
  datacaster
61
41
  end
62
42
  end