rschema 3.1.1 → 3.2.0
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/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
|