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