rschema 3.1.1 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rschema.rb +13 -5
- data/lib/rschema/coercers.rb +2 -0
- data/lib/rschema/coercers/any.rb +37 -31
- data/lib/rschema/coercers/boolean.rb +33 -23
- data/lib/rschema/coercers/chain.rb +38 -32
- data/lib/rschema/coercers/date.rb +29 -20
- data/lib/rschema/coercers/fixed_hash/default_arrays_to_empty.rb +57 -56
- data/lib/rschema/coercers/fixed_hash/default_booleans_to_false.rb +56 -55
- data/lib/rschema/coercers/fixed_hash/remove_extraneous_attributes.rb +43 -39
- data/lib/rschema/coercers/fixed_hash/symbolize_keys.rb +55 -51
- data/lib/rschema/coercers/float.rb +22 -15
- data/lib/rschema/coercers/integer.rb +21 -15
- data/lib/rschema/coercers/nil_empty_strings.rb +20 -17
- data/lib/rschema/coercers/symbol.rb +20 -17
- data/lib/rschema/coercers/time.rb +29 -20
- data/lib/rschema/coercion_wrapper.rb +25 -26
- data/lib/rschema/coercion_wrapper/rack_params.rb +18 -19
- data/lib/rschema/dsl.rb +20 -13
- data/lib/rschema/error.rb +9 -4
- data/lib/rschema/options.rb +5 -0
- data/lib/rschema/rails.rb +60 -0
- data/lib/rschema/result.rb +9 -11
- data/lib/rschema/schemas.rb +2 -0
- data/lib/rschema/schemas/anything.rb +23 -24
- data/lib/rschema/schemas/boolean.rb +36 -30
- data/lib/rschema/schemas/coercer.rb +47 -46
- data/lib/rschema/schemas/convenience.rb +122 -123
- data/lib/rschema/schemas/enum.rb +41 -34
- data/lib/rschema/schemas/fixed_hash.rb +165 -162
- data/lib/rschema/schemas/fixed_length_array.rb +66 -58
- data/lib/rschema/schemas/maybe.rb +28 -28
- data/lib/rschema/schemas/pipeline.rb +35 -35
- data/lib/rschema/schemas/predicate.rb +40 -34
- data/lib/rschema/schemas/set.rb +57 -48
- data/lib/rschema/schemas/sum.rb +31 -34
- data/lib/rschema/schemas/type.rb +44 -38
- data/lib/rschema/schemas/variable_hash.rb +63 -61
- data/lib/rschema/schemas/variable_length_array.rb +57 -51
- data/lib/rschema/version.rb +3 -1
- metadata +54 -25
data/lib/rschema/schemas/enum.rb
CHANGED
@@ -1,41 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module RSchema
|
2
|
-
module Schemas
|
4
|
+
module Schemas
|
5
|
+
#
|
6
|
+
# A schema that matches a values in a given set.
|
7
|
+
#
|
8
|
+
# @example Rock-Paper-Scissors values
|
9
|
+
# schema = RSchema.define { enum([:rock, :paper, :scissors]) }
|
10
|
+
# schema.valid?(:rock) #=> true
|
11
|
+
# schema.valid?(:paper) #=> true
|
12
|
+
# schema.valid?(:gun) #=> false
|
13
|
+
#
|
14
|
+
class Enum
|
15
|
+
attr_reader :members, :subschema
|
3
16
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
# schema = RSchema.define { enum([:rock, :paper, :scissors]) }
|
9
|
-
# schema.valid?(:rock) #=> true
|
10
|
-
# schema.valid?(:paper) #=> true
|
11
|
-
# schema.valid?(:gun) #=> false
|
12
|
-
#
|
13
|
-
class Enum
|
14
|
-
attr_reader :members, :subschema
|
17
|
+
def initialize(members, subschema)
|
18
|
+
@members = members
|
19
|
+
@subschema = subschema
|
20
|
+
end
|
15
21
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
22
|
+
def call(value, options)
|
23
|
+
subresult = subschema.call(value, options)
|
24
|
+
if subresult.invalid?
|
25
|
+
subresult
|
26
|
+
elsif members.include?(subresult.value)
|
27
|
+
subresult
|
28
|
+
else
|
29
|
+
Result.failure(error(subresult.value))
|
30
|
+
end
|
31
|
+
end
|
20
32
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
subresult
|
25
|
-
elsif members.include?(subresult.value)
|
26
|
-
subresult
|
27
|
-
else
|
28
|
-
Result.failure(Error.new(
|
29
|
-
schema: self,
|
30
|
-
value: subresult.value,
|
31
|
-
symbolic_name: :not_a_member,
|
32
|
-
))
|
33
|
-
end
|
34
|
-
end
|
33
|
+
def with_wrapped_subschemas(wrapper)
|
34
|
+
self.class.new(members, wrapper.wrap(subschema))
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
private
|
38
|
+
|
39
|
+
def error(value)
|
40
|
+
Error.new(
|
41
|
+
schema: self,
|
42
|
+
value: value,
|
43
|
+
symbolic_name: :not_a_member,
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
38
47
|
end
|
39
48
|
end
|
40
|
-
end
|
41
|
-
end
|
@@ -1,184 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module RSchema
|
2
|
-
module Schemas
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# schema.valid?({ name: "
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
4
|
+
module Schemas
|
5
|
+
#
|
6
|
+
# A schema that matches `Hash` objects with known keys
|
7
|
+
#
|
8
|
+
# @example A typical fixed hash schema
|
9
|
+
# schema = RSchema.define do
|
10
|
+
# fixed_hash(
|
11
|
+
# name: _String,
|
12
|
+
# optional(:age) => _Integer,
|
13
|
+
# )
|
14
|
+
# end
|
15
|
+
# schema.valid?({ name: "Tom" }) #=> true
|
16
|
+
# schema.valid?({ name: "Dane", age: 55 }) #=> true
|
17
|
+
#
|
18
|
+
class FixedHash
|
19
|
+
attr_reader :attributes
|
20
|
+
|
21
|
+
def initialize(attributes)
|
22
|
+
@attributes = attributes
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
def call(value, options)
|
26
|
+
return not_a_hash_result(value) unless value.is_a?(Hash)
|
27
|
+
return missing_attrs_result(value) if missing_keys(value).any?
|
28
|
+
return extraneous_attrs_result(value) if extraneous_keys(value).any?
|
29
|
+
|
30
|
+
subresults = attr_subresults(value, options)
|
31
|
+
if subresults.values.any?(&:invalid?)
|
32
|
+
Result.failure(failure_error(subresults))
|
33
|
+
else
|
34
|
+
Result.success(success_value(subresults))
|
35
|
+
end
|
36
|
+
end
|
28
37
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
Result.success(success_value(subresults))
|
34
|
-
end
|
35
|
-
end
|
38
|
+
def with_wrapped_subschemas(wrapper)
|
39
|
+
wrapped_attributes = attributes.map do |attr|
|
40
|
+
attr.with_wrapped_value_schema(wrapper)
|
41
|
+
end
|
36
42
|
|
37
|
-
|
38
|
-
|
39
|
-
attr.with_wrapped_value_schema(wrapper)
|
40
|
-
end
|
43
|
+
self.class.new(wrapped_attributes)
|
44
|
+
end
|
41
45
|
|
42
|
-
|
43
|
-
|
46
|
+
def [](attr_key)
|
47
|
+
attributes.find { |attr| attr.key == attr_key }
|
48
|
+
end
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
|
50
|
+
#
|
51
|
+
# Creates a new {FixedHash} schema with the given attributes merged in
|
52
|
+
#
|
53
|
+
# @param new_attributes [Array<Attribute>] The attributes to merge
|
54
|
+
# @return [FixedHash] A new schema with the given attributes merged in
|
55
|
+
#
|
56
|
+
# @example Merging new attributes into an existing {Schemas::FixedHash}
|
57
|
+
# person_schema = RSchema.define_hash {{
|
58
|
+
# name: _String,
|
59
|
+
# age: _Integer,
|
60
|
+
# }}
|
61
|
+
# person_schema.valid?(name: "t", age: 5) #=> true
|
62
|
+
# person_schema.valid?(name: "t", age: 5, id: 3) #=> false
|
63
|
+
#
|
64
|
+
# person_with_id_schema = RSchema.define do
|
65
|
+
# person_schema.merge(attributes(
|
66
|
+
# id: _Integer,
|
67
|
+
# ))
|
68
|
+
# end
|
69
|
+
# person_with_id_schema.valid?(name: "t", age: 5, id: 3) #=> true
|
70
|
+
# person_with_id_schema.valid?(name: "t", age: 5) #=> false
|
71
|
+
#
|
72
|
+
def merge(new_attributes)
|
73
|
+
merged_attrs = (attributes + new_attributes)
|
74
|
+
.map { |attr| [attr.key, attr] }
|
75
|
+
.to_h
|
76
|
+
.values
|
77
|
+
|
78
|
+
self.class.new(merged_attrs)
|
79
|
+
end
|
48
80
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
def merge(new_attributes)
|
72
|
-
merged_attrs = (attributes + new_attributes)
|
73
|
-
.map { |attr| [attr.key, attr] }
|
74
|
-
.to_h
|
75
|
-
.values
|
76
|
-
|
77
|
-
self.class.new(merged_attrs)
|
78
|
-
end
|
81
|
+
#
|
82
|
+
# Creates a new {FixedHash} schema with the given attributes removed
|
83
|
+
#
|
84
|
+
# @param attribute_keys [Array<Object>] The keys to remove
|
85
|
+
# @return [FixedHash] A new schema with the given attributes removed
|
86
|
+
#
|
87
|
+
# @example Removing an attribute
|
88
|
+
# cat_and_dog = RSchema.define_hash {{
|
89
|
+
# dog: _String,
|
90
|
+
# cat: _String,
|
91
|
+
# }}
|
92
|
+
#
|
93
|
+
# only_cat = RSchema.define { cat_and_dog.without(:dog) }
|
94
|
+
# only_cat.valid?({ cat: 'meow' }) #=> true
|
95
|
+
# only_cat.valid?({ cat: 'meow', dog: 'woof' }) #=> false
|
96
|
+
#
|
97
|
+
def without(attribute_keys)
|
98
|
+
filtered_attrs = attributes
|
99
|
+
.reject { |attr| attribute_keys.include?(attr.key) }
|
100
|
+
|
101
|
+
self.class.new(filtered_attrs)
|
102
|
+
end
|
79
103
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
#
|
86
|
-
# @example Removing an attribute
|
87
|
-
# cat_and_dog = RSchema.define_hash {{
|
88
|
-
# dog: _String,
|
89
|
-
# cat: _String,
|
90
|
-
# }}
|
91
|
-
#
|
92
|
-
# only_cat = RSchema.define { cat_and_dog.without(:dog) }
|
93
|
-
# only_cat.valid?({ cat: 'meow' }) #=> true
|
94
|
-
# only_cat.valid?({ cat: 'meow', dog: 'woof' }) #=> false
|
95
|
-
#
|
96
|
-
def without(attribute_keys)
|
97
|
-
filtered_attrs = attributes
|
98
|
-
.reject { |attr| attribute_keys.include?(attr.key) }
|
99
|
-
|
100
|
-
self.class.new(filtered_attrs)
|
101
|
-
end
|
104
|
+
Attribute = Struct.new(:key, :value_schema, :optional) do
|
105
|
+
def with_wrapped_value_schema(wrapper)
|
106
|
+
self.class.new(key, wrapper.wrap(value_schema), optional)
|
107
|
+
end
|
108
|
+
end
|
102
109
|
|
103
|
-
|
104
|
-
def with_wrapped_value_schema(wrapper)
|
105
|
-
self.class.new(key, wrapper.wrap(value_schema), optional)
|
106
|
-
end
|
107
|
-
end
|
110
|
+
private
|
108
111
|
|
109
|
-
|
112
|
+
def missing_keys(value)
|
113
|
+
attributes
|
114
|
+
.reject(&:optional)
|
115
|
+
.map(&:key)
|
116
|
+
.reject { |k| value.key?(k) }
|
117
|
+
end
|
110
118
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
119
|
+
def missing_attrs_result(value)
|
120
|
+
Result.failure(
|
121
|
+
Error.new(
|
122
|
+
schema: self,
|
123
|
+
value: value,
|
124
|
+
symbolic_name: :missing_attributes,
|
125
|
+
vars: {
|
126
|
+
missing_keys: missing_keys(value)
|
127
|
+
},
|
128
|
+
),
|
129
|
+
)
|
130
|
+
end
|
117
131
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
symbolic_name: :missing_attributes,
|
123
|
-
vars: {
|
124
|
-
missing_keys: missing_keys(value),
|
125
|
-
}
|
126
|
-
))
|
127
|
-
end
|
132
|
+
def extraneous_keys(value)
|
133
|
+
allowed_keys = attributes.map(&:key)
|
134
|
+
value.keys.reject { |k| allowed_keys.include?(k) }
|
135
|
+
end
|
128
136
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
137
|
+
def extraneous_attrs_result(value)
|
138
|
+
Result.failure(
|
139
|
+
Error.new(
|
140
|
+
schema: self,
|
141
|
+
value: value,
|
142
|
+
symbolic_name: :extraneous_attributes,
|
143
|
+
vars: {
|
144
|
+
extraneous_keys: extraneous_keys(value)
|
145
|
+
},
|
146
|
+
),
|
147
|
+
)
|
148
|
+
end
|
133
149
|
|
134
|
-
|
135
|
-
|
136
|
-
schema: self,
|
137
|
-
value: value,
|
138
|
-
symbolic_name: :extraneous_attributes,
|
139
|
-
vars: {
|
140
|
-
extraneous_keys: extraneous_keys(value),
|
141
|
-
},
|
142
|
-
))
|
143
|
-
end
|
150
|
+
def attr_subresults(value, options)
|
151
|
+
subresults_by_key = {}
|
144
152
|
|
145
|
-
|
146
|
-
|
153
|
+
@attributes.map do |attr|
|
154
|
+
next unless value.key?(attr.key)
|
155
|
+
subresult = attr.value_schema.call(value[attr.key], options)
|
156
|
+
subresults_by_key[attr.key] = subresult
|
157
|
+
break if subresult.invalid? && options.fail_fast?
|
158
|
+
end
|
147
159
|
|
148
|
-
|
149
|
-
if value.has_key?(attr.key)
|
150
|
-
subresult = attr.value_schema.call(value[attr.key], options)
|
151
|
-
subresults_by_key[attr.key] = subresult
|
152
|
-
break if subresult.invalid? && options.fail_fast?
|
160
|
+
subresults_by_key
|
153
161
|
end
|
154
|
-
end
|
155
|
-
|
156
|
-
subresults_by_key
|
157
|
-
end
|
158
162
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
163
|
+
def failure_error(subresults)
|
164
|
+
subresults
|
165
|
+
.select { |_, result| result.invalid? }
|
166
|
+
.map { |key, result| [key, result.error] }
|
167
|
+
.to_h
|
168
|
+
end
|
165
169
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
170
|
+
def success_value(subresults)
|
171
|
+
subresults
|
172
|
+
.map { |key, attr_result| [key, attr_result.value] }
|
173
|
+
.to_h
|
174
|
+
end
|
171
175
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
176
|
+
def not_a_hash_result(value)
|
177
|
+
Result.failure(
|
178
|
+
Error.new(
|
179
|
+
schema: self,
|
180
|
+
value: value,
|
181
|
+
symbolic_name: :not_a_hash,
|
182
|
+
),
|
183
|
+
)
|
184
|
+
end
|
185
|
+
end
|
180
186
|
end
|
181
187
|
end
|
182
|
-
|
183
|
-
end
|
184
|
-
end
|
@@ -1,72 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module RSchema
|
2
|
-
module Schemas
|
4
|
+
module Schemas
|
5
|
+
#
|
6
|
+
# A schema that represents an array of fixed length
|
7
|
+
#
|
8
|
+
# Each element in the fixed-length array has its own subschema
|
9
|
+
#
|
10
|
+
# @example A fixed-length array schema
|
11
|
+
# schema = RSchema.define { array(_Integer, _String) }
|
12
|
+
# schema.valid?([5, "hello"]) #=> true
|
13
|
+
# schema.valid?([5]) #=> false
|
14
|
+
# schema.valid?([5, "hello", "world"]) #=> false
|
15
|
+
#
|
16
|
+
class FixedLengthArray
|
17
|
+
attr_reader :subschemas
|
3
18
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
# Each element in the fixed-length array has its own subschema
|
8
|
-
#
|
9
|
-
# @example A fixed-length array schema
|
10
|
-
# schema = RSchema.define { array(_Integer, _String) }
|
11
|
-
# schema.valid?([5, "hello"]) #=> true
|
12
|
-
# schema.valid?([5]) #=> false
|
13
|
-
# schema.valid?([5, "hello", "world"]) #=> false
|
14
|
-
#
|
15
|
-
class FixedLengthArray
|
16
|
-
attr_reader :subschemas
|
19
|
+
def initialize(subschemas)
|
20
|
+
@subschemas = subschemas
|
21
|
+
end
|
17
22
|
|
18
|
-
|
19
|
-
|
20
|
-
|
23
|
+
def call(value, options)
|
24
|
+
return type_failure(value) unless value.is_a?(Array)
|
25
|
+
return size_failure(value) unless value.size == @subschemas.size
|
21
26
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
27
|
+
validated_array, error = apply_subschemas(value, options)
|
28
|
+
if error.empty?
|
29
|
+
Result.success(validated_array)
|
30
|
+
else
|
31
|
+
Result.failure(error)
|
32
|
+
end
|
33
|
+
end
|
30
34
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
value: value,
|
36
|
-
))
|
37
|
-
end
|
35
|
+
def with_wrapped_subschemas(wrapper)
|
36
|
+
wrapped_subschemas = subschemas.map { |ss| wrapper.wrap(ss) }
|
37
|
+
self.class.new(wrapped_subschemas)
|
38
|
+
end
|
38
39
|
|
39
|
-
|
40
|
-
if error.empty?
|
41
|
-
Result.success(validate_value)
|
42
|
-
else
|
43
|
-
Result.failure(error)
|
44
|
-
end
|
45
|
-
end
|
40
|
+
private
|
46
41
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
42
|
+
def apply_subschemas(array, options)
|
43
|
+
validated_array = []
|
44
|
+
errors = {}
|
51
45
|
|
52
|
-
|
46
|
+
array.zip(@subschemas).each_with_index do |(subvalue, subschema), idx|
|
47
|
+
result = subschema.call(subvalue, options)
|
48
|
+
if result.valid?
|
49
|
+
validated_array << result.value
|
50
|
+
else
|
51
|
+
errors[idx] = result.error
|
52
|
+
break if options.fail_fast?
|
53
|
+
end
|
54
|
+
end
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
-
errors = {}
|
56
|
+
[validated_array, errors]
|
57
|
+
end
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
59
|
+
def type_failure(value)
|
60
|
+
Result.failure(
|
61
|
+
Error.new(
|
62
|
+
symbolic_name: :not_an_array,
|
63
|
+
schema: self,
|
64
|
+
value: value,
|
65
|
+
),
|
66
|
+
)
|
65
67
|
end
|
66
|
-
end
|
67
68
|
|
68
|
-
|
69
|
+
def size_failure(value)
|
70
|
+
Result.failure(
|
71
|
+
Error.new(
|
72
|
+
symbolic_name: :incorrect_size,
|
73
|
+
schema: self,
|
74
|
+
value: value,
|
75
|
+
),
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
69
79
|
end
|
70
80
|
end
|
71
|
-
end
|
72
|
-
end
|