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.
- checksums.yaml +4 -4
- data/README.md +278 -330
- data/lib/rschema.rb +104 -17
- data/lib/rschema/coercers.rb +3 -0
- data/lib/rschema/coercers/any.rb +40 -0
- data/lib/rschema/coercers/boolean.rb +30 -0
- data/lib/rschema/coercers/chain.rb +41 -0
- data/lib/rschema/coercers/date.rb +25 -0
- data/lib/rschema/coercers/fixed_hash/default_booleans_to_false.rb +62 -0
- data/lib/rschema/coercers/fixed_hash/remove_extraneous_attributes.rb +42 -0
- data/lib/rschema/coercers/fixed_hash/symbolize_keys.rb +62 -0
- data/lib/rschema/coercers/float.rb +18 -0
- data/lib/rschema/coercers/integer.rb +18 -0
- data/lib/rschema/coercers/symbol.rb +21 -0
- data/lib/rschema/coercers/time.rb +25 -0
- data/lib/rschema/coercion_wrapper.rb +46 -0
- data/lib/rschema/coercion_wrapper/rack_params.rb +21 -0
- data/lib/rschema/dsl.rb +271 -42
- data/lib/rschema/error.rb +12 -30
- data/lib/rschema/options.rb +2 -2
- data/lib/rschema/result.rb +18 -4
- data/lib/rschema/schemas.rb +3 -0
- data/lib/rschema/schemas/anything.rb +14 -12
- data/lib/rschema/schemas/boolean.rb +20 -21
- data/lib/rschema/schemas/coercer.rb +37 -0
- data/lib/rschema/schemas/convenience.rb +53 -0
- data/lib/rschema/schemas/enum.rb +25 -25
- data/lib/rschema/schemas/fixed_hash.rb +110 -91
- data/lib/rschema/schemas/fixed_length_array.rb +48 -48
- data/lib/rschema/schemas/maybe.rb +18 -17
- data/lib/rschema/schemas/pipeline.rb +20 -19
- data/lib/rschema/schemas/predicate.rb +24 -21
- data/lib/rschema/schemas/set.rb +40 -45
- data/lib/rschema/schemas/sum.rb +24 -28
- data/lib/rschema/schemas/type.rb +22 -21
- data/lib/rschema/schemas/variable_hash.rb +53 -52
- data/lib/rschema/schemas/variable_length_array.rb +39 -38
- data/lib/rschema/version.rb +1 -1
- metadata +49 -5
- data/lib/rschema/http_coercer.rb +0 -218
@@ -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
|
+
|
data/lib/rschema/dsl.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
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(
|
69
|
+
Schemas::FixedLengthArray.new(subschemas)
|
13
70
|
end
|
14
71
|
end
|
15
72
|
|
16
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
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(
|
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
|
-
|
70
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|