rschema 3.0.1.pre5 → 3.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +188 -164
- data/lib/rschema.rb +35 -20
- data/lib/rschema/dsl.rb +89 -82
- data/lib/rschema/options.rb +4 -0
- data/lib/rschema/schemas/anything.rb +10 -0
- data/lib/rschema/schemas/boolean.rb +10 -0
- data/lib/rschema/schemas/coercer.rb +8 -0
- data/lib/rschema/schemas/convenience.rb +91 -6
- data/lib/rschema/schemas/enum.rb +10 -0
- data/lib/rschema/schemas/fixed_hash.rb +57 -10
- data/lib/rschema/schemas/fixed_length_array.rb +12 -0
- data/lib/rschema/schemas/maybe.rb +12 -1
- data/lib/rschema/schemas/pipeline.rb +15 -0
- data/lib/rschema/schemas/predicate.rb +11 -0
- data/lib/rschema/schemas/set.rb +10 -0
- data/lib/rschema/schemas/sum.rb +11 -0
- data/lib/rschema/schemas/type.rb +17 -0
- data/lib/rschema/schemas/variable_hash.rb +10 -0
- data/lib/rschema/schemas/variable_length_array.rb +10 -0
- data/lib/rschema/version.rb +1 -1
- metadata +18 -4
@@ -1,5 +1,13 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
4
|
+
#
|
5
|
+
# A schema that applies a coercer to a value, before passing the coerced
|
6
|
+
# value to a subschema.
|
7
|
+
#
|
8
|
+
# This is not a type of schema that you would typically create yourself.
|
9
|
+
# It is used internally to implement RSchema's coercion functionality.
|
10
|
+
#
|
3
11
|
class Coercer
|
4
12
|
attr_reader :coercer, :subschema
|
5
13
|
|
@@ -2,21 +2,78 @@ require 'delegate'
|
|
2
2
|
|
3
3
|
module RSchema
|
4
4
|
module Schemas
|
5
|
+
|
6
|
+
#
|
7
|
+
# A wrapper that provides convenience methods for schema objects.
|
8
|
+
#
|
9
|
+
# Because this class inherits from `SimpleDelegator`, convenience wrappers
|
10
|
+
# behave like their underlying schemas. That is, you can call methods on the
|
11
|
+
# underlying schema object through the convenience wrapper.
|
12
|
+
#
|
13
|
+
# Schema objects only need to implement the `call` method to validate values.
|
14
|
+
# This small interface is simple for schema classes to implement, but not
|
15
|
+
# very descriptive when actually using the schema objects. So, to make
|
16
|
+
# schema objects nicer to use, this class provides a variety of
|
17
|
+
# more-descriptive methods like {#validate}, {#validate!}, {#valid?}, and
|
18
|
+
# {#invalid?}.
|
19
|
+
#
|
5
20
|
class Convenience < SimpleDelegator
|
6
|
-
def initialize(
|
21
|
+
def initialize(underlying_schema)
|
7
22
|
super
|
8
23
|
end
|
9
24
|
|
10
|
-
|
25
|
+
# @return [schema] the underlying schema object
|
26
|
+
def underlying_schema
|
11
27
|
__getobj__
|
12
28
|
end
|
13
29
|
|
30
|
+
#
|
31
|
+
# Applies the schema to a value
|
32
|
+
#
|
33
|
+
# This is that same as the `call` method available on all schema objects,
|
34
|
+
# except that the `options` param is optional.
|
35
|
+
#
|
36
|
+
# @param value [Object] The value to validate
|
37
|
+
# @param options [RSchema::Options]
|
38
|
+
#
|
39
|
+
# @return [RSchema::Result]
|
40
|
+
#
|
14
41
|
def validate(value, options=Options.default)
|
15
42
|
call(value, options)
|
16
43
|
end
|
17
44
|
|
45
|
+
#
|
46
|
+
# Returns the validation error for the given value
|
47
|
+
#
|
48
|
+
# @param value [Object] The value to validate
|
49
|
+
# @param options [RSchema::Options]
|
50
|
+
#
|
51
|
+
# @return The error object if `value` is invalid, otherwise `nil`.
|
52
|
+
#
|
53
|
+
# @see Result#error
|
54
|
+
#
|
55
|
+
def error_for(value, options=Options.default)
|
56
|
+
result = underlying_schema.call(value, options)
|
57
|
+
if result.valid?
|
58
|
+
nil
|
59
|
+
else
|
60
|
+
result.error
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# Applies the schema to a value, raising an exception if the value is invalid
|
66
|
+
#
|
67
|
+
# @param value [Object] The value to validate
|
68
|
+
# @param options [RSchema::Options]
|
69
|
+
#
|
70
|
+
# @raise [RSchema::Invalid] If the value is not valid
|
71
|
+
# @return [Object] The validated value
|
72
|
+
#
|
73
|
+
# @see Result#value
|
74
|
+
#
|
18
75
|
def validate!(value, options=Options.default)
|
19
|
-
result = call(value, options)
|
76
|
+
result = underlying_schema.call(value, options)
|
20
77
|
if result.valid?
|
21
78
|
result.value
|
22
79
|
else
|
@@ -24,11 +81,32 @@ class Convenience < SimpleDelegator
|
|
24
81
|
end
|
25
82
|
end
|
26
83
|
|
84
|
+
#
|
85
|
+
# Checks whether a value is valid or not
|
86
|
+
#
|
87
|
+
# @param value [Object] The value to validate
|
88
|
+
# @return [Boolean] `true` if the value is valid, otherwise `false`
|
89
|
+
#
|
27
90
|
def valid?(value)
|
28
|
-
result = call(value, Options.
|
91
|
+
result = underlying_schema.call(value, Options.fail_fast)
|
29
92
|
result.valid?
|
30
93
|
end
|
31
94
|
|
95
|
+
#
|
96
|
+
# The opposite of {#valid?}
|
97
|
+
#
|
98
|
+
# @see #valid?
|
99
|
+
#
|
100
|
+
def invalid?(value)
|
101
|
+
not valid?(value)
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Wraps the given schema in a {Convenience}, if it isn't already wrapped.
|
106
|
+
#
|
107
|
+
# @param schema [schema] The schema to wrap
|
108
|
+
# @return {Convenience}
|
109
|
+
#
|
32
110
|
def self.wrap(schema)
|
33
111
|
if schema.is_a?(self)
|
34
112
|
schema
|
@@ -37,15 +115,22 @@ class Convenience < SimpleDelegator
|
|
37
115
|
end
|
38
116
|
end
|
39
117
|
|
118
|
+
#
|
119
|
+
# Removes any {Convenience} wrappers from a schema
|
120
|
+
#
|
121
|
+
# @param schema [schema] The schema to unwrap
|
122
|
+
# @return [schema] The underlying schema, with all {Convenience} wrappers
|
123
|
+
# removed
|
40
124
|
def self.unwrap(schema)
|
41
125
|
while schema.is_a?(self)
|
42
|
-
schema = schema.
|
126
|
+
schema = schema.underlying_schema
|
43
127
|
end
|
44
128
|
schema
|
45
129
|
end
|
46
130
|
|
131
|
+
# @!visibility private
|
47
132
|
def with_wrapped_subschemas(wrapper)
|
48
|
-
self.class.new(wrapper.wrap(
|
133
|
+
self.class.new(wrapper.wrap(underlying_schema))
|
49
134
|
end
|
50
135
|
|
51
136
|
end
|
data/lib/rschema/schemas/enum.rb
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
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
|
+
#
|
3
13
|
class Enum
|
4
14
|
attr_reader :members, :subschema
|
5
15
|
|
@@ -1,5 +1,19 @@
|
|
1
1
|
module RSchema
|
2
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
|
+
#
|
3
17
|
class FixedHash
|
4
18
|
attr_reader :attributes
|
5
19
|
|
@@ -32,6 +46,28 @@ class FixedHash
|
|
32
46
|
attributes.find{ |attr| attr.key == attr_key }
|
33
47
|
end
|
34
48
|
|
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
|
+
#
|
35
71
|
def merge(new_attributes)
|
36
72
|
merged_attrs = (attributes + new_attributes)
|
37
73
|
.map { |attr| [attr.key, attr] }
|
@@ -41,6 +77,22 @@ class FixedHash
|
|
41
77
|
self.class.new(merged_attrs)
|
42
78
|
end
|
43
79
|
|
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
|
+
#
|
44
96
|
def without(attribute_keys)
|
45
97
|
filtered_attrs = attributes
|
46
98
|
.reject { |attr| attribute_keys.include?(attr.key) }
|
@@ -104,16 +156,11 @@ class FixedHash
|
|
104
156
|
subresults_by_key
|
105
157
|
end
|
106
158
|
|
107
|
-
def failure_error(
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
error[key] = attr_result.error
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
error
|
159
|
+
def failure_error(subresults)
|
160
|
+
subresults
|
161
|
+
.select{ |_, result| result.invalid? }
|
162
|
+
.map{ |key, result| [key, result.error] }
|
163
|
+
.to_h
|
117
164
|
end
|
118
165
|
|
119
166
|
def success_value(subresults)
|
@@ -1,5 +1,17 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
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
|
+
#
|
3
15
|
class FixedLengthArray
|
4
16
|
attr_reader :subschemas
|
5
17
|
|
@@ -1,5 +1,16 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
4
|
+
#
|
5
|
+
# A schema representing that a value may be `nil`
|
6
|
+
#
|
7
|
+
# If the value is not `nil`, it must conform to the subschema
|
8
|
+
#
|
9
|
+
# @example A nil-able Integer
|
10
|
+
# schema = RSchema.define{ maybe(_Integer) }
|
11
|
+
# schema.valid?(5) #=> true
|
12
|
+
# schema.valid?(nil) #=> true
|
13
|
+
#
|
3
14
|
class Maybe
|
4
15
|
attr_reader :subschema
|
5
16
|
|
@@ -8,7 +19,7 @@ class Maybe
|
|
8
19
|
end
|
9
20
|
|
10
21
|
def call(value, options)
|
11
|
-
if
|
22
|
+
if nil == value
|
12
23
|
Result.success(value)
|
13
24
|
else
|
14
25
|
@subschema.call(value, options)
|
@@ -1,5 +1,20 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
4
|
+
#
|
5
|
+
# A schema that chains together an ordered list of other schemas
|
6
|
+
#
|
7
|
+
# @example A schema for positive floats
|
8
|
+
# schema = RSchema.define do
|
9
|
+
# pipeline(
|
10
|
+
# _Float,
|
11
|
+
# predicate{ |f| f > 0.0 },
|
12
|
+
# )
|
13
|
+
# end
|
14
|
+
# schema.valid?(6.2) #=> true
|
15
|
+
# schema.valid?('hi') #=> false (because it's not a Float)
|
16
|
+
# schema.valid?(-6.2) #=> false (because predicate failed)
|
17
|
+
#
|
3
18
|
class Pipeline
|
4
19
|
attr_reader :subschemas
|
5
20
|
|
@@ -1,5 +1,16 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
4
|
+
#
|
5
|
+
# A schema that uses a given block to determine whether a value is valid
|
6
|
+
#
|
7
|
+
# @example A predicate that checks if numbers are odd
|
8
|
+
# schema = RSchema.define do
|
9
|
+
# predicate('odd'){ |x| x.odd? }
|
10
|
+
# end
|
11
|
+
# schema.valid?(5) #=> true
|
12
|
+
# schema.valid?(6) #=> false
|
13
|
+
#
|
3
14
|
class Predicate
|
4
15
|
attr_reader :block, :name
|
5
16
|
|
data/lib/rschema/schemas/set.rb
CHANGED
@@ -2,6 +2,16 @@ require 'set'
|
|
2
2
|
|
3
3
|
module RSchema
|
4
4
|
module Schemas
|
5
|
+
|
6
|
+
#
|
7
|
+
# A schema that matches `Set` objects (from the Ruby standard library)
|
8
|
+
#
|
9
|
+
# @example A set of integers
|
10
|
+
# require 'set'
|
11
|
+
# schema = RSchema.define { set(_Integer) }
|
12
|
+
# schema.valid?(Set[1, 2, 3]) #=> true
|
13
|
+
# schema.valid?(Set[:a, :b, :c]) #=> false
|
14
|
+
#
|
5
15
|
class Set
|
6
16
|
attr_reader :subschema
|
7
17
|
|
data/lib/rschema/schemas/sum.rb
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
4
|
+
#
|
5
|
+
# A schema that represents a "sum type"
|
6
|
+
#
|
7
|
+
# Values must conform to one of the subschemas.
|
8
|
+
#
|
9
|
+
# @example A schema that matches both Integers and Strings
|
10
|
+
# schema = RSchema.define { either(_String, _Integer) }
|
11
|
+
# schema.valid?("hello") #=> true
|
12
|
+
# schema.valid?(5) #=> true
|
13
|
+
#
|
3
14
|
class Sum
|
4
15
|
attr_reader :subschemas
|
5
16
|
|
data/lib/rschema/schemas/type.rb
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
4
|
+
#
|
5
|
+
# A schema that matches values of a given type (i.e. `value.is_a?(type)`)
|
6
|
+
#
|
7
|
+
# @example An Integer schema
|
8
|
+
# schema = RSchema.define { _Integer }
|
9
|
+
# schema.valid?(5) #=> true
|
10
|
+
#
|
11
|
+
# @example A namespaced type
|
12
|
+
# schema = RSchema.define do
|
13
|
+
# # This will not work:
|
14
|
+
# # _ActiveWhatever::Thing
|
15
|
+
#
|
16
|
+
# # This will work:
|
17
|
+
# type(ActiveWhatever::Thing)
|
18
|
+
# end
|
19
|
+
#
|
3
20
|
class Type
|
4
21
|
attr_reader :type
|
5
22
|
|
@@ -1,5 +1,15 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
4
|
+
#
|
5
|
+
# A schema that matches variable-sized `Hash` objects, where the keys are _not_
|
6
|
+
# known ahead of time.
|
7
|
+
#
|
8
|
+
# @example A hash of integers to strings
|
9
|
+
# schema = RSchema.define { variable_hash(_Integer => _String) }
|
10
|
+
# schema.valid?({ 5 => "hello", 7 => "world" }) #=> true
|
11
|
+
# schema.valid?({}) #=> true
|
12
|
+
#
|
3
13
|
class VariableHash
|
4
14
|
attr_reader :key_schema, :value_schema
|
5
15
|
|
@@ -1,5 +1,15 @@
|
|
1
1
|
module RSchema
|
2
2
|
module Schemas
|
3
|
+
|
4
|
+
#
|
5
|
+
# A schema that matches variable-length arrays, where all elements conform to
|
6
|
+
# a single subschema
|
7
|
+
#
|
8
|
+
# @example A variable-length array schema
|
9
|
+
# schema = RSchema.define { array(_Integer) }
|
10
|
+
# schema.valid?([1,2,3]) #=> true
|
11
|
+
# schema.valid?([]) #=> true
|
12
|
+
#
|
3
13
|
class VariableLengthArray
|
4
14
|
attr_accessor :element_schema
|
5
15
|
|
data/lib/rschema/version.rb
CHANGED