rschema 3.1.1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rschema.rb +13 -5
  3. data/lib/rschema/coercers.rb +2 -0
  4. data/lib/rschema/coercers/any.rb +37 -31
  5. data/lib/rschema/coercers/boolean.rb +33 -23
  6. data/lib/rschema/coercers/chain.rb +38 -32
  7. data/lib/rschema/coercers/date.rb +29 -20
  8. data/lib/rschema/coercers/fixed_hash/default_arrays_to_empty.rb +57 -56
  9. data/lib/rschema/coercers/fixed_hash/default_booleans_to_false.rb +56 -55
  10. data/lib/rschema/coercers/fixed_hash/remove_extraneous_attributes.rb +43 -39
  11. data/lib/rschema/coercers/fixed_hash/symbolize_keys.rb +55 -51
  12. data/lib/rschema/coercers/float.rb +22 -15
  13. data/lib/rschema/coercers/integer.rb +21 -15
  14. data/lib/rschema/coercers/nil_empty_strings.rb +20 -17
  15. data/lib/rschema/coercers/symbol.rb +20 -17
  16. data/lib/rschema/coercers/time.rb +29 -20
  17. data/lib/rschema/coercion_wrapper.rb +25 -26
  18. data/lib/rschema/coercion_wrapper/rack_params.rb +18 -19
  19. data/lib/rschema/dsl.rb +20 -13
  20. data/lib/rschema/error.rb +9 -4
  21. data/lib/rschema/options.rb +5 -0
  22. data/lib/rschema/rails.rb +60 -0
  23. data/lib/rschema/result.rb +9 -11
  24. data/lib/rschema/schemas.rb +2 -0
  25. data/lib/rschema/schemas/anything.rb +23 -24
  26. data/lib/rschema/schemas/boolean.rb +36 -30
  27. data/lib/rschema/schemas/coercer.rb +47 -46
  28. data/lib/rschema/schemas/convenience.rb +122 -123
  29. data/lib/rschema/schemas/enum.rb +41 -34
  30. data/lib/rschema/schemas/fixed_hash.rb +165 -162
  31. data/lib/rschema/schemas/fixed_length_array.rb +66 -58
  32. data/lib/rschema/schemas/maybe.rb +28 -28
  33. data/lib/rschema/schemas/pipeline.rb +35 -35
  34. data/lib/rschema/schemas/predicate.rb +40 -34
  35. data/lib/rschema/schemas/set.rb +57 -48
  36. data/lib/rschema/schemas/sum.rb +31 -34
  37. data/lib/rschema/schemas/type.rb +44 -38
  38. data/lib/rschema/schemas/variable_hash.rb +63 -61
  39. data/lib/rschema/schemas/variable_length_array.rb +57 -51
  40. data/lib/rschema/version.rb +3 -1
  41. metadata +54 -25
@@ -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
- # A schema that matches a values in a given set.
6
- #
7
- # @example Rock-Paper-Scissors values
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
- def initialize(members, subschema)
17
- @members = members
18
- @subschema = subschema
19
- end
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
- def call(value, options)
22
- subresult = subschema.call(value, options)
23
- if subresult.invalid?
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
- def with_wrapped_subschemas(wrapper)
37
- self.class.new(members, wrapper.wrap(subschema))
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
- # A schema that matches `Hash` objects with known keys
6
- #
7
- # @example A typical fixed hash schema
8
- # schema = RSchema.define do
9
- # fixed_hash(
10
- # name: _String,
11
- # optional(:age) => _Integer,
12
- # )
13
- # end
14
- # schema.valid?({ name: "Tom" }) #=> true
15
- # schema.valid?({ name: "Dane", age: 55 }) #=> true
16
- #
17
- class FixedHash
18
- attr_reader :attributes
19
-
20
- def initialize(attributes)
21
- @attributes = attributes
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
- def call(value, options)
25
- return not_a_hash_result(value) unless value.is_a?(Hash)
26
- return missing_attrs_result(value) if missing_keys(value).any?
27
- return extraneous_attrs_result(value) if extraneous_keys(value).any?
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
- subresults = attr_subresults(value, options)
30
- if subresults.values.any?(&:invalid?)
31
- Result.failure(failure_error(subresults))
32
- else
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
- def with_wrapped_subschemas(wrapper)
38
- wrapped_attributes = attributes.map do |attr|
39
- attr.with_wrapped_value_schema(wrapper)
40
- end
43
+ self.class.new(wrapped_attributes)
44
+ end
41
45
 
