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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +278 -330
  3. data/lib/rschema.rb +104 -17
  4. data/lib/rschema/coercers.rb +3 -0
  5. data/lib/rschema/coercers/any.rb +40 -0
  6. data/lib/rschema/coercers/boolean.rb +30 -0
  7. data/lib/rschema/coercers/chain.rb +41 -0
  8. data/lib/rschema/coercers/date.rb +25 -0
  9. data/lib/rschema/coercers/fixed_hash/default_booleans_to_false.rb +62 -0
  10. data/lib/rschema/coercers/fixed_hash/remove_extraneous_attributes.rb +42 -0
  11. data/lib/rschema/coercers/fixed_hash/symbolize_keys.rb +62 -0
  12. data/lib/rschema/coercers/float.rb +18 -0
  13. data/lib/rschema/coercers/integer.rb +18 -0
  14. data/lib/rschema/coercers/symbol.rb +21 -0
  15. data/lib/rschema/coercers/time.rb +25 -0
  16. data/lib/rschema/coercion_wrapper.rb +46 -0
  17. data/lib/rschema/coercion_wrapper/rack_params.rb +21 -0
  18. data/lib/rschema/dsl.rb +271 -42
  19. data/lib/rschema/error.rb +12 -30
  20. data/lib/rschema/options.rb +2 -2
  21. data/lib/rschema/result.rb +18 -4
  22. data/lib/rschema/schemas.rb +3 -0
  23. data/lib/rschema/schemas/anything.rb +14 -12
  24. data/lib/rschema/schemas/boolean.rb +20 -21
  25. data/lib/rschema/schemas/coercer.rb +37 -0
  26. data/lib/rschema/schemas/convenience.rb +53 -0
  27. data/lib/rschema/schemas/enum.rb +25 -25
  28. data/lib/rschema/schemas/fixed_hash.rb +110 -91
  29. data/lib/rschema/schemas/fixed_length_array.rb +48 -48
  30. data/lib/rschema/schemas/maybe.rb +18 -17
  31. data/lib/rschema/schemas/pipeline.rb +20 -19
  32. data/lib/rschema/schemas/predicate.rb +24 -21
  33. data/lib/rschema/schemas/set.rb +40 -45
  34. data/lib/rschema/schemas/sum.rb +24 -28
  35. data/lib/rschema/schemas/type.rb +22 -21
  36. data/lib/rschema/schemas/variable_hash.rb +53 -52
  37. data/lib/rschema/schemas/variable_length_array.rb +39 -38
  38. data/lib/rschema/version.rb +1 -1
  39. metadata +49 -5
  40. data/lib/rschema/http_coercer.rb +0 -218
