rschema 3.0.1.pre3 → 3.0.1.pre4
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/rschema.rb
CHANGED
@@ -1,36 +1,123 @@
|
|
1
|
+
require 'docile'
|
2
|
+
|
1
3
|
require 'rschema/options'
|
2
4
|
require 'rschema/error'
|
3
5
|
require 'rschema/result'
|
4
|
-
require 'rschema/schemas
|
5
|
-
require 'rschema/schemas/maybe'
|
6
|
-
require 'rschema/schemas/enum'
|
7
|
-
require 'rschema/schemas/boolean'
|
8
|
-
require 'rschema/schemas/sum'
|
9
|
-
require 'rschema/schemas/pipeline'
|
10
|
-
require 'rschema/schemas/anything'
|
11
|
-
require 'rschema/schemas/predicate'
|
12
|
-
require 'rschema/schemas/set'
|
13
|
-
require 'rschema/schemas/variable_hash'
|
14
|
-
require 'rschema/schemas/fixed_hash'
|
15
|
-
require 'rschema/schemas/variable_length_array'
|
16
|
-
require 'rschema/schemas/fixed_length_array'
|
6
|
+
require 'rschema/schemas'
|
17
7
|
require 'rschema/dsl'
|
18
|
-
require 'rschema/
|
8
|
+
require 'rschema/coercers'
|
9
|
+
require 'rschema/coercion_wrapper'
|
19
10
|
|
11
|
+
#
|
12
|
+
# Schema-based validation and coercion
|
13
|
+
#
|
20
14
|
module RSchema
|
21
|
-
|
22
|
-
|
15
|
+
|
16
|
+
#
|
17
|
+
# Runs a block using a DSL.
|
18
|
+
#
|
19
|
+
# @param dsl [Object] An optional DSL object to run the block with.
|
20
|
+
# Uses {RSchema#default_dsl} if nil.
|
21
|
+
# @yield Invokes the given block with access to the methods on `dsl`.
|
22
|
+
# @return The return value of the given block (usually some kind of schema object)
|
23
|
+
#
|
24
|
+
# @example Creating a typical fixed hash schema
|
25
|
+
# person_schema = RSchema.define do
|
26
|
+
# fixed_hash(
|
27
|
+
# name: _String,
|
28
|
+
# age: _Integer,
|
29
|
+
# )
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
def self.define(dsl = nil, &block)
|
33
|
+
schema = dsl_eval(dsl, &block)
|
34
|
+
Schemas::Convenience.wrap(schema)
|
23
35
|
end
|
24
36
|
|
37
|
+
def self.dsl_eval(dsl = nil, &block)
|
38
|
+
Docile::Execution.exec_in_proxy_context(
|
39
|
+
dsl || default_dsl,
|
40
|
+
Docile::FallbackContextProxy,
|
41
|
+
&block
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# A shortcut for:
|
47
|
+
#
|
48
|
+
# RSchema.define do
|
49
|
+
# fixed_hash(...)
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# @yield Invokes the given block with access to the methods of the default DSL.
|
53
|
+
# @yieldreturn The attributes of the hash schema (the argument to {DSL#fixed_hash}).
|
54
|
+
# @return [Schemas::FixedHash]
|
55
|
+
#
|
56
|
+
# @example A typical schema
|
57
|
+
#
|
58
|
+
# person_schema = RSchema.define_hash {{
|
59
|
+
# name: _String,
|
60
|
+
# age: _Integer,
|
61
|
+
# }}
|
62
|
+
#
|
25
63
|
def self.define_hash(&block)
|
26
|
-
|
64
|
+
Schemas::Convenience.wrap(
|
65
|
+
default_dsl.fixed_hash(dsl_eval(&block))
|
66
|
+
)
|
27
67
|
end
|
28
68
|
|
69
|
+
#
|
70
|
+
# A shortcut for:
|
71
|
+
#
|
72
|
+
# RSchema.define do
|
73
|
+
# predicate { ... }
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# @param name [String] An arbitraty name for the predicate schema.
|
77
|
+
# @yield [value] Yields a single value.
|
78
|
+
# @yieldreturn [Boolean] Truthy if the value is valid, otherwise falsey.
|
79
|
+
# @return [Schemas::Predicate]
|
80
|
+
#
|
81
|
+
# @example A predicate schema that only allows `odd?` objects.
|
82
|
+
#
|
83
|
+
# odd_schema = RSchema.define_predicate('odd') do |x|
|
84
|
+
# x.odd?
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# @see DSL#predicate
|
88
|
+
#
|
89
|
+
def self.define_predicate(name = nil, &block)
|
90
|
+
Schemas::Convenience.wrap(
|
91
|
+
default_dsl.predicate(name, &block)
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# @return The default DSL object.
|
97
|
+
# @see DefaultDSL
|
98
|
+
#
|
29
99
|
def self.default_dsl
|
30
100
|
@default_dsl ||= DefaultDSL.new
|
31
101
|
end
|
32
102
|
|
103
|
+
#
|
104
|
+
# The class of the default RSchema DSL.
|
105
|
+
#
|
106
|
+
# By default, this only includes the methods from the {RSchema::DSL} mixin.
|
107
|
+
#
|
108
|
+
# Your own code and other gems may include modules into this class, in order
|
109
|
+
# to add new methods to the default DSL.
|
110
|
+
#
|
33
111
|
class DefaultDSL
|
34
112
|
include RSchema::DSL
|
35
113
|
end
|
114
|
+
|
115
|
+
class Invalid < StandardError
|
116
|
+
attr_reader :validation_error
|
117
|
+
|
118
|
+
def initialize(validation_error)
|
119
|
+
super()
|
120
|
+
@validation_error = validation_error
|
121
|
+
end
|
122
|
+
end
|
36
123
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RSchema
|
2
|
+
module Coercers
|
3
|
+
|
4
|
+
class Any
|
5
|
+
attr_reader :subcoercers
|
6
|
+
|
7
|
+
def self.[](*subbuilders)
|
8
|
+
Builder.new(subbuilders)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(subcoercers)
|
12
|
+
@subcoercers = subcoercers
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(value)
|
16
|
+
subcoercers.each do |coercer|
|
17
|
+
result = coercer.call(value)
|
18
|
+
return result if result.valid?
|
19
|
+
end
|
20
|
+
Result.failure
|
21
|
+
end
|
22
|
+
|
23
|
+
class Builder
|
24
|
+
attr_reader :subbuilders
|
25
|
+
|
26
|
+
def initialize(subbuilders)
|
27
|
+
@subbuilders = subbuilders
|
28
|
+
end
|
29
|
+
|
30
|
+
def build(schema)
|
31
|
+
subcoercers = subbuilders.map do |builder|
|
32
|
+
builder.build(schema)
|
33
|
+
end
|
34
|
+
Any.new(subcoercers)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RSchema
|
2
|
+
module Coercers
|
3
|
+
|
4
|
+
module Boolean
|
5
|
+
extend self
|
6
|
+
|
7
|
+
TRUTHY_STRINGS = ['on', '1', 'true', 'yes']
|
8
|
+
FALSEY_STRINGS = ['off', '0', 'false', 'no']
|
9
|
+
|
10
|
+
def build(schema)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(value)
|
15
|
+
case value
|
16
|
+
when true, false then Result.success(value)
|
17
|
+
when nil then Result.success(false)
|
18
|
+
when String
|
19
|
+
case
|
20
|
+
when TRUTHY_STRINGS.include?(value.downcase) then Result.success(true)
|
21
|
+
when FALSEY_STRINGS.include?(value.downcase) then Result.success(false)
|
22
|
+
else Result.failure
|
23
|
+
end
|
24
|
+
else Result.failure
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module RSchema
|
2
|
+
module Coercers
|
3
|
+
|
4
|
+
class Chain
|
5
|
+
attr_reader :subcoercers
|
6
|
+
|
7
|
+
def self.[](*subbuilders)
|
8
|
+
Builder.new(subbuilders)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(subcoercers)
|
12
|
+
@subcoercers = subcoercers
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(value)
|
16
|
+
result = Result.success(value)
|
17
|
+
subcoercers.each do |coercer|
|
18
|
+
result = coercer.call(result.value)
|
19
|
+
break if result.invalid?
|
20
|
+
end
|
21
|
+
result
|
22
|
+
end
|
23
|
+
|
24
|
+
class Builder
|
25
|
+
attr_reader :subbuilders
|
26
|
+
|
27
|
+
def initialize(subbuilders)
|
28
|
+
@subbuilders = subbuilders
|
29
|
+
end
|
30
|
+
|
31
|
+
def build(schema)
|
32
|
+
subcoercers = subbuilders.map do |builder|
|
33
|
+
builder.build(schema)
|
34
|
+
end
|
35
|
+
Chain.new(subcoercers)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module RSchema
|
2
|
+
module Coercers
|
3
|
+
|
4
|
+
module Date
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def build(schema)
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(value)
|
12
|
+
case value
|
13
|
+
when ::Date
|
14
|
+
Result.success(value)
|
15
|
+
when ::String
|
16
|
+
date = ::Date.parse(value) rescue nil
|
17
|
+
date ? Result.success(date) : Result.failure
|
18
|
+
else
|
19
|
+
Result.failure
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module RSchema
|
4
|
+
module Coercers
|
5
|
+
module FixedHash
|
6
|
+
|
7
|
+
# The HTTP standard says that when a form is submitted, all unchecked
|
8
|
+
# check boxes will _not_ be sent to the server. That is, they will not
|
9
|
+
# be present at all in the params hash.
|
10
|
+
#
|
11
|
+
# This class coerces these missing values into `false`.
|
12
|
+
class DefaultBooleansToFalse
|
13
|
+
attr_reader :hash_attributes
|
14
|
+
|
15
|
+
def self.build(schema)
|
16
|
+
new(schema)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(fixed_hash_schema)
|
20
|
+
#TODO: make fixed hash attributes frozen, and eliminate dup
|
21
|
+
@hash_attributes = fixed_hash_schema.attributes.map(&:dup)
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(value)
|
25
|
+
Result.success(default_bools_to_false(value))
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def default_bools_to_false(hash)
|
30
|
+
missing_keys = keys_for_bool_defaulting - hash.keys
|
31
|
+
|
32
|
+
if missing_keys.any?
|
33
|
+
defaults = missing_keys.map{ |k| [k, false] }.to_h
|
34
|
+
hash.merge(defaults)
|
35
|
+
else
|
36
|
+
hash # no coercion necessary
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def keys_for_bool_defaulting
|
41
|
+
@keys_for_bool_defaulting ||= Set.new(
|
42
|
+
hash_attributes
|
43
|
+
.reject(&:optional)
|
44
|
+
.select { |attr| is_bool_schema?(attr.value_schema) }
|
45
|
+
.map(&:key)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_bool_schema?(schema)
|
50
|
+
# dig through all the coercers
|
51
|
+
non_coercer = schema
|
52
|
+
while non_coercer.is_a?(Schemas::Coercer)
|
53
|
+
non_coercer = non_coercer.subschema
|
54
|
+
end
|
55
|
+
|
56
|
+
non_coercer.is_a?(Schemas::Boolean)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module RSchema
|
2
|
+
module Coercers
|
3
|
+
module FixedHash
|
4
|
+
|
5
|
+
class RemoveExtraneousAttributes
|
6
|
+
attr_reader :hash_attributes
|
7
|
+
|
8
|
+
def self.build(schema)
|
9
|
+
new(schema)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(fixed_hash_schema)
|
13
|
+
#TODO: make fixed hash attributes frozen, and eliminate dup
|
14
|
+
@hash_attributes = fixed_hash_schema.attributes.map(&:dup)
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(value)
|
18
|
+
Result.success(remove_extraneous_elements(value))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def remove_extraneous_elements(hash)
|
24
|
+
keys_to_remove = hash.keys - valid_keys
|
25
|
+
|
26
|
+
if keys_to_remove.any?
|
27
|
+
hash.dup.tap do |stripped_hash|
|
28
|
+
keys_to_remove.each { |k| stripped_hash.delete(k) }
|
29
|
+
end
|
30
|
+
else
|
31
|
+
hash
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid_keys
|
36
|
+
@valid_keys ||= hash_attributes.map(&:key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module RSchema
|
4
|
+
module Coercers
|
5
|
+
module FixedHash
|
6
|
+
|
7
|
+
class SymbolizeKeys
|
8
|
+
attr_reader :hash_attributes
|
9
|
+
|
10
|
+
def self.build(schema)
|
11
|
+
new(schema)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(fixed_hash_schema)
|
15
|
+
#TODO: make fixed hash attributes frozen, and eliminate dup
|
16
|
+
@hash_attributes = fixed_hash_schema.attributes.map(&:dup)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(value)
|
20
|
+
Result.success(symbolize_keys(value))
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def symbolize_keys(hash)
|
26
|
+
keys = keys_to_symbolize(hash)
|
27
|
+
if keys.any?
|
28
|
+
hash.dup.tap do |new_hash|
|
29
|
+
keys.each { |k| new_hash[k.to_sym] = new_hash.delete(k) }
|
30
|
+
end
|
31
|
+
else
|
32
|
+
hash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def keys_to_symbolize(hash)
|
37
|
+
non_string_keys = Set.new(hash.keys) - string_keys
|
38
|
+
non_string_keys.intersection(symbol_keys_as_strings)
|
39
|
+
end
|
40
|
+
|
41
|
+
def symbol_keys_as_strings
|
42
|
+
@symbol_keys_as_strings ||= Set.new(
|
43
|
+
all_keys
|
44
|
+
.select{ |k| k.is_a?(::Symbol) }
|
45
|
+
.map(&:to_s)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def string_keys
|
50
|
+
@string_keys ||= Set.new(
|
51
|
+
all_keys.select { |k| k.is_a?(::String) }
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def all_keys
|
56
|
+
@all_keys ||= hash_attributes.map(&:key)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|