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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +59 -0
- data/LICENSE.txt +21 -0
- data/README.md +981 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/datacaster.gemspec +30 -0
- data/lib/datacaster.rb +50 -0
- data/lib/datacaster/absent.rb +19 -0
- data/lib/datacaster/and_node.rb +22 -0
- data/lib/datacaster/and_with_error_aggregation_node.rb +31 -0
- data/lib/datacaster/array_schema.rb +35 -0
- data/lib/datacaster/base.rb +77 -0
- data/lib/datacaster/caster.rb +30 -0
- data/lib/datacaster/checker.rb +26 -0
- data/lib/datacaster/comparator.rb +24 -0
- data/lib/datacaster/hash_mapper.rb +70 -0
- data/lib/datacaster/hash_schema.rb +56 -0
- data/lib/datacaster/or_node.rb +22 -0
- data/lib/datacaster/predefined.rb +179 -0
- data/lib/datacaster/result.rb +61 -0
- data/lib/datacaster/runner_context.rb +21 -0
- data/lib/datacaster/terminator.rb +77 -0
- data/lib/datacaster/then_node.rb +35 -0
- data/lib/datacaster/transformer.rb +21 -0
- data/lib/datacaster/trier.rb +27 -0
- data/lib/datacaster/validator.rb +41 -0
- data/lib/datacaster/version.rb +3 -0
- metadata +138 -0
@@ -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
|