rschema 3.0.1.pre3 → 3.0.1.pre4
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 +4 -4
- data/README.md +278 -330
- data/lib/rschema.rb +104 -17
- data/lib/rschema/coercers.rb +3 -0
- data/lib/rschema/coercers/any.rb +40 -0
- data/lib/rschema/coercers/boolean.rb +30 -0
- data/lib/rschema/coercers/chain.rb +41 -0
- data/lib/rschema/coercers/date.rb +25 -0
- data/lib/rschema/coercers/fixed_hash/default_booleans_to_false.rb +62 -0
- data/lib/rschema/coercers/fixed_hash/remove_extraneous_attributes.rb +42 -0
- data/lib/rschema/coercers/fixed_hash/symbolize_keys.rb +62 -0
- data/lib/rschema/coercers/float.rb +18 -0
- data/lib/rschema/coercers/integer.rb +18 -0
- data/lib/rschema/coercers/symbol.rb +21 -0
- data/lib/rschema/coercers/time.rb +25 -0
- data/lib/rschema/coercion_wrapper.rb +46 -0
- data/lib/rschema/coercion_wrapper/rack_params.rb +21 -0
- data/lib/rschema/dsl.rb +271 -42
- data/lib/rschema/error.rb +12 -30
- data/lib/rschema/options.rb +2 -2
- data/lib/rschema/result.rb +18 -4
- data/lib/rschema/schemas.rb +3 -0
- data/lib/rschema/schemas/anything.rb +14 -12
- data/lib/rschema/schemas/boolean.rb +20 -21
- data/lib/rschema/schemas/coercer.rb +37 -0
- data/lib/rschema/schemas/convenience.rb +53 -0
- data/lib/rschema/schemas/enum.rb +25 -25
- data/lib/rschema/schemas/fixed_hash.rb +110 -91
- data/lib/rschema/schemas/fixed_length_array.rb +48 -48
- data/lib/rschema/schemas/maybe.rb +18 -17
- data/lib/rschema/schemas/pipeline.rb +20 -19
- data/lib/rschema/schemas/predicate.rb +24 -21
- data/lib/rschema/schemas/set.rb +40 -45
- data/lib/rschema/schemas/sum.rb +24 -28
- data/lib/rschema/schemas/type.rb +22 -21
- data/lib/rschema/schemas/variable_hash.rb +53 -52
- data/lib/rschema/schemas/variable_length_array.rb +39 -38
- data/lib/rschema/version.rb +1 -1
- metadata +49 -5
- data/lib/rschema/http_coercer.rb +0 -218
data/lib/rschema/error.rb
CHANGED
@@ -2,45 +2,27 @@ module RSchema
|
|
2
2
|
class Error
|
3
3
|
attr_reader :schema, :value, :symbolic_name, :vars
|
4
4
|
|
5
|
-
def initialize(schema:, value:, symbolic_name:, vars:
|
5
|
+
def initialize(schema:, value:, symbolic_name:, vars: {})
|
6
|
+
raise ArgumentError.new("vars must be a hash") unless vars.is_a?(Hash)
|
7
|
+
|
6
8
|
@schema = schema
|
7
9
|
@value = value
|
8
10
|
@symbolic_name = symbolic_name
|
9
11
|
@vars = vars
|
10
|
-
freeze
|
11
|
-
end
|
12
12
|
|
13
|
-
|
14
|
-
if detailed
|
15
|
-
<<~EOS
|
16
|
-
Error: #{symbolic_name}
|
17
|
-
Schema: #{schema.class.name}
|
18
|
-
Value: #{value.inspect}
|
19
|
-
Vars: #{vars.inspect}
|
20
|
-
EOS
|
21
|
-
else
|
22
|
-
"Error #{schema.class}/#{symbolic_name} for value: #{value.inspect}"
|
23
|
-
end
|
13
|
+
freeze
|
24
14
|
end
|
25
15
|
|
26
|
-
def
|
27
|
-
{
|
28
|
-
schema: schema.class.name,
|
29
|
-
error: symbolic_name.to_s,
|
30
|
-
value: jsonify(value),
|
31
|
-
vars: jsonify(vars),
|
32
|
-
}
|
16
|
+
def to_s
|
17
|
+
"#{schema.class}/#{symbolic_name}"
|
33
18
|
end
|
34
19
|
|
35
|
-
|
20
|
+
def inspect
|
21
|
+
attrs = vars.merge(value: value)
|
22
|
+
.map{ |k, v| "#{k}=#{v.inspect}" }
|
23
|
+
.join(' ')
|
36
24
|
|
37
|
-
|
38
|
-
|
39
|
-
when String, Symbol, Numeric, TrueClass, FalseClass, NilClass then value
|
40
|
-
when Array then value.map{ |element| jsonify(element) }
|
41
|
-
when Hash then value.map{ |k, v| [jsonify(k), jsonify(v)] }.to_h
|
42
|
-
else String(value)
|
43
|
-
end
|
44
|
-
end
|
25
|
+
"<#{self.class} #{to_s} #{attrs}>"
|
26
|
+
end
|
45
27
|
end
|
46
28
|
end
|
data/lib/rschema/options.rb
CHANGED
data/lib/rschema/result.rb
CHANGED
@@ -1,17 +1,26 @@
|
|
1
1
|
module RSchema
|
2
2
|
class Result
|
3
|
-
def self.success(value)
|
4
|
-
|
3
|
+
def self.success(value = nil)
|
4
|
+
if value.nil?
|
5
|
+
NIL_SUCCESS
|
6
|
+
else
|
7
|
+
new(true, value, nil)
|
8
|
+
end
|
5
9
|
end
|
6
10
|
|
7
|
-
def self.failure(error)
|
8
|
-
|
11
|
+
def self.failure(error = nil)
|
12
|
+
if error.nil?
|
13
|
+
NIL_FAILURE
|
14
|
+
else
|
15
|
+
new(false, nil, error)
|
16
|
+
end
|
9
17
|
end
|
10
18
|
|
11
19
|
def initialize(valid, value, error)
|
12
20
|
@valid = valid
|
13
21
|
@value = value
|
14
22
|
@error = error
|
23
|
+
freeze
|
15
24
|
end
|
16
25
|
|
17
26
|
def valid?
|
@@ -35,5 +44,10 @@ module RSchema
|
|
35
44
|
end
|
36
45
|
|
37
46
|
class InvalidError < StandardError; end
|
47
|
+
|
48
|
+
# @!visibility private
|
49
|
+
NIL_SUCCESS = new(true, nil, nil)
|
50
|
+
# @!visibility private
|
51
|
+
NIL_FAILURE = new(false, nil, nil)
|
38
52
|
end
|
39
53
|
end
|
@@ -1,17 +1,19 @@
|
|
1
1
|
module RSchema
|
2
|
-
|
3
|
-
|
4
|
-
def self.instance
|
5
|
-
@instance ||= new
|
6
|
-
end
|
2
|
+
module Schemas
|
3
|
+
class Anything
|
7
4
|
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
def self.instance
|
6
|
+
@instance ||= new
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(value, options)
|
10
|
+
Result.success(value)
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
end
|
13
|
+
def with_wrapped_subschemas(wrapper)
|
14
|
+
self
|
16
15
|
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
17
19
|
end
|
@@ -1,27 +1,26 @@
|
|
1
1
|
module RSchema
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
8
|
-
|
9
|
-
def call(value, options=Options.default)
|
10
|
-
if value.equal?(true) || value.equal?(false)
|
11
|
-
Result.success(value)
|
12
|
-
else
|
13
|
-
Result.failure(Error.new(
|
14
|
-
schema: self,
|
15
|
-
value: value,
|
16
|
-
symbolic_name: :not_a_boolean,
|
17
|
-
))
|
18
|
-
end
|
19
|
-
end
|
2
|
+
module Schemas
|
3
|
+
class Boolean
|
4
|
+
def self.instance
|
5
|
+
@instance ||= new
|
6
|
+
end
|
20
7
|
|
21
|
-
|
22
|
-
|
23
|
-
|
8
|
+
def call(value, options)
|
9
|
+
if value.equal?(true) || value.equal?(false)
|
10
|
+
Result.success(value)
|
11
|
+
else
|
12
|
+
Result.failure(Error.new(
|
13
|
+
schema: self,
|
14
|
+
value: value,
|
15
|
+
symbolic_name: :not_a_boolean,
|
16
|
+
))
|
24
17
|
end
|
18
|
+
end
|
25
19
|
|
20
|
+
def with_wrapped_subschemas(wrapper)
|
21
|
+
self
|
26
22
|
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
27
26
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module RSchema
|
2
|
+
module Schemas
|
3
|
+
class Coercer
|
4
|
+
attr_reader :coercer, :subschema
|
5
|
+
|
6
|
+
def initialize(coercer, subschema)
|
7
|
+
byebug if coercer.is_a?(Array)
|
8
|
+
@coercer = coercer
|
9
|
+
@subschema = subschema
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(value, options)
|
13
|
+
result = coercer.call(value)
|
14
|
+
if result.valid?
|
15
|
+
@subschema.call(result.value, options)
|
16
|
+
else
|
17
|
+
failure(value, result.error)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def with_wrapped_subschemas(wrapper)
|
22
|
+
self.class.new(coercer, wrapper.wrap(subschema))
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def failure(value, name)
|
28
|
+
return Result.failure(Error.new(
|
29
|
+
schema: self,
|
30
|
+
value: value,
|
31
|
+
symbolic_name: name || :coercion_failure,
|
32
|
+
))
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module RSchema
|
4
|
+
module Schemas
|
5
|
+
class Convenience < SimpleDelegator
|
6
|
+
def initialize(raw_schema)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def raw_schema
|
11
|
+
__getobj__
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate(value, options=Options.default)
|
15
|
+
call(value, options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate!(value, options=Options.default)
|
19
|
+
result = call(value, options)
|
20
|
+
if result.valid?
|
21
|
+
result.value
|
22
|
+
else
|
23
|
+
raise RSchema::Invalid.new(result.error)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid?(value)
|
28
|
+
result = call(value, Options.new(fail_fast: true))
|
29
|
+
result.valid?
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.wrap(schema)
|
33
|
+
if schema.is_a?(self)
|
34
|
+
schema
|
35
|
+
else
|
36
|
+
new(schema)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.unwrap(schema)
|
41
|
+
while schema.is_a?(self)
|
42
|
+
schema = schema.raw_schema
|
43
|
+
end
|
44
|
+
schema
|
45
|
+
end
|
46
|
+
|
47
|
+
def with_wrapped_subschemas(wrapper)
|
48
|
+
self.class.new(wrapper.wrap(raw_schema))
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/rschema/schemas/enum.rb
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
module RSchema
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
module Schemas
|
3
|
+
class Enum
|
4
|
+
attr_reader :members, :subschema
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def call(value, options=Options.default)
|
12
|
-
subresult = subschema.call(value, options)
|
13
|
-
if subresult.invalid?
|
14
|
-
subresult
|
15
|
-
elsif members.include?(subresult.value)
|
16
|
-
subresult
|
17
|
-
else
|
18
|
-
Result.failure(Error.new(
|
19
|
-
schema: self,
|
20
|
-
value: subresult.value,
|
21
|
-
symbolic_name: :not_a_member,
|
22
|
-
))
|
23
|
-
end
|
24
|
-
end
|
6
|
+
def initialize(members, subschema)
|
7
|
+
@members = members
|
8
|
+
@subschema = subschema
|
9
|
+
end
|
25
10
|
|
26
|
-
|
27
|
-
|
28
|
-
|
11
|
+
def call(value, options)
|
12
|
+
subresult = subschema.call(value, options)
|
13
|
+
if subresult.invalid?
|
14
|
+
subresult
|
15
|
+
elsif members.include?(subresult.value)
|
16
|
+
subresult
|
17
|
+
else
|
18
|
+
Result.failure(Error.new(
|
19
|
+
schema: self,
|
20
|
+
value: subresult.value,
|
21
|
+
symbolic_name: :not_a_member,
|
22
|
+
))
|
29
23
|
end
|
30
24
|
end
|
25
|
+
|
26
|
+
def with_wrapped_subschemas(wrapper)
|
27
|
+
self.class.new(members, wrapper.wrap(subschema))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
31
|
end
|
@@ -1,118 +1,137 @@
|
|
1
1
|
module RSchema
|
2
|
-
|
2
|
+
module Schemas
|
3
|
+
class FixedHash
|
4
|
+
attr_reader :attributes
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
+
def initialize(attributes)
|
7
|
+
@attributes = attributes
|
8
|
+
end
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
+
def call(value, options)
|
11
|
+
return not_a_hash_result(value) unless value.is_a?(Hash)
|
12
|
+
return missing_attrs_result(value) if missing_keys(value).any?
|
13
|
+
return extraneous_attrs_result(value) if extraneous_keys(value).any?
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
Result.failure(failure_error(subresults))
|
19
|
-
else
|
20
|
-
Result.success(success_value(subresults))
|
21
|
-
end
|
22
|
-
end
|
15
|
+
subresults = attr_subresults(value, options)
|
16
|
+
if subresults.values.any?(&:invalid?)
|
17
|
+
Result.failure(failure_error(subresults))
|
18
|
+
else
|
19
|
+
Result.success(success_value(subresults))
|
20
|
+
end
|
21
|
+
end
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
def with_wrapped_subschemas(wrapper)
|
24
|
+
wrapped_attributes = attributes.map do |attr|
|
25
|
+
attr.with_wrapped_value_schema(wrapper)
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
28
|
+
self.class.new(wrapped_attributes)
|
29
|
+
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
def [](attr_key)
|
32
|
+
attributes.find{ |attr| attr.key == attr_key }
|
33
|
+
end
|
35
34
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
def merge(new_attributes)
|
36
|
+
merged_attrs = (attributes + new_attributes)
|
37
|
+
.map { |attr| [attr.key, attr] }
|
38
|
+
.to_h
|
39
|
+
.values
|
41
40
|
|
42
|
-
|
41
|
+
self.class.new(merged_attrs)
|
42
|
+
end
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
.map(&:key)
|
48
|
-
.reject{ |k| value.has_key?(k) }
|
49
|
-
end
|
44
|
+
def without(attribute_keys)
|
45
|
+
filtered_attrs = attributes
|
46
|
+
.reject { |attr| attribute_keys.include?(attr.key) }
|
50
47
|
|
51
|
-
|
52
|
-
|
53
|
-
schema: self,
|
54
|
-
value: value,
|
55
|
-
symbolic_name: :missing_attributes,
|
56
|
-
vars: missing_keys(value),
|
57
|
-
))
|
58
|
-
end
|
48
|
+
self.class.new(filtered_attrs)
|
49
|
+
end
|
59
50
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
51
|
+
Attribute = Struct.new(:key, :value_schema, :optional) do
|
52
|
+
def with_wrapped_value_schema(wrapper)
|
53
|
+
self.class.new(key, wrapper.wrap(value_schema), optional)
|
54
|
+
end
|
55
|
+
end
|
64
56
|
|
65
|
-
|
66
|
-
Result.failure(Error.new(
|
67
|
-
schema: self,
|
68
|
-
value: value,
|
69
|
-
symbolic_name: :extraneous_attributes,
|
70
|
-
vars: extraneous_keys(value),
|
71
|
-
))
|
72
|
-
end
|
57
|
+
private
|
73
58
|
|
74
|
-
|
75
|
-
|
59
|
+
def missing_keys(value)
|
60
|
+
attributes
|
61
|
+
.reject(&:optional)
|
62
|
+
.map(&:key)
|
63
|
+
.reject{ |k| value.has_key?(k) }
|
64
|
+
end
|
76
65
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
66
|
+
def missing_attrs_result(value)
|
67
|
+
Result.failure(Error.new(
|
68
|
+
schema: self,
|
69
|
+
value: value,
|
70
|
+
symbolic_name: :missing_attributes,
|
71
|
+
vars: {
|
72
|
+
missing_keys: missing_keys(value),
|
73
|
+
}
|
74
|
+
))
|
75
|
+
end
|
84
76
|
|
85
|
-
|
86
|
-
|
77
|
+
def extraneous_keys(value)
|
78
|
+
allowed_keys = attributes.map(&:key)
|
79
|
+
value.keys.reject{ |k| allowed_keys.include?(k) }
|
80
|
+
end
|
87
81
|
|
88
|
-
|
89
|
-
|
82
|
+
def extraneous_attrs_result(value)
|
83
|
+
Result.failure(Error.new(
|
84
|
+
schema: self,
|
85
|
+
value: value,
|
86
|
+
symbolic_name: :extraneous_attributes,
|
87
|
+
vars: {
|
88
|
+
extraneous_keys: extraneous_keys(value),
|
89
|
+
},
|
90
|
+
))
|
91
|
+
end
|
90
92
|
|
91
|
-
|
92
|
-
|
93
|
-
error[key] = attr_result.error
|
94
|
-
end
|
95
|
-
end
|
93
|
+
def attr_subresults(value, options)
|
94
|
+
subresults_by_key = {}
|
96
95
|
|
97
|
-
|
96
|
+
@attributes.map do |attr|
|
97
|
+
if value.has_key?(attr.key)
|
98
|
+
subresult = attr.value_schema.call(value[attr.key], options)
|
99
|
+
subresults_by_key[attr.key] = subresult
|
100
|
+
break if subresult.invalid? && options.fail_fast?
|
98
101
|
end
|
102
|
+
end
|
99
103
|
|
100
|
-
|
101
|
-
|
102
|
-
.map{ |key, attr_result| [key, attr_result.value] }
|
103
|
-
.to_h
|
104
|
-
end
|
104
|
+
subresults_by_key
|
105
|
+
end
|
105
106
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
)
|
113
|
-
)
|
107
|
+
def failure_error(results)
|
108
|
+
error = {}
|
109
|
+
|
110
|
+
results.each do |key, attr_result|
|
111
|
+
if attr_result.invalid?
|
112
|
+
error[key] = attr_result.error
|
114
113
|
end
|
115
114
|
end
|
116
115
|
|
116
|
+
error
|
117
|
+
end
|
118
|
+
|
119
|
+
def success_value(subresults)
|
120
|
+
subresults
|
121
|
+
.map{ |key, attr_result| [key, attr_result.value] }
|
122
|
+
.to_h
|
123
|
+
end
|
124
|
+
|
125
|
+
def not_a_hash_result(value)
|
126
|
+
Result.failure(
|
127
|
+
Error.new(
|
128
|
+
schema: self,
|
129
|
+
value: value,
|
130
|
+
symbolic_name: :not_a_hash,
|
131
|
+
)
|
132
|
+
)
|
117
133
|
end
|
118
134
|
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|