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