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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE +20 -0
  4. data/README.md +21 -0
  5. data/config/errors.yml +91 -0
  6. data/lib/dry-schema.rb +1 -0
  7. data/lib/dry/schema.rb +51 -0
  8. data/lib/dry/schema/compiler.rb +31 -0
  9. data/lib/dry/schema/config.rb +52 -0
  10. data/lib/dry/schema/constants.rb +13 -0
  11. data/lib/dry/schema/dsl.rb +382 -0
  12. data/lib/dry/schema/extensions.rb +3 -0
  13. data/lib/dry/schema/extensions/monads.rb +18 -0
  14. data/lib/dry/schema/json.rb +16 -0
  15. data/lib/dry/schema/key.rb +166 -0
  16. data/lib/dry/schema/key_coercer.rb +37 -0
  17. data/lib/dry/schema/key_map.rb +133 -0
  18. data/lib/dry/schema/macros.rb +6 -0
  19. data/lib/dry/schema/macros/core.rb +51 -0
  20. data/lib/dry/schema/macros/dsl.rb +74 -0
  21. data/lib/dry/schema/macros/each.rb +18 -0
  22. data/lib/dry/schema/macros/filled.rb +24 -0
  23. data/lib/dry/schema/macros/hash.rb +46 -0
  24. data/lib/dry/schema/macros/key.rb +137 -0
  25. data/lib/dry/schema/macros/maybe.rb +37 -0
  26. data/lib/dry/schema/macros/optional.rb +17 -0
  27. data/lib/dry/schema/macros/required.rb +17 -0
  28. data/lib/dry/schema/macros/value.rb +41 -0
  29. data/lib/dry/schema/message.rb +103 -0
  30. data/lib/dry/schema/message_compiler.rb +193 -0
  31. data/lib/dry/schema/message_compiler/visitor_opts.rb +30 -0
  32. data/lib/dry/schema/message_set.rb +123 -0
  33. data/lib/dry/schema/messages.rb +42 -0
  34. data/lib/dry/schema/messages/abstract.rb +143 -0
  35. data/lib/dry/schema/messages/i18n.rb +60 -0
  36. data/lib/dry/schema/messages/namespaced.rb +53 -0
  37. data/lib/dry/schema/messages/yaml.rb +82 -0
  38. data/lib/dry/schema/params.rb +16 -0
  39. data/lib/dry/schema/predicate.rb +80 -0
  40. data/lib/dry/schema/predicate_inferrer.rb +49 -0
  41. data/lib/dry/schema/predicate_registry.rb +38 -0
  42. data/lib/dry/schema/processor.rb +151 -0
  43. data/lib/dry/schema/result.rb +164 -0
  44. data/lib/dry/schema/rule_applier.rb +45 -0
  45. data/lib/dry/schema/trace.rb +103 -0
  46. data/lib/dry/schema/type_registry.rb +42 -0
  47. data/lib/dry/schema/types.rb +12 -0
  48. data/lib/dry/schema/value_coercer.rb +27 -0
  49. data/lib/dry/schema/version.rb +5 -0
  50. metadata +255 -0