@@ -0,0 +1,18 @@
1
+ module RSchema
2
+ module Coercers
3
+
4
+ module Float
5
+ extend self
6
+
7
+ def build(schema)
8
+ self
9
+ end
10
+
11
+ def call(value)
12
+ flt = Float(value) rescue nil
13
+ flt ? Result.success(flt) : Result.failure
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module RSchema
2
+ module Coercers
3
+
4
+ module Integer
5
+ extend self
6
+
7
+ def build(schema)
8
+ self
9
+ end
10
+
11
+ def call(value)
12
+ int = Integer(value) rescue nil
13
+ int ? Result.success(int) : Result.failure
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module RSchema
2
+ module Coercers
3
+
4
+ module Symbol
5
+ extend self
6
+
7
+ def build(schema)
8
+ self
9
+ end
10
+
11
+ def call(value)
12
+ case value
13
+ when ::Symbol then Result.success(value)
14
+ when ::String then Result.success(value.to_sym)
15
+ else Result.failure
16
+ end
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ module RSchema
2
+ module Coercers
3
+
4
+ module Time
5
+ extend self
6
+
7
+ def build(schema)
8
+ self
9
+ end
10
+
11
+ def call(value)
12
+ case value
13
+ when ::Time
14
+ Result.success(value)
15
+ when ::String
16
+ time = ::Time.parse(value) rescue nil
17
+ time ? Result.success(time) : Result.failure
18
+ else
19
+ Result.failure
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,46 @@
1
+ module RSchema
2
+ class CoercionWrapper
3
+ def initialize(&initializer)
4
+ @builder_by_schema = {}
5
+ @builder_by_type = {}
6
+ instance_eval(&initializer) if initializer
7
+ end
8
+
9
+ def coerce(schema_type, with:)
10
+ @builder_by_schema[schema_type] = with
11
+ end
12
+
13
+ def coerce_type(type, with:)
14
+ @builder_by_type[type] = with
15
+ end
16
+
17
+ def wrap(schema)
18
+ wrapped_schema = schema.with_wrapped_subschemas(self)
19
+ wrap_with_coercer(wrapped_schema)
20
+ end
21
+
22
+ private
23
+
24
+ def builder_for_schema(schema)
25
+ @builder_by_schema.fetch(schema.class) do
26
+ if schema.is_a?(Schemas::Type)
27
+ @builder_by_type.fetch(schema.type, nil)
28
+ else
29
+ nil
30
+ end
31
+ end
32
+ end
33
+
34
+ def wrap_with_coercer(schema)
35
+ builder = builder_for_schema(schema)
36
+ if builder
37
+ coercer = builder.build(schema)
38
+ Schemas::Coercer.new(coercer, schema)
39
+ else
40
+ schema
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+
@@ -0,0 +1,21 @@
1
+ module RSchema
2
+ class CoercionWrapper
3
+
4
+ RACK_PARAMS = CoercionWrapper.new do
5
+ coerce_type Symbol, with: Coercers::Symbol
6
+ coerce_type Integer, with: Coercers::Integer
7
+ coerce_type Float, with: Coercers::Float
8
+ coerce_type Time, with: Coercers::Time
9
+ coerce_type Date, with: Coercers::Date
10
+
11
+ coerce Schemas::Boolean, with: Coercers::Boolean
12
+ coerce Schemas::FixedHash, with: Coercers::Chain[
13
+ Coercers::FixedHash::SymbolizeKeys,
14
+ Coercers::FixedHash::RemoveExtraneousAttributes,
15
+ Coercers::FixedHash::DefaultBooleansToFalse,
16
+ ]
17
+ end
18
+
19
+ end
20
+ end
21
+
@@ -1,83 +1,318 @@
1
1
  module RSchema
2
+ #
3
+ # A mixin containing all the standard RSchema DSL methods.
4
+ #
5
+ # If you are making a custom DSL, you can include this mixin to get ONLY the
6
+ # standard RSchema DSL methods, without any of the extra ones that may have
7
+ # been included by third-party gems.
8
+ #
9
+ # @note Do not include your custom DSL methods into this module.
10
+ # Include them into the {DefaultDSL} class instead.
11
+ #
12
+ # @see RSchema.define
13
+ # @see RSchema.default_dsl
14
+ #
2
15
  module DSL
16
+ OptionalWrapper = Struct.new(:key)
17
+
18
+ #
19
+ # Creates a {Schemas::Type} schema.
20
+ #
21
+ # The preferred way to create type schemas is using an underscore, like:
22
+ #
23
+ # _Integer
24
+ #
25
+ # The DSL will turn the above code into:
26
+ #
27
+ # type(Integer)
28
+ #
29
+ # Underscores will not work for namespaced types (types that include `::`).
30
+ # In that case, it is necessary to use the `type` method:
31
+ #
32
+ # _MyNamespace::MyType # this will NOT work
33
+ # type(MyNamespace::MyType) # this will work
34
+ #
35
+ # @param type [Class]
36
+ # @return [Schemas::Type]
37
+ #
38
+ # @example An `Integer` type schema
39
+ # type(Integer)
40
+ # # exactly the same as:
41
+ # _Integer
42
+ #
3
43
  def type(type)
4
44
  Schemas::Type.new(type)
5
45
  end
6
- alias_method :_, :type
7
46
 
