datacaster 0.9.1

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