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,60 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
require 'dry/schema/messages/abstract'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Schema
|
6
|
+
# I18n message backend
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
class Messages::I18n < Messages::Abstract
|
10
|
+
attr_reader :t
|
11
|
+
|
12
|
+
::I18n.load_path.concat(config.paths)
|
13
|
+
|
14
|
+
# @api private
|
15
|
+
def initialize
|
16
|
+
super
|
17
|
+
@t = I18n.method(:t)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get a message for the given key and its options
|
21
|
+
#
|
22
|
+
# @param [Symbol] key
|
23
|
+
# @param [Hash] options
|
24
|
+
#
|
25
|
+
# @return [String]
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
def get(key, options = {})
|
29
|
+
t.(key, options) if key
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check if given key is defined
|
33
|
+
#
|
34
|
+
# @return [Boolean]
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def key?(key, options)
|
38
|
+
::I18n.exists?(key, options.fetch(:locale, default_locale)) ||
|
39
|
+
::I18n.exists?(key, I18n.default_locale)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Merge messages from an additional path
|
43
|
+
#
|
44
|
+
# @param [String] path
|
45
|
+
#
|
46
|
+
# @return [Messages::I18n]
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def merge(path)
|
50
|
+
::I18n.load_path << path
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
def default_locale
|
56
|
+
I18n.locale || I18n.default_locale || super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Dry
|
2
|
+
module Schema
|
3
|
+
module Messages
|
4
|
+
# Namespaced messages backend
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
class Namespaced <Dry::Schema::Messages::Abstract
|
8
|
+
# @api private
|
9
|
+
attr_reader :namespace
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
attr_reader :messages
|
13
|
+
|
14
|
+
# @api private
|
15
|
+
attr_reader :root
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
def initialize(namespace, messages)
|
19
|
+
super()
|
20
|
+
@namespace = namespace
|
21
|
+
@messages = messages
|
22
|
+
@root = messages.root
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get a message for the given key and its options
|
26
|
+
#
|
27
|
+
# @param [Symbol] key
|
28
|
+
# @param [Hash] options
|
29
|
+
#
|
30
|
+
# @return [String]
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
def get(key, options = {})
|
34
|
+
messages.get(key, options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Check if given key is defined
|
38
|
+
#
|
39
|
+
# @return [Boolean]
|
40
|
+
#
|
41
|
+
# @api public
|
42
|
+
def key?(key, *args)
|
43
|
+
messages.key?(key, *args)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @api private
|
47
|
+
def lookup_paths(tokens)
|
48
|
+
super(tokens.merge(root: "#{root}.rules.#{namespace}")) + super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
require 'dry/equalizer'
|
5
|
+
require 'dry/schema/messages/abstract'
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Schema
|
9
|
+
# Plain YAML message backend
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
class Messages::YAML < Messages::Abstract
|
13
|
+
include Dry::Equalizer(:data)
|
14
|
+
|
15
|
+
attr_reader :data
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
configure do |config|
|
19
|
+
config.root = '%{locale}.errors'.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
def self.load(paths = config.paths)
|
24
|
+
new(paths.map { |path| load_file(path) }.reduce(:merge))
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def self.load_file(path)
|
29
|
+
flat_hash(YAML.load_file(path))
|
30
|
+
end
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
def self.flat_hash(h, f = [], g = {})
|
34
|
+
return g.update(f.join('.'.freeze) => h) unless h.is_a? Hash
|
35
|
+
h.each { |k, r| flat_hash(r, f + [k], g) }
|
36
|
+
g
|
37
|
+
end
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
def initialize(data)
|
41
|
+
super()
|
42
|
+
@data = data
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get a message for the given key and its options
|
46
|
+
#
|
47
|
+
# @param [Symbol] key
|
48
|
+
# @param [Hash] options
|
49
|
+
#
|
50
|
+
# @return [String]
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
def get(key, options = {})
|
54
|
+
data[key % { locale: options.fetch(:locale, default_locale) }]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check if given key is defined
|
58
|
+
#
|
59
|
+
# @return [Boolean]
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
def key?(key, options = {})
|
63
|
+
data.key?(key % { locale: options.fetch(:locale, default_locale) })
|
64
|
+
end
|
65
|
+
|
66
|
+
# Merge messages from an additional path
|
67
|
+
#
|
68
|
+
# @param [String] path
|
69
|
+
#
|
70
|
+
# @return [Messages::I18n]
|
71
|
+
#
|
72
|
+
# @api public
|
73
|
+
def merge(overrides)
|
74
|
+
if overrides.is_a?(Hash)
|
75
|
+
self.class.new(data.merge(self.class.flat_hash(overrides)))
|
76
|
+
else
|
77
|
+
self.class.new(data.merge(Messages::YAML.load_file(overrides)))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'dry/schema/processor'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Schema
|
5
|
+
# Params schema type
|
6
|
+
#
|
7
|
+
# @see Processor
|
8
|
+
# @see Schema.Params
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Params < Processor
|
12
|
+
config.key_map_type = :stringified
|
13
|
+
config.type_registry = config.type_registry.namespaced(:params)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'dry/equalizer'
|
2
|
+
require 'dry/logic/operators'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Schema
|
6
|
+
# Predicate objects used within the DSL
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
class Predicate
|
10
|
+
# A negated predicate
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class Negation
|
14
|
+
# @api private
|
15
|
+
attr_reader :predicate
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
def initialize(predicate)
|
19
|
+
@predicate = predicate
|
20
|
+
end
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
def to_ast(*args)
|
24
|
+
[:not, predicate.to_ast(*args)]
|
25
|
+
end
|
26
|
+
alias_method :ast, :to_ast
|
27
|
+
end
|
28
|
+
|
29
|
+
include Dry::Logic::Operators
|
30
|
+
include Dry::Equalizer(:name, :args, :block)
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
attr_reader :compiler
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
attr_reader :name
|
37
|
+
|
38
|
+
# @api private
|
39
|
+
attr_reader :args
|
40
|
+
|
41
|
+
# @api private
|
42
|
+
attr_reader :block
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def initialize(compiler, name, args, block)
|
46
|
+
@compiler = compiler
|
47
|
+
@name = name
|
48
|
+
@args = args
|
49
|
+
@block = block
|
50
|
+
end
|
51
|
+
|
52
|
+
# Negate a predicate
|
53
|
+
#
|
54
|
+
# @return [Negation]
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def !
|
58
|
+
Negation.new(self)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @api private
|
62
|
+
def ensure_valid
|
63
|
+
if compiler.predicates[name].arity - 1 != args.size
|
64
|
+
raise ArgumentError, "#{name} predicate arity is invalid"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
def to_rule
|
70
|
+
compiler.visit(to_ast)
|
71
|
+
end
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
def to_ast(*)
|
75
|
+
[:predicate, [name, compiler.predicates.arg_list(name, *args)]]
|
76
|
+
end
|
77
|
+
alias_method :ast, :to_ast
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'dry/core/cache'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Schema
|
5
|
+
# PredicateInferrer is used internally by `Macros::Value`
|
6
|
+
# for inferring type-check predicates from type specs.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class PredicateInferrer
|
10
|
+
extend Dry::Core::Cache
|
11
|
+
|
12
|
+
TYPE_TO_PREDICATE = Hash.new do |hash, type|
|
13
|
+
primitive = type.meta[:maybe] ? type.right.primitive : type.primitive
|
14
|
+
|
15
|
+
if hash.key?(primitive)
|
16
|
+
hash[primitive]
|
17
|
+
else
|
18
|
+
:"#{primitive.name.split('::').last.downcase}?"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
TYPE_TO_PREDICATE.update(
|
23
|
+
FalseClass => :false?,
|
24
|
+
Integer => :int?,
|
25
|
+
NilClass => :nil?,
|
26
|
+
String => :str?,
|
27
|
+
TrueClass => :true?
|
28
|
+
).freeze
|
29
|
+
|
30
|
+
# Infer predicate identifier from the provided type
|
31
|
+
#
|
32
|
+
# @return [Symbol]
|
33
|
+
#
|
34
|
+
# @api private
|
35
|
+
def self.[](type)
|
36
|
+
fetch_or_store(type.hash) {
|
37
|
+
predicates =
|
38
|
+
if type.is_a?(Dry::Types::Sum) && !type.meta[:maybe]
|
39
|
+
[self[type.left], self[type.right]]
|
40
|
+
else
|
41
|
+
TYPE_TO_PREDICATE[type]
|
42
|
+
end
|
43
|
+
|
44
|
+
Array(predicates).flatten
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'dry/logic/predicates'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Schema
|
5
|
+
# A registry with predicate objects from `Dry::Logic::Predicates`
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class PredicateRegistry
|
9
|
+
# @api private
|
10
|
+
attr_reader :predicates
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
def initialize(predicates = Dry::Logic::Predicates)
|
14
|
+
@predicates = predicates
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
def [](name)
|
19
|
+
predicates[name]
|
20
|
+
end
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
def key?(name)
|
24
|
+
predicates.respond_to?(name)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def arg_list(name, *values)
|
29
|
+
predicate = self[name]
|
30
|
+
|
31
|
+
predicate
|
32
|
+
.parameters
|
33
|
+
.map(&:last)
|
34
|
+
.zip(values + Array.new(predicate.arity - values.size, Undefined))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'dry/configurable'
|
2
|
+
require 'dry/initializer'
|
3
|
+
|
4
|
+
require 'dry/schema/type_registry'
|
5
|
+
require 'dry/schema/rule_applier'
|
6
|
+
require 'dry/schema/key_coercer'
|
7
|
+
require 'dry/schema/value_coercer'
|
8
|
+
|
9
|
+
module Dry
|
10
|
+
module Schema
|
11
|
+
# Processes input data using objects configured within the DSL
|
12
|
+
#
|
13
|
+
# Processing is split into 4 main steps:
|
14
|
+
#
|
15
|
+
# 1. Prepare input hash using a key map
|
16
|
+
# 2. Apply pre-coercion filtering rules (optional step, used only when `filter` was used)
|
17
|
+
# 3. Apply value coercions based on type specifications
|
18
|
+
# 4. Apply rules
|
19
|
+
#
|
20
|
+
# @see Params
|
21
|
+
# @see JSON
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
class Processor
|
25
|
+
extend Dry::Initializer
|
26
|
+
extend Dry::Configurable
|
27
|
+
|
28
|
+
setting :key_map_type
|
29
|
+
setting :type_registry, TypeRegistry.new
|
30
|
+
|
31
|
+
param :steps, default: -> { EMPTY_ARRAY.dup }
|
32
|
+
|
33
|
+
# Define a schema for your processor class
|
34
|
+
#
|
35
|
+
# @see Params
|
36
|
+
# @see JSON
|
37
|
+
#
|
38
|
+
# @return [Class]
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
def self.define(&block)
|
42
|
+
@__definition__ ||= DSL.new(
|
43
|
+
processor_type: self, parent: superclass.definition, **config, &block
|
44
|
+
)
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return DSL configured via #define
|
49
|
+
#
|
50
|
+
# @return [DSL]
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
def self.definition
|
54
|
+
@__definition__
|
55
|
+
end
|
56
|
+
|
57
|
+
# Build a new processor object
|
58
|
+
#
|
59
|
+
# @return [Processor]
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
def self.new(&block)
|
63
|
+
if block
|
64
|
+
super.tap(&block)
|
65
|
+
elsif definition
|
66
|
+
definition.call
|
67
|
+
else
|
68
|
+
raise ArgumentError, 'Cannot create a schema without a definition'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Append a step
|
73
|
+
#
|
74
|
+
# @return [Processor]
|
75
|
+
#
|
76
|
+
# @api private
|
77
|
+
def <<(step)
|
78
|
+
steps << step
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Apply processing steps to the provided input
|
83
|
+
#
|
84
|
+
# @param [Hash] input
|
85
|
+
#
|
86
|
+
# @return [Result]
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
def call(input)
|
90
|
+
Result.new(input, message_compiler: message_compiler) do |result|
|
91
|
+
steps.each do |step|
|
92
|
+
output = step.(result)
|
93
|
+
result.replace(output) if output.is_a?(::Hash)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return the key map
|
99
|
+
#
|
100
|
+
# @return [KeyMap]
|
101
|
+
#
|
102
|
+
# @api public
|
103
|
+
def key_map
|
104
|
+
@__key_map__ ||= steps.detect { |s| s.is_a?(KeyCoercer) }.key_map
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return the type schema
|
108
|
+
#
|
109
|
+
# @return [Dry::Types::Safe]
|
110
|
+
#
|
111
|
+
# @api private
|
112
|
+
def type_schema
|
113
|
+
@__type_schema__ ||= steps.detect { |s| s.is_a?(ValueCoercer) }.type_schema
|
114
|
+
end
|
115
|
+
|
116
|
+
# Return AST representation of the rules
|
117
|
+
#
|
118
|
+
# @api private
|
119
|
+
def to_ast
|
120
|
+
rule_applier.to_ast
|
121
|
+
end
|
122
|
+
|
123
|
+
# Return the message compiler
|
124
|
+
#
|
125
|
+
# @return [MessageCompiler]
|
126
|
+
#
|
127
|
+
# @api private
|
128
|
+
def message_compiler
|
129
|
+
rule_applier.message_compiler
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return the rules from rule applier
|
133
|
+
#
|
134
|
+
# @return [MessageCompiler]
|
135
|
+
#
|
136
|
+
# @api private
|
137
|
+
def rules
|
138
|
+
rule_applier.rules
|
139
|
+
end
|
140
|
+
|
141
|
+
# Return the rule applier
|
142
|
+
#
|
143
|
+
# @api private
|
144
|
+
def rule_applier
|
145
|
+
# TODO: make this more explicit through class types
|
146
|
+
@__rule_applier__ ||= steps.last
|
147
|
+
end
|
148
|
+
alias_method :to_rule, :rule_applier
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|