8
- def Array(*subchemas)
9
- if subchemas.count == 1
10
- Schemas::VariableLengthArray.new(subchemas.first)
47
+ #
48
+ # Creates a {Schemas::VariableLengthArray} if given one argument, otherwise
49
+ # creates a {Schemas::FixedLengthArray}
50
+ #
51
+ # @param subschemas [Array<schema>] one or more schema objects representing elements
52
+ # in the array.
53
+ # @return [Schemas::VariableLengthArray, Schemas::FixedLengthArray]
54
+ #
55
+ # @example A variable-length array schema
56
+ # array(_Integer)
57
+ # # matches [1, 2, 3, 4]
58
+ #
59
+ # @example A fixed-length array schema
60
+ # array(_Integer, _String)
61
+ # # matches [5, "hello"]
62
+ #
63
+ def array(*subschemas)
64
+ subschemas = subschemas.map{ |ss| inconvenience(ss) }
65
+
66
+ if subschemas.count == 1
67
+ Schemas::VariableLengthArray.new(subschemas.first)
11
68
  else
12
- Schemas::FixedLengthArray.new(subchemas)
69
+ Schemas::FixedLengthArray.new(subschemas)
13
70
  end
14
71
  end
15
72
 
16
- def Boolean
73
+ #
74
+ # Returns the {Schemas::Boolean} schema
75
+ #
76
+ # @return [Schemas::Boolean]
77
+ #
78
+ # @example The boolean schema
79
+ # boolean
80
+ # # matches only `true` and `false`
81
+ #
82
+ def boolean
17
83
  Schemas::Boolean.instance
18
84
  end
19
85
 
20
- def Hash(attribute_hash)
21
- Schemas::FixedHash.new(__fixed_hash_attributes(attribute_hash))
22
- end
23
-
24
- def Hash_based_on(preexisting_hash_schema, new_attributes_hash)
25
- old_attrs = preexisting_hash_schema.attributes
26
- .map{ |attr| [attr.key, attr] }
27
- .to_h
28
-
29
- new_attrs = __fixed_hash_attributes(new_attributes_hash)
30
- .map{ |attr| [attr.key, attr] }
31
- .to_h
32
-
33
- merged_attrs = old_attrs.merge(new_attrs)
34
- .values
35
- .reject{ |attr| attr.value_schema.nil? }
36
-
37
- Schemas::FixedHash.new(merged_attrs)
86
+ #
87
+ # Creates a {Schemas::FixedHash} schema
88
+ #
89
+ # @param attribute_hash (see #attributes)
90
+ # @return [Schemas::FixedHash]
91
+ #
92
+ # @example A typical fixed hash schema
93
+ # fixed_hash(
94
+ # name: _String,
95
+ # optional(:age) => _Integer,
96
+ # )
97
+ # # matches { name: "Tom" }
98
+ # # matches { name: "Dane", age: 55 }
99
+ #
100
+ def fixed_hash(attribute_hash)
101
+ Schemas::FixedHash.new(attributes(attribute_hash))
38
102
  end
103
+ alias_method :hash, :fixed_hash
39
104
 
40
- def Set(subschema)
41
- Schemas::Set.new(subschema)
105
+ #
106
+ # Creates a {Schemas::Set} schema
107
+ #
108
+ # @param subschema [schema] A schema representing the elements of the set
109
+ # @return [Schemas::Set]
110
+ #
111
+ # @example A set of integers
112
+ # set(_Integer)
113
+ # # matches Set[1,2,3]
114
+ #
115
+ def set(subschema)
116
+ Schemas::Set.new(inconvenience(subschema))
42
117
  end
43
118
 
119
+ #
120
+ # Wraps a key in an {OptionalWrapper}, for use with the {#fixed_hash} or
121
+ # {#attributes} methods.
122
+ #
123
+ # @param key [Object] Any arbitrary value
124
+ # @return [OptionalWrapper] An {OptionalWrapper} containing the given key.
125
+ #
126
+ # @see OptionalWrapper
127
+ # @see #fixed_hash
128
+ # @see #attributes
129
+ #
130
+ # @example (see #fixed_hash)
131
+ #
44
132
  def optional(key)
45
133
  OptionalWrapper.new(key)
46
134
  end
47
135
 
