datacaster 2.0.2 → 3.1.0

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