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.
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