dry-schema 0.1.0
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 +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
|