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