48
- def VariableHash(subschemas)
136
+ #
137
+ # Creates a {Schemas::VariableHash} schema
138
+ #
139
+ # @param subschemas [Hash] A hash with a single key, and a single value.
140
+ # The key is a schema representing all keys.
141
+ # The value is a schema representing all values.
142
+ # @return [Schemas::VariableHash]
143
+ #
144
+ # @example A hash of integers to strings
145
+ # variable_hash(_Integer => _String)
146
+ # # matches { 5 => "hello", 7 => "world" }
147
+ #
148
+ def variable_hash(subschemas)
49
149
  unless subschemas.is_a?(Hash) && subschemas.size == 1
50
150
  raise ArgumentError, 'argument must be a Hash of size 1'
51
151
  end
52
152
 
53
153
  key_schema, value_schema = subschemas.first
54
- Schemas::VariableHash.new(key_schema, value_schema)
154
+ Schemas::VariableHash.new(
155
+ inconvenience(key_schema),
156
+ inconvenience(value_schema),
157
+ )
158
+ end
159
+
160
+ #
161
+ # Turns an "attribute hash" into an array of {Schemas::FixedHash::Attribute}.
162
+ # Primarily for use with {Schemas::FixedHash#merge}.
163
+ #
164
+ # @param attribute_hash [Hash<key, schema>] A hash of keys to subschemas.
165
+ # The values of this parameter must be schema objects.
166
+ # The keys should be the exact keys expected in the represented `Hash`
167
+ # (`Strings`, `Symbols`, whatever). Keys can be wrapped with {#optional}
168
+ # to indicate that the key can be missing in the represented `Hash`.
169
+ # @return [Array<Schemas::FixedHash::Attribute>]
170
+ #
171
+ # @see Schemas::FixedHash#merge
172
+ #
173
+ # @example Merging new attributes into an existing {Schemas::FixedHash} schema
174
+ # person_schema = fixed_hash(
175
+ # first_name: _String,
176
+ # last_name: _String,
177
+ # )
178
+ #
179
+ # person_record_schema = person_schema.merge(attributes(
180
+ # id: _Integer,
181
+ # optional(:updated_at) => _Time,
182
+ # ))
183
+ #
184
+ # # person_record_schema matches:
185
+ # # {
186
+ # # id: 3,
187
+ # # updated_at: Time.now,
188
+ # # first_name: "Tom",
189
+ # # last_name: "Dalling",
190
+ # # }
191
+ #
192
+ def attributes(attribute_hash)
193
+ attribute_hash.map do |dsl_key, value_schema|
194
+ optional = dsl_key.is_a?(OptionalWrapper)
195
+ key = optional ? dsl_key.key : dsl_key
196
+ Schemas::FixedHash::Attribute.new(key, inconvenience(value_schema), optional)
197
+ end
55
198
  end
56
199
 
200
+ #
201
+ # Creates a {Schemas::Maybe} schema
202
+ #
203
+ # @param subschema [schema] A schema representing the value, if the value
204
+ # is not `nil`.
205
+ # @return [Schemas::Maybe]
206
+ #
207
+ # @example A nullable Integer
208
+ # maybe(_Integer)
209
+ # # matches 5
210
+ # # matches nil
211
+ #
57
212
  def maybe(subschema)
58
- Schemas::Maybe.new(subschema)
213
+ Schemas::Maybe.new(inconvenience(subschema))
59
214
  end
60
215
 
216
+ #
217
+ # Creates a {Schemas::Enum} schema
218
+ #
219
+ # @param valid_values [Array<Object>] An array of all possible valid values.
220
+ # @param subschema [schema] A schema that represents all enum members.
221
+ # If this is `nil`, the schema is inferred to be the type of the first
222
+ # element in `valid_values` (e.g. `enum([:a,:b,:c])` will have `_Symbol`
223
+ # as the inferred subschema).
224
+ # @return [Schemas::Enum]
225
+ #
226
+ # @example Valid Rock-Paper-Scissors turn values
227
+ # enum([:rock, :paper, :scissors])
228
+ # # matches :rock
229
+ # # matches :paper
230
+ # # matches :scissors
231
+ #
61
232
  def enum(valid_values, subschema=nil)
