datacaster 2.0.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +518 -268
  3. data/config/locales/en.yml +24 -0
  4. data/datacaster.gemspec +2 -0
  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/structure_cleaner.rb +103 -0
  19. data/lib/datacaster/context_nodes/user_context.rb +20 -0
  20. data/lib/datacaster/definition_dsl.rb +37 -0
  21. data/lib/datacaster/hash_mapper.rb +13 -16
  22. data/lib/datacaster/hash_schema.rb +14 -15
  23. data/lib/datacaster/i18n_values/base.rb +87 -0
  24. data/lib/datacaster/i18n_values/key.rb +34 -0
  25. data/lib/datacaster/i18n_values/scope.rb +28 -0
  26. data/lib/datacaster/message_keys_merger.rb +8 -15
  27. data/lib/datacaster/or_node.rb +3 -4
  28. data/lib/datacaster/predefined.rb +119 -64
  29. data/lib/datacaster/result.rb +35 -14
  30. data/lib/datacaster/runtimes/base.rb +47 -0
  31. data/lib/datacaster/runtimes/i18n.rb +20 -0
  32. data/lib/datacaster/runtimes/structure_cleaner.rb +47 -0
  33. data/lib/datacaster/runtimes/user_context.rb +39 -0
  34. data/lib/datacaster/substitute_i18n.rb +48 -0
  35. data/lib/datacaster/then_node.rb +7 -8
  36. data/lib/datacaster/transformer.rb +4 -8
  37. data/lib/datacaster/trier.rb +9 -11
  38. data/lib/datacaster/validator.rb +8 -9
  39. data/lib/datacaster/version.rb +1 -1
  40. data/lib/datacaster.rb +15 -35
  41. metadata +57 -9
  42. data/lib/datacaster/definition_context.rb +0 -20
  43. 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
@@ -12,6 +12,10 @@ module Datacaster
12
12
  "#<Datacaster.absent>"
13
13
  end
14
14
 
15
+ def to_s
16
+ ""
17
+ end
18
+
15
19
  def present?
16
20
  false
17
21
  end
@@ -5,13 +5,11 @@ module Datacaster
5
5
  @right = right
6
6
  end
7
7
 
8
- def cast(object)
9
- object = super(object)
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
- object = super(object)
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.errors, right_result.errors))
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
- # support of shortcut nested validation definitions, e.g. array_schema({a: [integer], b: {c: integer}})
5
- @element_caster = shortcut_definition(element_caster)
6
- end
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
- def cast(object)
9
- object = super(object)
10
- checked_schema = object.meta[:checked_schema] || []
9
+ @empty_error_keys = ['.empty', 'datacaster.errors.empty']
10
+ @error_keys.unshift(error_keys[:empty]) if error_keys[:empty]
11
+ end
11
12
 
12
- array = object.value
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
- return Datacaster.ErrorResult(["must be array"]) if !array.respond_to?(:map) || !array.respond_to?(:zip)
15
- return Datacaster.ErrorResult(["must not be empty"]) if array.empty?
17
+ runtime.will_check!
16
18
 
17
19
  result =
18
- array.zip(checked_schema).map do |x, schema|
19
- x = Datacaster.ValidResult(x, meta: {checked_schema: schema})
20
- @element_caster.(x)
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
- checked_schema = result.map { |x| x.meta[:checked_schema] }
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.errors] }.to_h)
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
 
@@ -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 then(other)
47
- ThenNode.new(self, other)
44
+ def cast_errors(error_caster)
45
+ ContextNodes::ErrorsCaster.new(self, error_caster)
48
46
  end
49
47
 
50
- def set_definition_context(definition_context)
51
- @definition_context = definition_context
48
+ def then(other)
49
+ ThenNode.new(self, other)
52
50
  end
53
51
 
54
- def with_context(additional_context)
55
- @definition_context.context = OpenStruct.new(additional_context)
56
- self
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 = cast(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
- raise "#cast_errors must return Datacaster.ValidResult, currently it is #{error_cast.inspect}" unless error_cast.valid?
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
- Datacaster.ErrorResult(
69
- @cast_errors.(object.errors).value,
70
- meta: object.meta
71
- )
71
+ def with_runtime(runtime)
72
+ ->(object) do
73
+ call_with_runtime(object, runtime)
74
+ end
72
75
  end
73
76
 
74
- def cast_errors(object)
75
- @cast_errors = shortcut_definition(object)
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 inspect
80
- "#<Datacaster::Base>"
81
+ def i18n_map_keys(mapping)
82
+ ContextNodes::I18nKeysMapper.new(self, mapping)
81
83
  end
82
84
 
83
- private
85
+ def i18n_scope(scope, **args)
86
+ ContextNodes::I18n.new(self, I18nValues::Scope.new(scope, args))
87
+ end
84
88
 
85
- def cast(object)
86
- Datacaster.ValidResult(object)
89
+ def i18n_vars(vars)
90
+ ContextNodes::I18n.new(self, I18nValues::Scope.new(nil, vars))
87
91
  end
88
92
 
89
- # Translates hashes like {a: <IntegerChecker>} to <HashSchema {a: <IntegerChecker>}>
90
- # and arrays like [<IntegerChecker>] to <ArraySchema <IntegerChecker>>
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
@@ -1,17 +1,13 @@
1
1
  module Datacaster
2
2
  class Caster < 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
  @cast = block
8
7
  end
9
8
 
10
- def cast(object)
11
- intermediary_result = super(object)
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::#{@name}Caster>"
23
+ "#<Datacaster::Caster>"
28
24
  end
29
25
  end
30
26
  end
@@ -1,26 +1,24 @@
1
1
  module Datacaster
2
2
  class Checker < Base
3
- def initialize(name, error, &block)
3
+ def initialize(error_key = nil, &block)
4
4
  raise "Expected block" unless block_given?
5
5
 
6
- @name = name
7
- @error = error
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
- intermediary_result = super(object)
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([@error])
16
+ Datacaster.ErrorResult(I18nValues::Key.new(@error_keys, value: object))
19
17
  end
20
18
  end
21
19
 
22
20
  def inspect
23
- "#<Datacaster::#{@name}Checker>"
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, name, error = nil)
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
- def cast(object)
10
- intermediary_result = super(object)
11
- object = intermediary_result.value
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([@error])
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::#{@name}Comparator>"
21
+ "#<Datacaster::Comparator>"
22
22
  end
23
23
  end
24
24
  end
@@ -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