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.
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