233
+ subschema = inconvenience(subschema) if subschema
62
234
  Schemas::Enum.new(valid_values, subschema || type(valid_values.first.class))
63
235
  end
64
236
 
237
+ #
238
+ # Creates a {Schemas::Sum} schema.
239
+ #
240
+ # @param subschemas [Array<schema>] Schemas representing all the possible
241
+ # valid values.
242
+ # @return [Schemas::Sum]
243
+ #
244
+ # @example A schema that matches both Integers and Strings
245
+ # either(_String, _Integer)
246
+ # # matches "hello"
247
+ # # matches 1337
248
+ #
65
249
  def either(*subschemas)
250
+ subschemas = subschemas.map{ |ss| inconvenience(ss) }
66
251
  Schemas::Sum.new(subschemas)
67
252
  end
68
253
 
69
- def predicate(&block)
70
- Schemas::Predicate.new(block)
254
+ #
255
+ # Creates a {Schemas::Predicate} schema.
256
+ #
257
+ # @param name [String] An optional name for the predicate schema. This
258
+ # serves no purpose other than to provide useful debugging information,
259
+ # or perhaps some metadata for the schema.
260
+ # @yield Values being validated are yielded to the given block. The return
261
+ # value of the block indicates whether the value is valid or not.
262
+ # @yieldparam value [Object] The value being validated
263
+ # @yieldreturn [Boolean] Truthy if the value is valid, otherwise falsey.
264
+ # @return [Schemas::Predicate]
265
+ #
266
+ # @example A predicate that checks if numbers are odd
267
+ # predicate('odd'){ |x| x.odd? }
268
+ # # matches 5
269
+ #
270
+ def predicate(name = nil, &block)
271
+ Schemas::Predicate.new(name, &block)
71
272
  end
72
273
 
274
+ #
275
+ # Creates a {Schemas::Pipeline} schema.
276
+ #
277
+ # @param subschemas [Array<schema>] The schemas to be pipelined together,
278
+ # in order.
279
+ # @return [Schemas::Pipeline]
280
+ #
281
+ # @example A schema for positive floats
282
+ # pipeline(
283
+ # _Float,
284
+ # predicate{ |f| f > 0.0 },
285
+ # )
286
+ # # matches 6.2
287
+ #
73
288
  def pipeline(*subschemas)
289
+ subschemas = subschemas.map{ |ss| inconvenience(ss) }
74
290
  Schemas::Pipeline.new(subschemas)
75
291
  end
76
292
 
293
+ #
294
+ # Returns the {Schemas::Anything} schema.
295
+ #
296
+ # @return [Schemas::Anything]
297
+ #
298
+ # @example The anything schema
299
+ # anything
300
+ # # matches nil
301
+ # # matches 6.2
302
+ # # matches { hello: Time.now }
303
+ #
77
304
  def anything
78
305
  Schemas::Anything.instance
79
306
  end
80
307
 
308
+ def convenience(schema)
309
+ Schemas::Convenience.wrap(schema)
310
+ end
311
+
312
+ def inconvenience(schema)
313
+ Schemas::Convenience.unwrap(schema)
314
+ end
315
+
81
316
  def method_missing(sym, *args, &block)
82
317
  type = sym.to_s
83
318
  if type.start_with?('_') && args.empty? && block.nil?
@@ -88,16 +323,10 @@ module RSchema
88
323
  end
89
324
  end
90
325
 
91
- OptionalWrapper = Struct.new(:key)
92
-
93
- private
94
-
95
- def __fixed_hash_attributes(attribute_hash)
96
- attribute_hash.map do |dsl_key, value_schema|
97
- optional = dsl_key.kind_of?(OptionalWrapper)
98
- key = optional ? dsl_key.key : dsl_key
99
- Schemas::FixedHash::Attribute.new(key, value_schema, optional)
100
- end
101
- end
326
+ # @!visibility private
327
+ def respond_to?(sym, include_all=false)
328
+ # check if method starts with an underscore followed by a capital
329
+ super || !!sym.to_s.match(/\A_[A-Z]/)
330
+ end
102
331
  end
103
332
  end