@@ -0,0 +1,164 @@
1
+ require 'dry/initializer'
2
+ require 'dry/equalizer'
3
+
4
+ module Dry
5
+ module Schema
6
+ # Processing result
7
+ #
8
+ # @see Processor#call
9
+ #
10
+ # @api public
11
+ class Result
12
+ include Dry::Equalizer(:output, :errors)
13
+
14
+ extend Dry::Initializer
15
+
16
+ # @api private
17
+ param :output
18
+ alias_method :to_h, :output
19
+ alias_method :to_hash, :output
20
+
21
+ # @api private
22
+ param :results, default: -> { EMPTY_ARRAY.dup }
23
+
24
+ # @api private
25
+ option :message_compiler
26
+
27
+ # @api private
28
+ def self.new(*args)
29
+ result = super
30
+ yield(result)
31
+ result.freeze
32
+ end
33
+
34
+ # @api private
35
+ def replace(hash)
36
+ @output = hash
37
+ self
38
+ end
39
+
40
+ # @api private
41
+ def concat(other)
42
+ results.concat(other)
43
+ result_ast.concat(other.map(&:to_ast))
44
+ self
45
+ end
46
+
47
+ # Read value from the output hash
48
+ #
49
+ # @param [Symbol] name
50
+ #
51
+ # @return [Object]
52
+ #
53
+ # @api public
54
+ def [](name)
55
+ output[name]
56
+ end
57
+
58
+ # Check if a given key is present in the output
59
+ #
60
+ # @param [Symbol] name
61
+ #
62
+ # @return [Boolean]
63
+ #
64
+ # @api public
65
+ def key?(name)
66
+ output.key?(name)
67
+ end
68
+
69
+ # Check if a given key resulted in an error
70
+ #
71
+ # @param [Symbol] name
72
+ #
73
+ # @return [Boolean]
74
+ #
75
+ # @api public
76
+ def error?(name)
77
+ errors.key?(name)
78
+ end
79
+
80
+ # Check if the result is successful
81
+ #
82
+ # @return [Boolean]
83
+ #
84
+ # @api public
85
+ def success?
86
+ results.empty?
87
+ end
88
+
89
+ # Check if the result is not successful
90
+ #
91
+ # @return [Boolean]
92
+ #
93
+ # @api public
94
+ def failure?
95
+ !success?
96
+ end
97
+
98
+ # Get human-readable error representation
99
+ #
100
+ # @see #message_set
101
+ #
102
+ # @return [Hash<Symbol=>Array>]
103
+ #
104
+ # @api public
105
+ def errors(options = EMPTY_HASH)
106
+ message_set(options.merge(hints: false)).dump
107
+ end
108
+
109
+ # Get all messages including hints
110
+ #
111
+ # @see #message_set
112
+ #
113
+ # @return [Hash<Symbol=>Array>]
114
+ #
115
+ # @api public
116
+ def messages(options = EMPTY_HASH)
117
+ message_set(options.merge(hints: true)).dump
118
+ end
119
+
120
+ # Get hints exclusively without errors
121
+ #
122
+ # @see #message_set
123
+ #
124
+ # @return [Hash<Symbol=>Array>]
125
+ #
126
+ # @api public
127
+ def hints(options = EMPTY_HASH)
128
+ message_set(options.merge(failures: false)).dump
129
+ end
130
+
131
+ # Return the message set
132
+ #
133
+ # @param [Hash] options
134
+ # @option options [Symbol] :locale Alternative locale (default is :en)
135
+ # @option options [Boolean] :hints Whether to include hint messages or not
136
+ # @option options [Boolean] :full Whether to generate messages that include key names
137
+ #
138
+ # @return [MessageSet]
139
+ #
140
+ # @api public
141
+ def message_set(options = EMPTY_HASH)
142
+ message_compiler.with(options).(result_ast)
143
+ end
144
+
145
+ # Return a string representation of the result
146
+ #
147
+ # @return [String]
148
+ #
149
+ # @api public
150
+ def inspect
151
+ "#<#{self.class}#{to_h.inspect} errors=#{errors.inspect}>"
152
+ end
153
+
154
+ private
155
+
156
+ # A list of failure ASTs produced by rule result objects
157
+ #
158
+ # @api private
159
+ def result_ast
160
+ @__result__ast ||= results.map(&:to_ast)
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,45 @@
1
+ require 'dry/initializer'
2
+
3
+ require 'dry/schema/constants'
4
+ require 'dry/schema/config'
5
+ require 'dry/schema/result'
6
+ require 'dry/schema/messages'
7
+ require 'dry/schema/message_compiler'
8
+
9
+ module Dry
10
+ module Schema
11
+ # Applies rules defined within the DSL
12
+ #
13
+ # @api private
14
+ class RuleApplier
15
+ extend Dry::Initializer
16
+
17
+ # @api private
18
+ param :rules
19
+
20
+ # @api private
21
+ option :config, default: proc { Config.new }
22
+
23
+ # @api private
24
+ option :message_compiler, default: proc { MessageCompiler.new(Messages.setup(config)) }
25
+
26
+ # @api private
27
+ def call(input)
28
+ results = EMPTY_ARRAY.dup
29
+
30
+ rules.each do |name, rule|
31
+ next if input.error?(name)
32
+ result = rule.(input)
33
+ results << result if result.failure?
34
+ end
35
+
36
+ input.concat(results)
37
+ end
38
+
39
+ # @api private
40
+ def to_ast
41
+ [:set, rules.values.map(&:to_ast)]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,103 @@
1
+ require 'dry/schema/constants'
2
+ require 'dry/schema/compiler'
3
+ require 'dry/schema/predicate'
4
+
5
+ module Dry
6
+ module Schema
7
+ # Captures predicates defined within the DSL
8
+ #
9
+ # @api private
10
+ class Trace < BasicObject
11
+ INVALID_PREDICATES = %i[key?].freeze
12
+
13
+ include ::Dry::Equalizer(:compiler, :captures)
14
+
15
+ undef eql?
16
+
17
+ # @api private
18
+ attr_reader :compiler
19
+
20
+ # @api private
21
+ attr_reader :captures
22
+
23
+ # @api private
24
+ def initialize(compiler = Compiler.new)
25
+ @compiler = compiler
26
+ @captures = []
27
+ end
28
+
29
+ # @api private
30
+ def evaluate(*predicates, **opts, &block)
31
+ predicates.each do |predicate|
32
+ if predicate.respond_to?(:call)
33
+ append(predicate)
34
+ elsif predicate.is_a?(::Array)
35
+ append(predicate.map { |pred| __send__(pred) }.reduce(:|))
36
+ else
37
+ append(__send__(predicate))
38
+ end
39
+ end
40
+
41
+ opts.each do |predicate, *args|
42
+ append(__send__(predicate, *args))
43
+ end
44
+
45
+ self
46
+ end
47
+
48
+ # @api private
49
+ def append(op)
50
+ captures << op
51
+ self
52
+ end
53
+ alias_method :<<, :append
54
+
55
+ # @api private
56
+ def to_rule(name = nil)
57
+ return if captures.empty?
58
+
59
+ if name
60
+ compiler.visit([:key, [name, to_ast]])
61
+ else
62
+ reduced_rule
63
+ end
64
+ end
65
+
66
+ # @api private
67
+ def to_ast
68
+ reduced_rule.to_ast
69
+ end
70
+
71
+ # @api private
72
+ def class
73
+ ::Dry::Schema::Trace
74
+ end
75
+
76
+ private
77
+
78
+ # @api private
79
+ def reduced_rule
80
+ captures.map(&:to_ast).map(&compiler.method(:visit)).reduce(:and)
81
+ end
82
+
83
+ # @api private
84
+ def method_missing(meth, *args, &block)
85
+ if meth.to_s.end_with?(QUESTION_MARK)
86
+ if ::Dry::Schema::Trace::INVALID_PREDICATES.include?(meth)
87
+ ::Kernel.raise InvalidSchemaError, "#{meth} predicate cannot be used in this context"
88
+ end
89
+
90
+ unless compiler.supports?(meth)
91
+ ::Kernel.raise ::ArgumentError, "#{meth} predicate is not defined"
92
+ end
93
+
94
+ predicate = Predicate.new(compiler, meth, args, block)
95
+ predicate.ensure_valid
96
+ predicate
97
+ else
98
+ super
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,42 @@
1
+ require 'dry/schema/constants'
2
+ require 'dry/schema/types'
3
+
4
+ module Dry
5
+ module Schema
6
+ # A simple wrapper around Dry::Types registry
7
+ #
8
+ # This is used internally by specialized processor sub-classes
9
+ #
10
+ # @api private
11
+ class TypeRegistry
12
+ # @api private
13
+ attr_reader :types
14
+
15
+ # @api private
16
+ attr_reader :namespace
17
+
18
+ # @api private
19
+ def self.new(types = Dry::Types, namespace = nil)
20
+ super
21
+ end
22
+
23
+ # @api private
24
+ def initialize(types, namespace = nil)
25
+ @types = types
26
+ @namespace = namespace
27
+ end
28
+
29
+ # @api private
30
+ def namespaced(ns)
31
+ self.class.new(types, ns)
32
+ end
33
+
34
+ # @api private
35
+ def [](name)
36
+ key = [namespace, name].compact.join(DOT)
37
+ type = types.registered?(key) ? types[key] : types[name.to_s]
38
+ type
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ require 'dry/types'
2
+
3
+ module Dry
4
+ module Schema
5
+ # Schema's own type registry
6
+ #
7
+ # @api public
8
+ module Types
9
+ include Dry::Types.module
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ require 'dry/initializer'
2
+
3
+ module Dry
4
+ module Schema
5
+ # Used by the processors to coerce values in the input hash
6
+ #
7
+ # @api private
8
+ class ValueCoercer
9
+ extend Dry::Initializer
10
+
11
+ # @api private
12
+ param :type_schema
13
+
14
+ # @api private
15
+ def call(input)
16
+ if input.success?
17
+ type_schema[Hash(input)]
18
+ else
19
+ type_schema.member_types.reduce(EMPTY_HASH.dup) do |hash, (name, type)|
20
+ hash[name] = input.error?(name) ? input[name] : type[input[name]]
21
+ hash
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ module Dry
2
+ module Schema
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,255 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dry-schema
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Solnica
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-01-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-configurable
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 0.1.3
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '0.1'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.1.3
47
+ - !ruby/object:Gem::Dependency
48
+ name: dry-equalizer
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.2'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.2'
61
+ - !ruby/object:Gem::Dependency
62
+ name: dry-initializer
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.4'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.4'
75
+ - !ruby/object:Gem::Dependency
76
+ name: dry-logic
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 0.5.0
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: '0.5'
85
+ type: :runtime
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 0.5.0
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '0.5'
95
+ - !ruby/object:Gem::Dependency
96
+ name: dry-types
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 0.14.0
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '0.14'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 0.14.0
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '0.14'
115
+ - !ruby/object:Gem::Dependency
116
+ name: dry-core
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '0.2'
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 0.2.1
125
+ type: :runtime
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.2'
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: 0.2.1
135
+ - !ruby/object:Gem::Dependency
136
+ name: bundler
137
+ requirement: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ type: :development
143
+ prerelease: false
144
+ version_requirements: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ - !ruby/object:Gem::Dependency
150
+ name: rake
151
+ requirement: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ type: :development
157
+ prerelease: false
158
+ version_requirements: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ - !ruby/object:Gem::Dependency
164
+ name: rspec
165
+ requirement: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ type: :development
171
+ prerelease: false
172
+ version_requirements: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ description:
178
+ email:
179
+ - piotr.solnica@gmail.com
180
+ executables: []
181
+ extensions: []
182
+ extra_rdoc_files: []
183
+ files:
184
+ - CHANGELOG.md
185
+ - LICENSE
186
+ - README.md
187
+ - config/errors.yml
188
+ - lib/dry-schema.rb
189
+ - lib/dry/schema.rb
190
+ - lib/dry/schema/compiler.rb
191
+ - lib/dry/schema/config.rb
192
+ - lib/dry/schema/constants.rb
193
+ - lib/dry/schema/dsl.rb
194
+ - lib/dry/schema/extensions.rb
195
+ - lib/dry/schema/extensions/monads.rb
196
+ - lib/dry/schema/json.rb
197
+ - lib/dry/schema/key.rb
198
+ - lib/dry/schema/key_coercer.rb
199
+ - lib/dry/schema/key_map.rb
200
+ - lib/dry/schema/macros.rb
201
+ - lib/dry/schema/macros/core.rb
202
+ - lib/dry/schema/macros/dsl.rb
203
+ - lib/dry/schema/macros/each.rb
204
+ - lib/dry/schema/macros/filled.rb
205
+ - lib/dry/schema/macros/hash.rb
206
+ - lib/dry/schema/macros/key.rb
207
+ - lib/dry/schema/macros/maybe.rb
208
+ - lib/dry/schema/macros/optional.rb
209
+ - lib/dry/schema/macros/required.rb
210
+ - lib/dry/schema/macros/value.rb
211
+ - lib/dry/schema/message.rb
212
+ - lib/dry/schema/message_compiler.rb
213
+ - lib/dry/schema/message_compiler/visitor_opts.rb
214
+ - lib/dry/schema/message_set.rb
215
+ - lib/dry/schema/messages.rb
216
+ - lib/dry/schema/messages/abstract.rb
217
+ - lib/dry/schema/messages/i18n.rb
218
+ - lib/dry/schema/messages/namespaced.rb
219
+ - lib/dry/schema/messages/yaml.rb
220
+ - lib/dry/schema/params.rb
221
+ - lib/dry/schema/predicate.rb
222
+ - lib/dry/schema/predicate_inferrer.rb
223
+ - lib/dry/schema/predicate_registry.rb
224
+ - lib/dry/schema/processor.rb
225
+ - lib/dry/schema/result.rb
226
+ - lib/dry/schema/rule_applier.rb
227
+ - lib/dry/schema/trace.rb
228
+ - lib/dry/schema/type_registry.rb
229
+ - lib/dry/schema/types.rb
230
+ - lib/dry/schema/value_coercer.rb
231
+ - lib/dry/schema/version.rb
232
+ homepage: https://github.com/dry-rb/dry-schema
233
+ licenses:
234
+ - MIT
235
+ metadata: {}
236
+ post_install_message:
237
+ rdoc_options: []
238
+ require_paths:
239
+ - lib
240
+ required_ruby_version: !ruby/object:Gem::Requirement
241
+ requirements:
242
+ - - ">="
243
+ - !ruby/object:Gem::Version
244
+ version: '0'
245
+ required_rubygems_version: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '0'
250
+ requirements: []
251
+ rubygems_version: 3.0.2
252
+ signing_key:
253
+ specification_version: 4
254
+ summary: Schema coercion and validation
255
+ test_files: []