datacaster 2.0.2 → 3.0.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.
- checksums.yaml +4 -4
- data/README.md +518 -268
- data/config/locales/en.yml +24 -0
- data/datacaster.gemspec +2 -0
- data/lib/datacaster/absent.rb +4 -0
- data/lib/datacaster/and_node.rb +3 -5
- data/lib/datacaster/and_with_error_aggregation_node.rb +5 -6
- data/lib/datacaster/array_schema.rb +18 -16
- data/lib/datacaster/base.rb +33 -44
- data/lib/datacaster/caster.rb +4 -8
- data/lib/datacaster/checker.rb +8 -10
- data/lib/datacaster/comparator.rb +9 -9
- data/lib/datacaster/config.rb +28 -0
- data/lib/datacaster/context_node.rb +43 -0
- data/lib/datacaster/context_nodes/errors_caster.rb +21 -0
- data/lib/datacaster/context_nodes/i18n.rb +20 -0
- data/lib/datacaster/context_nodes/i18n_keys_mapper.rb +27 -0
- data/lib/datacaster/context_nodes/structure_cleaner.rb +103 -0
- data/lib/datacaster/context_nodes/user_context.rb +20 -0
- data/lib/datacaster/definition_dsl.rb +37 -0
- data/lib/datacaster/hash_mapper.rb +13 -16
- data/lib/datacaster/hash_schema.rb +14 -15
- data/lib/datacaster/i18n_values/base.rb +87 -0
- data/lib/datacaster/i18n_values/key.rb +34 -0
- data/lib/datacaster/i18n_values/scope.rb +28 -0
- data/lib/datacaster/message_keys_merger.rb +8 -15
- data/lib/datacaster/or_node.rb +3 -4
- data/lib/datacaster/predefined.rb +119 -64
- data/lib/datacaster/result.rb +35 -14
- data/lib/datacaster/runtimes/base.rb +47 -0
- data/lib/datacaster/runtimes/i18n.rb +20 -0
- data/lib/datacaster/runtimes/structure_cleaner.rb +47 -0
- data/lib/datacaster/runtimes/user_context.rb +39 -0
- data/lib/datacaster/substitute_i18n.rb +48 -0
- data/lib/datacaster/then_node.rb +7 -8
- data/lib/datacaster/transformer.rb +4 -8
- data/lib/datacaster/trier.rb +9 -11
- data/lib/datacaster/validator.rb +8 -9
- data/lib/datacaster/version.rb +1 -1
- data/lib/datacaster.rb +15 -35
- metadata +57 -9
- data/lib/datacaster/definition_context.rb +0 -20
- data/lib/datacaster/terminator.rb +0 -98
@@ -0,0 +1,24 @@
|
|
1
|
+
en:
|
2
|
+
datacaster:
|
3
|
+
errors:
|
4
|
+
absent: should be absent
|
5
|
+
any: should be present
|
6
|
+
array: should be an array
|
7
|
+
cast: is invalid
|
8
|
+
check: is invalid
|
9
|
+
compare: "does not equal %{reference}"
|
10
|
+
decimal: is not a decimal number
|
11
|
+
empty: should not be empty
|
12
|
+
float: is not a float
|
13
|
+
hash_value: is not a hash
|
14
|
+
integer: is not an integer
|
15
|
+
integer32: is not a 32-bit integer
|
16
|
+
iso8601: is not a string with ISO-8601 date and time
|
17
|
+
must_be: "is not %{reference}"
|
18
|
+
responds_to: "does not respond to %{reference}"
|
19
|
+
string: is not a string
|
20
|
+
to_boolean: does not look like a boolean
|
21
|
+
to_float: does not look like a float
|
22
|
+
to_integer: does not look like an integer
|
23
|
+
non_empty_string: should be non-empty string
|
24
|
+
try: raised an error
|
data/datacaster.gemspec
CHANGED
@@ -25,6 +25,8 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_development_dependency 'activemodel', '>= 5.2'
|
26
26
|
spec.add_development_dependency 'rake', '~> 12.0'
|
27
27
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
28
|
+
spec.add_development_dependency 'i18n', '~> 1.14'
|
28
29
|
|
29
30
|
spec.add_runtime_dependency 'dry-monads', '>= 1.3', '< 1.4'
|
31
|
+
spec.add_runtime_dependency 'zeitwerk', '>= 2', '< 3'
|
30
32
|
end
|
data/lib/datacaster/absent.rb
CHANGED
data/lib/datacaster/and_node.rb
CHANGED
@@ -5,13 +5,11 @@ module Datacaster
|
|
5
5
|
@right = right
|
6
6
|
end
|
7
7
|
|
8
|
-
def cast(object)
|
9
|
-
|
10
|
-
|
11
|
-
left_result = @left.(object)
|
8
|
+
def cast(object, runtime:)
|
9
|
+
left_result = @left.with_runtime(runtime).(object)
|
12
10
|
return left_result unless left_result.valid?
|
13
11
|
|
14
|
-
@right.(left_result)
|
12
|
+
@right.with_runtime(runtime).(left_result.value)
|
15
13
|
end
|
16
14
|
|
17
15
|
def inspect
|
@@ -7,18 +7,17 @@ module Datacaster
|
|
7
7
|
|
8
8
|
# Works like AndNode, but doesn't stop at first error — in order to aggregate all Failures
|
9
9
|
# Makes sense only for Hash Schemas
|
10
|
-
def cast(object)
|
11
|
-
|
12
|
-
left_result = @left.(object)
|
10
|
+
def cast(object, runtime:)
|
11
|
+
left_result = @left.with_runtime(runtime).(object)
|
13
12
|
|
14
13
|
if left_result.valid?
|
15
|
-
@right.(left_result)
|
14
|
+
@right.with_runtime(runtime).(left_result.value)
|
16
15
|
else
|
17
|
-
right_result = @right.(object)
|
16
|
+
right_result = @right.with_runtime(runtime).(object)
|
18
17
|
if right_result.valid?
|
19
18
|
left_result
|
20
19
|
else
|
21
|
-
Datacaster.ErrorResult(self.class.merge_errors(left_result.
|
20
|
+
Datacaster.ErrorResult(self.class.merge_errors(left_result.raw_errors, right_result.raw_errors))
|
22
21
|
end
|
23
22
|
end
|
24
23
|
end
|
@@ -1,30 +1,32 @@
|
|
1
1
|
module Datacaster
|
2
2
|
class ArraySchema < Base
|
3
|
-
def initialize(element_caster)
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
def initialize(element_caster, error_keys = {})
|
4
|
+
@element_caster = element_caster
|
5
|
+
|
6
|
+
@not_array_error_keys = ['.array', 'datacaster.errors.array']
|
7
|
+
@not_array_error_keys.unshift(error_keys[:array]) if error_keys[:array]
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
@empty_error_keys = ['.empty', 'datacaster.errors.empty']
|
10
|
+
@error_keys.unshift(error_keys[:empty]) if error_keys[:empty]
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
+
def cast(array, runtime:)
|
14
|
+
return Datacaster.ErrorResult(I18nValues::Key.new(@not_array_error_keys, value: array)) if !array.respond_to?(:map) || !array.respond_to?(:zip)
|
15
|
+
return Datacaster.ErrorResult(I18nValues::Key.new(@empty_error_keys, value: array)) if array.empty?
|
13
16
|
|
14
|
-
|
15
|
-
return Datacaster.ErrorResult(["must not be empty"]) if array.empty?
|
17
|
+
runtime.will_check!
|
16
18
|
|
17
19
|
result =
|
18
|
-
array.
|
19
|
-
|
20
|
-
|
20
|
+
array.map.with_index do |x, i|
|
21
|
+
runtime.checked_key!(i) do
|
22
|
+
@element_caster.with_runtime(runtime).(x)
|
23
|
+
end
|
21
24
|
end
|
22
25
|
|
23
26
|
if result.all?(&:valid?)
|
24
|
-
|
25
|
-
Datacaster.ValidResult(result.map(&:value), meta: {checked_schema: checked_schema})
|
27
|
+
Datacaster.ValidResult(result.map!(&:value))
|
26
28
|
else
|
27
|
-
Datacaster.ErrorResult(result.each.with_index.reject { |x, _| x.valid? }.map { |x, i| [i, x.
|
29
|
+
Datacaster.ErrorResult(result.each.with_index.reject { |x, _| x.valid? }.map { |x, i| [i, x.raw_errors] }.to_h)
|
28
30
|
end
|
29
31
|
end
|
30
32
|
|
data/lib/datacaster/base.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require "ostruct"
|
2
|
-
|
3
1
|
module Datacaster
|
4
2
|
class Base
|
5
3
|
def self.merge_errors(left, right)
|
@@ -43,66 +41,57 @@ module Datacaster
|
|
43
41
|
AndWithErrorAggregationNode.new(self, other)
|
44
42
|
end
|
45
43
|
|
46
|
-
def
|
47
|
-
|
44
|
+
def cast_errors(error_caster)
|
45
|
+
ContextNodes::ErrorsCaster.new(self, error_caster)
|
48
46
|
end
|
49
47
|
|
50
|
-
def
|
51
|
-
|
48
|
+
def then(other)
|
49
|
+
ThenNode.new(self, other)
|
52
50
|
end
|
53
51
|
|
54
|
-
def with_context(
|
55
|
-
|
56
|
-
|
52
|
+
def with_context(context)
|
53
|
+
unless context.is_a?(Hash)
|
54
|
+
raise "with_context expected Hash as argument, got #{context.inspect} instead"
|
55
|
+
end
|
56
|
+
ContextNodes::UserContext.new(self, context)
|
57
57
|
end
|
58
58
|
|
59
59
|
def call(object)
|
60
|
-
object
|
61
|
-
|
62
|
-
return object if object.valid? || @cast_errors.nil?
|
63
|
-
|
64
|
-
error_cast = @cast_errors.(object.errors)
|
60
|
+
call_with_runtime(object, Runtimes::Base.new)
|
61
|
+
end
|
65
62
|
|
66
|
-
|
63
|
+
def call_with_runtime(object, runtime)
|
64
|
+
result = cast(object, runtime: runtime)
|
65
|
+
unless result.is_a?(Result)
|
66
|
+
raise RuntimeError.new("Caster should've returned Datacaster::Result, but returned #{result.inspect} instead")
|
67
|
+
end
|
68
|
+
result
|
69
|
+
end
|
67
70
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
71
|
+
def with_runtime(runtime)
|
72
|
+
->(object) do
|
73
|
+
call_with_runtime(object, runtime)
|
74
|
+
end
|
72
75
|
end
|
73
76
|
|
74
|
-
def
|
75
|
-
|
76
|
-
self
|
77
|
+
def i18n_key(*keys, **args)
|
78
|
+
ContextNodes::I18n.new(self, I18nValues::Key.new(keys, args))
|
77
79
|
end
|
78
80
|
|
79
|
-
def
|
80
|
-
|
81
|
+
def i18n_map_keys(mapping)
|
82
|
+
ContextNodes::I18nKeysMapper.new(self, mapping)
|
81
83
|
end
|
82
84
|
|
83
|
-
|
85
|
+
def i18n_scope(scope, **args)
|
86
|
+
ContextNodes::I18n.new(self, I18nValues::Scope.new(scope, args))
|
87
|
+
end
|
84
88
|
|
85
|
-
def
|
86
|
-
|
89
|
+
def i18n_vars(vars)
|
90
|
+
ContextNodes::I18n.new(self, I18nValues::Scope.new(nil, vars))
|
87
91
|
end
|
88
92
|
|
89
|
-
|
90
|
-
|
91
|
-
def shortcut_definition(definition)
|
92
|
-
case definition
|
93
|
-
when Datacaster::Base
|
94
|
-
definition
|
95
|
-
when Array
|
96
|
-
if definition.length != 1
|
97
|
-
raise ArgumentError.new("Datacaster: shortcut array definitions must have exactly 1 element in the array, e.g. [integer]")
|
98
|
-
end
|
99
|
-
ArraySchema.new(definition.first)
|
100
|
-
when Hash
|
101
|
-
HashSchema.new(definition)
|
102
|
-
else
|
103
|
-
return definition if definition.respond_to?(:call)
|
104
|
-
raise ArgumentError.new("Datacaster: Unknown definition #{definition.inspect}, which doesn't respond to #call")
|
105
|
-
end
|
93
|
+
def inspect
|
94
|
+
"#<Datacaster::Base>"
|
106
95
|
end
|
107
96
|
end
|
108
97
|
end
|
data/lib/datacaster/caster.rb
CHANGED
@@ -1,17 +1,13 @@
|
|
1
1
|
module Datacaster
|
2
2
|
class Caster < Base
|
3
|
-
def initialize(
|
3
|
+
def initialize(&block)
|
4
4
|
raise "Expected block" unless block_given?
|
5
5
|
|
6
|
-
@name = name
|
7
6
|
@cast = block
|
8
7
|
end
|
9
8
|
|
10
|
-
def cast(object)
|
11
|
-
|
12
|
-
object = intermediary_result.value
|
13
|
-
|
14
|
-
result = @cast.(object)
|
9
|
+
def cast(object, runtime:)
|
10
|
+
result = Runtimes::Base.(runtime, @cast, object)
|
15
11
|
|
16
12
|
raise TypeError.new("Either Datacaster::Result or Dry::Monads::Result " \
|
17
13
|
"should be returned from cast block") unless [Datacaster::Result, Dry::Monads::Result].any? { |k| result.is_a?(k) }
|
@@ -24,7 +20,7 @@ module Datacaster
|
|
24
20
|
end
|
25
21
|
|
26
22
|
def inspect
|
27
|
-
"#<Datacaster
|
23
|
+
"#<Datacaster::Caster>"
|
28
24
|
end
|
29
25
|
end
|
30
26
|
end
|
data/lib/datacaster/checker.rb
CHANGED
@@ -1,26 +1,24 @@
|
|
1
1
|
module Datacaster
|
2
2
|
class Checker < Base
|
3
|
-
def initialize(
|
3
|
+
def initialize(error_key = nil, &block)
|
4
4
|
raise "Expected block" unless block_given?
|
5
5
|
|
6
|
-
@
|
7
|
-
@
|
6
|
+
@error_keys = ['.check', 'datacaster.errors.check']
|
7
|
+
@error_keys.unshift(error_key) if error_key
|
8
|
+
|
8
9
|
@check = block
|
9
10
|
end
|
10
11
|
|
11
|
-
def cast(object)
|
12
|
-
|
13
|
-
object = intermediary_result.value
|
14
|
-
|
15
|
-
if @check.(object)
|
12
|
+
def cast(object, runtime:)
|
13
|
+
if Runtimes::Base.(runtime, @check, object)
|
16
14
|
Datacaster.ValidResult(object)
|
17
15
|
else
|
18
|
-
Datacaster.ErrorResult(
|
16
|
+
Datacaster.ErrorResult(I18nValues::Key.new(@error_keys, value: object))
|
19
17
|
end
|
20
18
|
end
|
21
19
|
|
22
20
|
def inspect
|
23
|
-
"#<Datacaster
|
21
|
+
"#<Datacaster::Checker>"
|
24
22
|
end
|
25
23
|
end
|
26
24
|
end
|
@@ -1,24 +1,24 @@
|
|
1
1
|
module Datacaster
|
2
2
|
class Comparator < Base
|
3
|
-
def initialize(value,
|
3
|
+
def initialize(value, error_key = nil)
|
4
4
|
@value = value
|
5
|
-
@name = name
|
6
|
-
@error = error || "must be equal to #{value.inspect}"
|
7
|
-
end
|
8
5
|
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
@error_keys = ['.compare', 'datacaster.errors.compare']
|
7
|
+
@error_keys.unshift(error_key) if error_key
|
8
|
+
end
|
12
9
|
|
10
|
+
def cast(object, runtime:)
|
13
11
|
if @value == object
|
14
12
|
Datacaster.ValidResult(object)
|
15
13
|
else
|
16
|
-
Datacaster.ErrorResult(
|
14
|
+
Datacaster.ErrorResult(
|
15
|
+
I18nValues::Key.new(@error_keys, reference: @value.inspect, value: object)
|
16
|
+
)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
def inspect
|
21
|
-
"#<Datacaster
|
21
|
+
"#<Datacaster::Comparator>"
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
data/lib/datacaster/config.rb
CHANGED
@@ -2,6 +2,10 @@ module Datacaster
|
|
2
2
|
module Config
|
3
3
|
extend self
|
4
4
|
|
5
|
+
attr_accessor :i18n_t
|
6
|
+
attr_accessor :i18n_exists
|
7
|
+
attr_accessor :i18n_module
|
8
|
+
|
5
9
|
def add_predefined_caster(name, definition)
|
6
10
|
caster =
|
7
11
|
case definition
|
@@ -15,5 +19,29 @@ module Datacaster
|
|
15
19
|
|
16
20
|
Predefined.define_method(name.to_sym) { caster }
|
17
21
|
end
|
22
|
+
|
23
|
+
def i18n_t
|
24
|
+
if @i18n_t.nil? && @i18n_module.nil?
|
25
|
+
i18n_initialize!
|
26
|
+
end
|
27
|
+
@i18n_t || ->(*args, **kwargs) { @i18n_module.t(*args, **kwargs) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def i18n_exists?
|
31
|
+
if @i18n_t.nil? && @i18n_module.nil?
|
32
|
+
i18n_initialize!
|
33
|
+
end
|
34
|
+
@i18n_exists || ->(*args, **kwargs) { @i18n_module.exists?(*args, **kwargs) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def i18n_initialize!
|
38
|
+
@i18n_module ||=
|
39
|
+
if defined?(::I18n)
|
40
|
+
I18n
|
41
|
+
else
|
42
|
+
SubstituteI18n
|
43
|
+
end
|
44
|
+
@i18n_module.load_path += [__dir__ + '/../../config/locales/en.yml']
|
45
|
+
end
|
18
46
|
end
|
19
47
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Datacaster
|
2
|
+
class ContextNode < Base
|
3
|
+
def initialize(base)
|
4
|
+
@base = base
|
5
|
+
end
|
6
|
+
|
7
|
+
def cast(object, runtime:)
|
8
|
+
@runtime = create_runtime(runtime)
|
9
|
+
result = @base.with_runtime(@runtime).call(object)
|
10
|
+
transform_result(result)
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"#<#{self.class.name} base: #{@base.inspect}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def create_runtime(parent)
|
20
|
+
parent
|
21
|
+
end
|
22
|
+
|
23
|
+
def runtime
|
24
|
+
@runtime
|
25
|
+
end
|
26
|
+
|
27
|
+
def transform_result(result)
|
28
|
+
if result.valid?
|
29
|
+
Datacaster.ValidResult(transform_success(result.value))
|
30
|
+
else
|
31
|
+
Datacaster.ErrorResult(transform_errors(result.raw_errors))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def transform_success(value)
|
36
|
+
value
|
37
|
+
end
|
38
|
+
|
39
|
+
def transform_errors(errors)
|
40
|
+
errors
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Datacaster
|
2
|
+
module ContextNodes
|
3
|
+
class ErrorsCaster < Datacaster::ContextNode
|
4
|
+
def initialize(base, error_caster)
|
5
|
+
super(base)
|
6
|
+
@caster = error_caster
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def transform_errors(errors)
|
12
|
+
result = @caster.with_runtime(@runtime).(errors)
|
13
|
+
if result.valid?
|
14
|
+
result.value
|
15
|
+
else
|
16
|
+
raise RuntimeError.new("Error caster tried to cast these errors: #{errors.inspect}, but didn't return ValidResult: #{result.inspect}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Datacaster
|
2
|
+
module ContextNodes
|
3
|
+
class I18n < Datacaster::ContextNode
|
4
|
+
def initialize(base, i18n_value)
|
5
|
+
super(base)
|
6
|
+
@i18n_value = i18n_value
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def create_runtime(parent)
|
12
|
+
Runtimes::I18n.new(parent)
|
13
|
+
end
|
14
|
+
|
15
|
+
def transform_errors(errors)
|
16
|
+
@i18n_value.with_args(runtime.args) * errors
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Datacaster
|
2
|
+
module ContextNodes
|
3
|
+
class I18nKeysMapper < Datacaster::ContextNode
|
4
|
+
def initialize(base, mapping)
|
5
|
+
super(base)
|
6
|
+
@mapping = mapping
|
7
|
+
@from_keys = @mapping.keys
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def transform_errors(errors)
|
13
|
+
return errors unless errors.length == 1 && errors.is_a?(Array)
|
14
|
+
|
15
|
+
error = errors.first
|
16
|
+
return errors unless error.is_a?(I18nValues::Key) || error.is_a?(I18nValues::Key)
|
17
|
+
|
18
|
+
keys = error.respond_to?(:keys) ? error.keys : [error.key]
|
19
|
+
key_to_remap = keys.find { |x| @from_keys.include?(x) }
|
20
|
+
return errors if key_to_remap.nil?
|
21
|
+
new_key = @mapping[key_to_remap]
|
22
|
+
|
23
|
+
[I18nValues::Key.new(new_key, error.args)]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Datacaster
|
2
|
+
module ContextNodes
|
3
|
+
class StructureCleaner < Datacaster::ContextNode
|
4
|
+
def initialize(base, strategy = :fail)
|
5
|
+
super(base)
|
6
|
+
|
7
|
+
unless %i[fail remove pass].include?(strategy)
|
8
|
+
raise ArgumentError.new("Strategy should be :fail (return error on extra keys), :remove (remove extra keys) or :pass (ignore presence of extra keys)")
|
9
|
+
end
|
10
|
+
|
11
|
+
@strategy = strategy
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
"#<#{self.class.name}(#{@strategy}) base: #{@base.inspect}>"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def create_runtime(parent)
|
21
|
+
Runtimes::StructureCleaner.new(parent)
|
22
|
+
end
|
23
|
+
|
24
|
+
def transform_result(result)
|
25
|
+
return result unless result.valid?
|
26
|
+
cast_success(result)
|
27
|
+
end
|
28
|
+
|
29
|
+
def cast_success(result)
|
30
|
+
return result if @strategy == :pass
|
31
|
+
return result if @runtime.unchecked?
|
32
|
+
|
33
|
+
cast_value(result, @runtime.checked_schema)
|
34
|
+
end
|
35
|
+
|
36
|
+
def cast_value(result, schema)
|
37
|
+
return result if schema == true
|
38
|
+
|
39
|
+
case result.value!
|
40
|
+
when Array
|
41
|
+
cast_array(result, schema)
|
42
|
+
when Hash
|
43
|
+
cast_hash(result, schema)
|
44
|
+
else
|
45
|
+
raise "Expected hash or array when checking #{value.inspect}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def cast_array(result, schema)
|
50
|
+
value = result.value!
|
51
|
+
|
52
|
+
unchecked_indicies = value.each_index.to_a - schema.keys
|
53
|
+
if unchecked_indicies.any?
|
54
|
+
return Datacaster.ErrorResult(unchecked_indicies.map { |i| [i, 'must be absent'] }.to_h)
|
55
|
+
end
|
56
|
+
|
57
|
+
output =
|
58
|
+
value.map.with_index do |x, i|
|
59
|
+
cast_value(Datacaster.ValidResult(x), schema[i])
|
60
|
+
end
|
61
|
+
|
62
|
+
if output.all?(&:valid?)
|
63
|
+
Datacaster.ValidResult(output.map(&:value))
|
64
|
+
else
|
65
|
+
Datacaster.ErrorResult(output.each.with_index.reject { |x, _| x.valid? }.map { |x, i| [i, x.raw_errors] }.to_h)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def cast_hash(result, schema)
|
70
|
+
errors = {}
|
71
|
+
output = {}
|
72
|
+
value = result.value!
|
73
|
+
|
74
|
+
value.each do |(k, v)|
|
75
|
+
next if v == Datacaster.absent
|
76
|
+
|
77
|
+
unless schema.key?(k)
|
78
|
+
errors[k] = ['must be absent'] if @strategy == :fail
|
79
|
+
next
|
80
|
+
end
|
81
|
+
|
82
|
+
if schema[k] == true
|
83
|
+
output[k] = v
|
84
|
+
next
|
85
|
+
end
|
86
|
+
|
87
|
+
nested_value = cast_value(Datacaster.ValidResult(v), schema[k])
|
88
|
+
if nested_value.valid?
|
89
|
+
output[k] = nested_value.value
|
90
|
+
else
|
91
|
+
errors[k] = nested_value.raw_errors
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if errors.empty?
|
96
|
+
Datacaster.ValidResult(output)
|
97
|
+
else
|
98
|
+
Datacaster.ErrorResult(errors)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Datacaster
|
2
|
+
module ContextNodes
|
3
|
+
class UserContext < Datacaster::ContextNode
|
4
|
+
def initialize(base, user_context)
|
5
|
+
super(base)
|
6
|
+
@user_context = user_context
|
7
|
+
end
|
8
|
+
|
9
|
+
def inspect
|
10
|
+
"#<#{self.class.name}(#{@user_context.inspect}) base: #{@base.inspect}>"
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def create_runtime(parent)
|
16
|
+
Runtimes::UserContext.new(parent, @user_context)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Datacaster
|
5
|
+
class DefinitionDSL
|
6
|
+
include Datacaster::Predefined
|
7
|
+
include Dry::Monads[:result]
|
8
|
+
|
9
|
+
# Translates hashes like {a: <IntegerChecker>} to <HashSchema {a: <IntegerChecker>}>
|
10
|
+
# and arrays like [<IntegerChecker>] to <ArraySchema <IntegerChecker>>
|
11
|
+
def self.expand(definition)
|
12
|
+
case definition
|
13
|
+
when Datacaster::Base
|
14
|
+
definition
|
15
|
+
when Array
|
16
|
+
if definition.length != 1
|
17
|
+
raise ArgumentError.new("Datacaster: DSL array definitions must have exactly 1 element in the array, e.g. [integer]")
|
18
|
+
end
|
19
|
+
ArraySchema.new(expand(definition.first))
|
20
|
+
when Hash
|
21
|
+
HashSchema.new(definition.transform_values { |x| expand(x) })
|
22
|
+
else
|
23
|
+
return definition if definition.respond_to?(:call)
|
24
|
+
raise ArgumentError.new("Datacaster: Unknown definition #{definition.inspect}, which doesn't respond to #call")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.eval(&block)
|
29
|
+
new.instance_exec(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(m, *args)
|
33
|
+
arg_string = args.empty? ? "" : "(#{args.map(&:inspect).join(', ')})"
|
34
|
+
raise RuntimeError, "Datacaster: unknown definition '#{m}#{arg_string}'", caller
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|