42
- self.class.new(wrapped_attributes)
43
- end
46
+ def [](attr_key)
47
+ attributes.find { |attr| attr.key == attr_key }
48
+ end
44
49
 
45
- def [](attr_key)
46
- attributes.find{ |attr| attr.key == attr_key }
47
- end
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
- # Creates a new {FixedHash} schema with the given attributes merged in
51
- #
52
- # @param new_attributes [Array<Attribute>] The attributes to merge
53
- # @return [FixedHash] A new schema with the given attributes merged in
54
- #
55
- # @example Merging new attributes into an existing {Schemas::FixedHash} schema
56
- # person_schema = RSchema.define_hash {{
57
- # name: _String,
58
- # age: _Integer,
59
- # }}
60
- # person_schema.valid?(name: "t", age: 5) #=> true
61
- # person_schema.valid?(name: "t", age: 5, id: 3) #=> false
62
- #
63
- # person_with_id_schema = RSchema.define do
64
- # person_schema.merge(attributes(
65
- # id: _Integer,
66
- # ))
67
- # end
68
- # person_with_id_schema.valid?(name: "t", age: 5, id: 3) #=> true
69
- # person_with_id_schema.valid?(name: "t", age: 5) #=> false
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
- # Creates a new {FixedHash} schema with the given attributes removed
82
- #
83
- # @param attribute_keys [Array<Object>] The keys to remove
84
- # @return [FixedHash] A new schema with the given attributes removed
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
- Attribute = Struct.new(:key, :value_schema, :optional) do
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
- private
112
+ def missing_keys(value)
113
+ attributes
114
+ .reject(&:optional)
115
+ .map(&:key)
116
+ .reject { |k| value.key?(k) }
117
+ end
110
118
 
111
- def missing_keys(value)
112
- attributes
113
- .reject(&:optional)
114
- .map(&:key)
115
- .reject{ |k| value.has_key?(k) }
116
- end
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
- def missing_attrs_result(value)
119
- Result.failure(Error.new(
120
- schema: self,
121
- value: value,
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
- def extraneous_keys(value)
130
- allowed_keys = attributes.map(&:key)
131
- value.keys.reject{ |k| allowed_keys.include?(k) }
132
- end
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
- def extraneous_attrs_result(value)
135
- Result.failure(Error.new(
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
- def attr_subresults(value, options)
146
- subresults_by_key = {}
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
- @attributes.map do |attr|
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
- def failure_error(subresults)
160
- subresults
161
- .select{ |_, result| result.invalid? }
162
- .map{ |key, result| [key, result.error] }
163
- .to_h
164
- end
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
- def success_value(subresults)
167
- subresults
168
- .map{ |key, attr_result| [key, attr_result.value] }
169
- .to_h
170
- end
170
+ def success_value(subresults)
171
+ subresults
172
+ .map { |key, attr_result| [key, attr_result.value] }
173
+ .to_h
174
+ end
171
175
 
172
- def not_a_hash_result(value)
173
- Result.failure(
174
- Error.new(
175
- schema: self,
176
- value: value,
177
- symbolic_name: :not_a_hash,
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
- # A schema that represents an array of fixed length
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
- def initialize(subschemas)
19
- @subschemas = subschemas
20
- end
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
- def call(value, options)
23
- unless value.kind_of?(Array)
24
- return Result.failure(Error.new(
25
- symbolic_name: :not_an_array,
26
- schema: self,
27
- value: value,
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
- unless value.size == @subschemas.size
32
- return Result.failure(Error.new(
33
- symbolic_name: :incorrect_size,
34
- schema: self,
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
- validate_value, error = apply_subschemas(value, options)
40
- if error.empty?
41
- Result.success(validate_value)
42
- else
43
- Result.failure(error)
44
- end
45
- end
40
+ private
46
41
 
47
- def with_wrapped_subschemas(wrapper)
48
- wrapped_subschemas = subschemas.map{ |ss| wrapper.wrap(ss) }
49
- self.class.new(wrapped_subschemas)
50
- end
42
+ def apply_subschemas(array, options)
43
+ validated_array = []
44
+ errors = {}
51
45
 
52
- private
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
- def apply_subschemas(array_value, options)
55
- validate_value = []
56
- errors = {}
56
+ [validated_array, errors]
57
+ end
57
58
 
58
- array_value.zip(@subschemas).each_with_index do |(subvalue, subschema), idx|
59
- result = subschema.call(subvalue, options)
60
- if result.valid?
61
- validate_value << result.value
62
- else
63
- errors[idx] = result.error
64
- break if options.fail_fast?
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
- [validate_value, errors]
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