datacaster 0.9.1

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.
@@ -0,0 +1,56 @@
1
+ module Datacaster
2
+ class HashSchema < Base
3
+ def initialize(fields)
4
+ @fields = fields
5
+ # support of shortcut nested validation definitions, e.g. array_schema(a: [integer], b: {c: integer})
6
+ @fields.transform_values! { |validator| shortcut_definition(validator) }
7
+ end
8
+
9
+ def call(object)
10
+ object = super(object)
11
+
12
+ return Datacaster.ErrorResult(["must be hash"]) unless object.value.is_a?(Hash)
13
+
14
+ checked_schema = object.meta[:checked_schema].dup || {}
15
+
16
+ errors = {}
17
+ result = {}
18
+
19
+ @fields.each do |key, validator|
20
+ value =
21
+ if object.value.key?(key)
22
+ object.value[key]
23
+ else
24
+ Datacaster.absent
25
+ end
26
+
27
+ new_value = validator.(value)
28
+ if new_value.valid?
29
+ result[key] = new_value.value
30
+ checked_schema[key] = new_value.meta[:checked_schema].dup || true
31
+ else
32
+ errors[key] = new_value.errors
33
+ end
34
+ end
35
+
36
+ if errors.empty?
37
+ # All unchecked key-value pairs are passed through, and eliminated by Terminator
38
+ # at the end of the chain
39
+ result_hash = object.value.merge(result)
40
+ result_hash.keys.each { |k| result_hash.delete(k) if result_hash[k] == Datacaster.absent }
41
+ Datacaster.ValidResult(result_hash, meta: {checked_schema: checked_schema})
42
+ else
43
+ Datacaster.ErrorResult(errors)
44
+ end
45
+ end
46
+
47
+ def inspect
48
+ field_descriptions =
49
+ @fields.map do |k, v|
50
+ "#{k.inspect} => #{v.inspect}"
51
+ end
52
+
53
+ "#<Datacaster::HashSchema {#{field_descriptions.join(', ')}}>"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,22 @@
1
+ module Datacaster
2
+ class OrNode < Base
3
+ def initialize(left, right)
4
+ @left = left
5
+ @right = right
6
+ end
7
+
8
+ def call(object)
9
+ object = super(object)
10
+
11
+ left_result = @left.(object)
12
+
13
+ return left_result if left_result.valid?
14
+
15
+ @right.(object)
16
+ end
17
+
18
+ def inspect
19
+ "#<Datacaster::OrNode L: #{@left.inspect} R: #{@right.inspect}>"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,179 @@
1
+ module Datacaster
2
+ module Predefined
3
+ extend self
4
+
5
+ # Base types
6
+
7
+ def cast(name = 'Anonymous', &block)
8
+ Caster.new(name, &block)
9
+ end
10
+
11
+ def check(name = 'Anonymous', error = 'is invalid', &block)
12
+ Checker.new(name, error, &block)
13
+ end
14
+
15
+ def compare(value, name = 'Anonymous', error = nil)
16
+ Comparator.new(value, name, error)
17
+ end
18
+
19
+ def transform(name = 'Anonymous', &block)
20
+ Transformer.new(name, &block)
21
+ end
22
+
23
+ def transform_if_present(name = 'Anonymous', &block)
24
+ raise 'Expected block' unless block_given?
25
+
26
+ Transformer.new(name) { |v| v == Datacaster.absent ? v : block.(v) }
27
+ end
28
+
29
+ def try(name = 'Anonymous', error = 'is invalid', catched_exception:, &block)
30
+ Trier.new(name, error, catched_exception, &block)
31
+ end
32
+
33
+ def array_schema(element_caster)
34
+ ArraySchema.new(element_caster)
35
+ end
36
+
37
+ def hash_schema(fields)
38
+ HashSchema.new(fields)
39
+ end
40
+
41
+ def transform_to_hash(fields)
42
+ HashMapper.new(fields)
43
+ end
44
+
45
+ def validate(active_model_validations, name = 'Anonymous')
46
+ Validator.new(active_model_validations, name)
47
+ end
48
+
49
+ # 'Meta' types
50
+
51
+ def absent
52
+ check('Absent', 'must be absent') { |x| x == Datacaster.absent }
53
+ end
54
+
55
+ def any
56
+ check('Any', 'must be set') { |x| x != Datacaster.absent }
57
+ end
58
+
59
+ def transform_to_value(value)
60
+ transform('ToValue') { value }
61
+ end
62
+
63
+ def remove
64
+ transform('Remove') { Datacaster.absent }
65
+ end
66
+
67
+ def pass
68
+ transform('Pass', &:itself)
69
+ end
70
+
71
+ def pick(*keys)
72
+ must_be(Enumerable) & transform("Picker") { |value|
73
+ result =
74
+ keys.map do |key|
75
+ if value.respond_to?(:key?) && !value.key?(key)
76
+ Datacaster.absent
77
+ elsif value.respond_to?(:length) && key.is_a?(Integer) && key > 0 && key >= value.length
78
+ Datacaster.absent
79
+ else
80
+ value[key]
81
+ end
82
+ end
83
+
84
+ keys.length == 1 ? result.first : result
85
+ }
86
+ end
87
+
88
+ def responds_to(method)
89
+ check('RespondsTo', "must respond to #{method.inspect}") { |x| x.respond_to?(method) }
90
+ end
91
+
92
+ def must_be(klass)
93
+ check('MustBe', "must be #{klass.inspect}") { |x| x.is_a?(klass) }
94
+ end
95
+
96
+ def optional(base)
97
+ absent | base
98
+ end
99
+
100
+ # Strict types
101
+
102
+ def decimal(digits = 8)
103
+ Trier.new('Decimal', 'must be decimal', [ArgumentError, TypeError]) do |x|
104
+ # strictly validate format of string, BigDecimal() doesn't do that
105
+ Float(x)
106
+
107
+ BigDecimal(x, digits)
108
+ end
109
+ end
110
+
111
+ def array
112
+ check('Array', 'must be array') { |x| x.is_a?(Array) }
113
+ end
114
+
115
+ def float
116
+ check('Float', 'must be float') { |x| x.is_a?(Float) }
117
+ end
118
+
119
+ # 'hash' is a bad method name, because it will overwrite built in Object#hash
120
+ def hash_value
121
+ check('Hash', 'must be hash') { |x| x.is_a?(Hash) }
122
+ end
123
+
124
+ def hash_with_symbolized_keys
125
+ hash_value & transform("SymbolizeKeys") { |x| x.symbolize_keys }
126
+ end
127
+
128
+ def integer
129
+ check('Integer', 'must be integer') { |x| x.is_a?(Integer) }
130
+ end
131
+
132
+ def integer32
133
+ integer & check('FourBytes', 'out of range') { |x| x.abs <= 2_147_483_647 }
134
+ end
135
+
136
+ def string
137
+ check('String', 'must be string') { |x| x.is_a?(String) }
138
+ end
139
+
140
+ def non_empty_string
141
+ string & check('NonEmptyString', 'must be present') { |x| !x.empty? }
142
+ end
143
+
144
+ # Form request types
145
+
146
+ def iso8601
147
+ string &
148
+ try('ISO8601', 'must be iso8601 string', catched_exception: [ArgumentError, TypeError]) { |x| DateTime.iso8601(x) }
149
+ end
150
+
151
+ def to_boolean
152
+ cast('ToBoolean') do |x|
153
+ if ['true', '1', true].include?(x)
154
+ Datacaster.ValidResult(true)
155
+ elsif ['false', '0', false].include?(x)
156
+ Datacaster.ValidResult(false)
157
+ else
158
+ Datacaster.ErrorResult(['must be boolean'])
159
+ end
160
+ end
161
+ end
162
+
163
+ def to_float
164
+ Trier.new('ToFloat', 'must be float', [ArgumentError, TypeError]) do |x|
165
+ Float(x)
166
+ end
167
+ end
168
+
169
+ def to_integer
170
+ Trier.new('ToInteger', 'must be integer', [ArgumentError, TypeError]) do |x|
171
+ Integer(x)
172
+ end
173
+ end
174
+
175
+ def optional_param(base)
176
+ transform_if_present("optional_param(#{base.inspect})") { |x| x == '' ? Datacaster::Absent.instance : x } & (absent | base)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,61 @@
1
+ require 'dry/monads'
2
+
3
+ module Datacaster
4
+ class Result
5
+ attr_accessor :meta
6
+ include Dry::Monads[:result]
7
+
8
+ def initialize(valid, value_or_errors, meta: nil)
9
+ @value_or_errors = value_or_errors
10
+ @valid = !!valid
11
+ @meta = meta || {}
12
+ end
13
+
14
+ def valid?
15
+ @valid
16
+ end
17
+
18
+ def value
19
+ @valid ? @value_or_errors : nil
20
+ end
21
+
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
26
+ @valid ? nil : @value_or_errors
27
+ end
28
+
29
+ def inspect
30
+ if @valid
31
+ "#<Datacaster::ValidResult(#{@value_or_errors.inspect})>"
32
+ else
33
+ "#<Datacaster::ErrorResult(#{@value_or_errors.inspect})>"
34
+ end
35
+ end
36
+
37
+ def to_dry_result
38
+ @valid ? Success(@value_or_errors) : Failure(@value_or_errors)
39
+ end
40
+ end
41
+
42
+ def self.ValidResult(object, meta: nil)
43
+ if object.is_a?(Result)
44
+ raise "Can't create valid result from error #{object.inspect}" unless object.valid?
45
+ object.meta = meta if meta
46
+ object
47
+ else
48
+ Result.new(true, object, meta: meta)
49
+ end
50
+ end
51
+
52
+ def self.ErrorResult(object, meta: nil)
53
+ if object.is_a?(Result)
54
+ raise "Can't create error result from valid #{object.inspect}" if object.valid?
55
+ object.meta = meta if meta
56
+ object
57
+ else
58
+ Result.new(false, object, meta: meta)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ require 'bigdecimal'
2
+ require 'date'
3
+
4
+ module Datacaster
5
+ class RunnerContext
6
+ include Singleton
7
+ include Datacaster::Predefined
8
+ include Dry::Monads[:result]
9
+
10
+ alias_method :array_of, :array_schema
11
+
12
+ def m(definition)
13
+ raise 'not implemented'
14
+ end
15
+
16
+ def method_missing(m, *args)
17
+ arg_string = args.empty? ? '' : "(#{args.map(&:inspect).join(', ')})"
18
+ raise "Datacaster: unknown definition '#{m}#{arg_string}'"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,77 @@
1
+ require 'singleton'
2
+ require 'dry-monads'
3
+
4
+ module Datacaster
5
+ class Terminator < Base
6
+ include Singleton
7
+ include Dry::Monads[:result]
8
+
9
+ def call(object, checked_schema = nil)
10
+ object = super(object)
11
+ checked_schema ||= object.meta[:checked_schema]
12
+
13
+ case object.value
14
+ when Array
15
+ check_array(object.value, checked_schema)
16
+ when Hash
17
+ check_hash(object.value, checked_schema)
18
+ else
19
+ Datacaster.ValidResult(object.value)
20
+ end
21
+ end
22
+
23
+ def inspect
24
+ "#<Datacaster::Terminator>"
25
+ end
26
+
27
+ private
28
+
29
+ def check_array(array, checked_schema)
30
+ return Datacaster.ValidResult(array) unless checked_schema
31
+
32
+ result = array.zip(checked_schema).map { |x, schema| call(x, schema) }
33
+
34
+ if result.all?(&:valid?)
35
+ Datacaster.ValidResult(result.map(&:value))
36
+ else
37
+ Datacaster.ErrorResult(result.each.with_index.reject { |x, _| x.valid? }.map { |x, i| [i, x.errors] }.to_h)
38
+ end
39
+ end
40
+
41
+ def check_hash(hash, checked_schema)
42
+ return Datacaster.ValidResult(hash) unless checked_schema
43
+
44
+ errors = {}
45
+ result = {}
46
+
47
+ hash.each do |(k, v)|
48
+ if v == Datacaster.absent
49
+ next
50
+ end
51
+
52
+ unless checked_schema.key?(k)
53
+ errors[k] = ["must be absent"]
54
+ next
55
+ end
56
+
57
+ if checked_schema[k] == true
58
+ result[k] = v
59
+ next
60
+ end
61
+
62
+ nested_value = call(v, checked_schema[k])
63
+ if nested_value.valid?
64
+ result[k] = nested_value.value
65
+ else
66
+ errors[k] = nested_value.errors
67
+ end
68
+ end
69
+
70
+ if errors.empty?
71
+ Datacaster.ValidResult(result)
72
+ else
73
+ Datacaster.ErrorResult(errors)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,35 @@
1
+ module Datacaster
2
+ class ThenNode < Base
3
+ def initialize(left, then_caster)
4
+ @left = left
5
+ @then = then_caster
6
+ end
7
+
8
+ def else(else_caster)
9
+ raise ArgumentError.new('Datacaster: double else clause is not permitted') if @else
10
+
11
+ @else = else_caster
12
+ self
13
+ end
14
+
15
+ def call(object)
16
+ unless @else
17
+ raise ArgumentError.new('Datacaster: use "a & b" instead of "a.then(b)" when there is no else-clause')
18
+ end
19
+
20
+ object = super(object)
21
+
22
+ left_result = @left.(object)
23
+
24
+ if left_result.valid?
25
+ @then.(left_result)
26
+ else
27
+ @else.(object)
28
+ end
29
+ end
30
+
31
+ def inspect
32
+ "#<Datacaster::ThenNode Then: #{@then.inspect} Else: #{@else.inspect}>"
33
+ end
34
+ end
35
+ end