dry-schema 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE +20 -0
- data/README.md +21 -0
- data/config/errors.yml +91 -0
- data/lib/dry-schema.rb +1 -0
- data/lib/dry/schema.rb +51 -0
- data/lib/dry/schema/compiler.rb +31 -0
- data/lib/dry/schema/config.rb +52 -0
- data/lib/dry/schema/constants.rb +13 -0
- data/lib/dry/schema/dsl.rb +382 -0
- data/lib/dry/schema/extensions.rb +3 -0
- data/lib/dry/schema/extensions/monads.rb +18 -0
- data/lib/dry/schema/json.rb +16 -0
- data/lib/dry/schema/key.rb +166 -0
- data/lib/dry/schema/key_coercer.rb +37 -0
- data/lib/dry/schema/key_map.rb +133 -0
- data/lib/dry/schema/macros.rb +6 -0
- data/lib/dry/schema/macros/core.rb +51 -0
- data/lib/dry/schema/macros/dsl.rb +74 -0
- data/lib/dry/schema/macros/each.rb +18 -0
- data/lib/dry/schema/macros/filled.rb +24 -0
- data/lib/dry/schema/macros/hash.rb +46 -0
- data/lib/dry/schema/macros/key.rb +137 -0
- data/lib/dry/schema/macros/maybe.rb +37 -0
- data/lib/dry/schema/macros/optional.rb +17 -0
- data/lib/dry/schema/macros/required.rb +17 -0
- data/lib/dry/schema/macros/value.rb +41 -0
- data/lib/dry/schema/message.rb +103 -0
- data/lib/dry/schema/message_compiler.rb +193 -0
- data/lib/dry/schema/message_compiler/visitor_opts.rb +30 -0
- data/lib/dry/schema/message_set.rb +123 -0
- data/lib/dry/schema/messages.rb +42 -0
- data/lib/dry/schema/messages/abstract.rb +143 -0
- data/lib/dry/schema/messages/i18n.rb +60 -0
- data/lib/dry/schema/messages/namespaced.rb +53 -0
- data/lib/dry/schema/messages/yaml.rb +82 -0
- data/lib/dry/schema/params.rb +16 -0
- data/lib/dry/schema/predicate.rb +80 -0
- data/lib/dry/schema/predicate_inferrer.rb +49 -0
- data/lib/dry/schema/predicate_registry.rb +38 -0
- data/lib/dry/schema/processor.rb +151 -0
- data/lib/dry/schema/result.rb +164 -0
- data/lib/dry/schema/rule_applier.rb +45 -0
- data/lib/dry/schema/trace.rb +103 -0
- data/lib/dry/schema/type_registry.rb +42 -0
- data/lib/dry/schema/types.rb +12 -0
- data/lib/dry/schema/value_coercer.rb +27 -0
- data/lib/dry/schema/version.rb +5 -0
- metadata +255 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'dry/monads/result'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Schema
|
5
|
+
class Result
|
6
|
+
include Dry::Monads::Result::Mixin
|
7
|
+
|
8
|
+
def to_monad(options = EMPTY_HASH)
|
9
|
+
if success?
|
10
|
+
Success(output)
|
11
|
+
else
|
12
|
+
Failure(messages(options))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
alias_method :to_result, :to_monad
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'dry/schema/processor'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Schema
|
5
|
+
# JSON schema type
|
6
|
+
#
|
7
|
+
# @see Processor
|
8
|
+
# @see Schema.JSON
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class JSON < Processor
|
12
|
+
config.key_map_type = :stringified
|
13
|
+
config.type_registry = config.type_registry.namespaced(:json)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module Dry
|
2
|
+
module Schema
|
3
|
+
# Key objects used by key maps
|
4
|
+
#
|
5
|
+
# @api public
|
6
|
+
class Key
|
7
|
+
extend Dry::Core::Cache
|
8
|
+
|
9
|
+
DEFAULT_COERCER = :itself.to_proc.freeze
|
10
|
+
|
11
|
+
include Dry.Equalizer(:name, :coercer)
|
12
|
+
|
13
|
+
# @!attribute[r] id
|
14
|
+
# @return [Symbol] The key identifier
|
15
|
+
# @api public
|
16
|
+
attr_reader :id
|
17
|
+
|
18
|
+
# @!attribute[r] name
|
19
|
+
# @return [Symbol, String, Object] The actual key name expected in an input hash
|
20
|
+
# @api public
|
21
|
+
attr_reader :name
|
22
|
+
|
23
|
+
# @!attribute[r] id
|
24
|
+
# @return [Proc, #call] A key name coercer function
|
25
|
+
# @api public
|
26
|
+
attr_reader :coercer
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def self.[](name, **opts)
|
30
|
+
new(name, **opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
def self.new(*args)
|
35
|
+
fetch_or_store(*args) { super }
|
36
|
+
end
|
37
|
+
|
38
|
+
# @api private
|
39
|
+
def initialize(id, name: id, coercer: DEFAULT_COERCER)
|
40
|
+
@id = id
|
41
|
+
@name = name
|
42
|
+
@coercer = coercer
|
43
|
+
end
|
44
|
+
|
45
|
+
# @api private
|
46
|
+
def read(source)
|
47
|
+
if source.key?(name)
|
48
|
+
yield(source[name])
|
49
|
+
elsif source.key?(coerced_name)
|
50
|
+
yield(source[coerced_name])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
def write(source, target)
|
56
|
+
read(source) { |value| target[coerced_name] = value }
|
57
|
+
end
|
58
|
+
|
59
|
+
# @api private
|
60
|
+
def coercible(&coercer)
|
61
|
+
new(coercer: coercer)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def stringified
|
66
|
+
new(name: name.to_s)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @api private
|
70
|
+
def new(new_opts = EMPTY_HASH)
|
71
|
+
self.class.new(id, { name: name, coercer: coercer }.merge(new_opts))
|
72
|
+
end
|
73
|
+
|
74
|
+
# @api private
|
75
|
+
def dump
|
76
|
+
name
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# @api private
|
82
|
+
def coerced_name
|
83
|
+
@__coerced_name__ ||= coercer[name]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# A specialized key type which handles nested hashes
|
88
|
+
#
|
89
|
+
# @api private
|
90
|
+
class Key::Hash < Key
|
91
|
+
include Dry.Equalizer(:name, :members, :coercer)
|
92
|
+
|
93
|
+
# @api private
|
94
|
+
attr_reader :members
|
95
|
+
|
96
|
+
# @api private
|
97
|
+
def initialize(id, members:, **opts)
|
98
|
+
super(id, **opts)
|
99
|
+
@members = members
|
100
|
+
end
|
101
|
+
|
102
|
+
# @api private
|
103
|
+
def read(source)
|
104
|
+
super if source.is_a?(::Hash)
|
105
|
+
end
|
106
|
+
|
107
|
+
def write(source, target)
|
108
|
+
read(source) { |value|
|
109
|
+
target[coerced_name] = value.is_a?(::Hash) ? members.write(value) : value
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
# @api private
|
114
|
+
def coercible(&coercer)
|
115
|
+
new(coercer: coercer, members: members.coercible(&coercer))
|
116
|
+
end
|
117
|
+
|
118
|
+
# @api private
|
119
|
+
def stringified
|
120
|
+
new(name: name.to_s, members: members.stringified)
|
121
|
+
end
|
122
|
+
|
123
|
+
# @api private
|
124
|
+
def dump
|
125
|
+
{ name => members.map(&:dump) }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# A specialized key type which handles nested arrays
|
130
|
+
#
|
131
|
+
# @api private
|
132
|
+
class Key::Array < Key
|
133
|
+
include Dry.Equalizer(:name, :member, :coercer)
|
134
|
+
|
135
|
+
attr_reader :member
|
136
|
+
|
137
|
+
# @api private
|
138
|
+
def initialize(id, member:, **opts)
|
139
|
+
super(id, **opts)
|
140
|
+
@member = member
|
141
|
+
end
|
142
|
+
|
143
|
+
# @api private
|
144
|
+
def write(source, target)
|
145
|
+
read(source) { |value|
|
146
|
+
target[coerced_name] = value.is_a?(::Array) ? value.map { |el| member.write(el) } : value
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
# @api private
|
151
|
+
def coercible(&coercer)
|
152
|
+
new(coercer: coercer, member: member.coercible(&coercer))
|
153
|
+
end
|
154
|
+
|
155
|
+
# @api private
|
156
|
+
def stringified
|
157
|
+
new(name: name.to_s, member: member.stringified)
|
158
|
+
end
|
159
|
+
|
160
|
+
# @api private
|
161
|
+
def dump
|
162
|
+
[name, member.dump]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'dry/core/cache'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Schema
|
5
|
+
# Coerces keys in a hash using provided coercer function
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class KeyCoercer
|
9
|
+
extend Dry::Core::Cache
|
10
|
+
|
11
|
+
TO_SYM = :to_sym.to_proc.freeze
|
12
|
+
|
13
|
+
attr_reader :key_map, :coercer
|
14
|
+
|
15
|
+
# @api private
|
16
|
+
def self.new(*args, &coercer)
|
17
|
+
fetch_or_store(*args, coercer) { super(*args, &coercer) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
def self.symbolized(*args)
|
22
|
+
new(*args, &TO_SYM)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
def initialize(key_map, &coercer)
|
27
|
+
@key_map = key_map.coercible(&coercer)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
def call(source)
|
32
|
+
key_map.write(Hash(source))
|
33
|
+
end
|
34
|
+
alias_method :[], :call
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'dry/equalizer'
|
2
|
+
require 'dry/core/cache'
|
3
|
+
require 'dry/schema/constants'
|
4
|
+
require 'dry/schema/key'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Schema
|
8
|
+
# Represents a list of keys defined by the DSL
|
9
|
+
#
|
10
|
+
# KeyMap objects expose an API for introspecting schema keys and the ability
|
11
|
+
# to rebuild an input hash using configured coercer function.
|
12
|
+
#
|
13
|
+
# Instances of this class are used as the very first step by the schema processors.
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
class KeyMap
|
17
|
+
extend Dry::Core::Cache
|
18
|
+
|
19
|
+
include Dry.Equalizer(:keys)
|
20
|
+
include Enumerable
|
21
|
+
|
22
|
+
# @!attribute[r] keys
|
23
|
+
# @return [Array<Key>] A list of defined key objects
|
24
|
+
attr_reader :keys
|
25
|
+
|
26
|
+
# Coerce a list of key specs into a key map
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# KeyMap[:id, :name]
|
30
|
+
# KeyMap[:title, :artist, tags: [:name]]
|
31
|
+
# KeyMap[:title, :artist, [:tags]]
|
32
|
+
#
|
33
|
+
# @return [KeyMap]
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def self.[](*keys)
|
37
|
+
new(keys)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Build new, or returned a cached instance of a key map
|
41
|
+
#
|
42
|
+
# @param [Array<Symbol, Array, Hash<Symbol=>Array>>
|
43
|
+
#
|
44
|
+
# @return [KeyMap]
|
45
|
+
def self.new(*args)
|
46
|
+
fetch_or_store(*args) { super }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set key objects
|
50
|
+
#
|
51
|
+
# @api private
|
52
|
+
def initialize(keys)
|
53
|
+
@keys = keys.map { |key|
|
54
|
+
case key
|
55
|
+
when Hash
|
56
|
+
root, rest = key.flatten
|
57
|
+
Key::Hash[root, members: KeyMap[*rest]]
|
58
|
+
when Array
|
59
|
+
root, rest = key
|
60
|
+
Key::Array[root, member: KeyMap[*rest]]
|
61
|
+
when Key
|
62
|
+
key
|
63
|
+
else
|
64
|
+
Key[key]
|
65
|
+
end
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Write a new hash based on the source hash
|
70
|
+
#
|
71
|
+
# @param [Hash] source The source hash
|
72
|
+
# @param [Hash] target The target hash
|
73
|
+
#
|
74
|
+
# @return [Hash]
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
def write(source, target = EMPTY_HASH.dup)
|
78
|
+
each { |key| key.write(source, target) }
|
79
|
+
target
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return a new key map that is configured to coerce keys using provided coercer function
|
83
|
+
#
|
84
|
+
# @return [KeyMap]
|
85
|
+
#
|
86
|
+
# @api public
|
87
|
+
def coercible(&coercer)
|
88
|
+
self.class.new(map { |key| key.coercible(&coercer) })
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return a new key map with stringified keys
|
92
|
+
#
|
93
|
+
# A stringified key map is suitable for reading hashes with string keys
|
94
|
+
#
|
95
|
+
# @return [KeyMap]
|
96
|
+
#
|
97
|
+
# @api public
|
98
|
+
def stringified
|
99
|
+
self.class.new(map(&:stringified))
|
100
|
+
end
|
101
|
+
|
102
|
+
# Iterate over keys
|
103
|
+
#
|
104
|
+
# @api public
|
105
|
+
def each(&block)
|
106
|
+
keys.each(&block)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Return a new key map merged with the provided one
|
110
|
+
#
|
111
|
+
# @param [KeyMap, Array] other Either a key map or an array with key specs
|
112
|
+
#
|
113
|
+
# @return [KeyMap]
|
114
|
+
def +(other)
|
115
|
+
self.class.new(keys + other.to_a)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Return a string representation of a key map
|
119
|
+
#
|
120
|
+
# @return [String]
|
121
|
+
def inspect
|
122
|
+
"#<#{self.class}[#{keys.map(&:dump).map(&:inspect).join(", ")}]>"
|
123
|
+
end
|
124
|
+
|
125
|
+
# Dump keys to their spec format
|
126
|
+
#
|
127
|
+
# @return [Array]
|
128
|
+
def dump
|
129
|
+
keys.map(&:dump)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'dry/initializer'
|
2
|
+
|
3
|
+
require 'dry/schema/constants'
|
4
|
+
require 'dry/schema/compiler'
|
5
|
+
require 'dry/schema/trace'
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Schema
|
9
|
+
module Macros
|
10
|
+
# Abstract macro class
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class Core
|
14
|
+
extend Dry::Initializer
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
option :name, default: proc { nil }, optional: true
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
option :compiler, default: proc { Compiler.new }
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
option :trace, default: proc { Trace.new }
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
option :schema_dsl, optional: true
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def new(options = EMPTY_HASH)
|
30
|
+
self.class.new({ name: name, compiler: compiler, schema_dsl: schema_dsl }.merge(options))
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
def to_rule
|
35
|
+
compiler.visit(to_ast)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @api private
|
39
|
+
def to_ast(*)
|
40
|
+
trace.to_ast
|
41
|
+
end
|
42
|
+
alias_method :ast, :to_ast
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def operation
|
46
|
+
raise NotImplementedError
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'dry/logic/operators'
|
2
|
+
|
3
|
+
require 'dry/schema/macros/core'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
module Macros
|
8
|
+
# Macro specialization used within the DSL
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class DSL < Core
|
12
|
+
include Dry::Logic::Operators
|
13
|
+
|
14
|
+
undef :eql?
|
15
|
+
undef :nil?
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
option :chain, default: -> { true }
|
19
|
+
|
20
|
+
# Specify predicates that should be applied to a value
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
def value(*predicates, **opts, &block)
|
24
|
+
append_macro(Macros::Value) do |macro|
|
25
|
+
macro.call(*predicates, **opts, &block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Prepends `:filled?` predicate
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def filled(*args, &block)
|
33
|
+
append_macro(Macros::Filled) do |macro|
|
34
|
+
macro.call(*args, &block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Specify a nested schema
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
def schema(*args, &block)
|
42
|
+
append_macro(Macros::Hash) do |macro|
|
43
|
+
macro.call(*args, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Specify predicates that should be applied to each element of an array
|
48
|
+
#
|
49
|
+
# @api public
|
50
|
+
def each(*args, &block)
|
51
|
+
append_macro(Macros::Each) do |macro|
|
52
|
+
macro.value(*args, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# @api private
|
59
|
+
def append_macro(macro_type, &block)
|
60
|
+
macro = macro_type.new(schema_dsl: schema_dsl, name: name)
|
61
|
+
|
62
|
+
yield(macro)
|
63
|
+
|
64
|
+
if chain
|
65
|
+
trace << macro
|
66
|
+
self
|
67
|
+
else
|
68
|
+
macro
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|