dry-schema 0.6.0 → 1.0.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/lib/dry/schema.rb +36 -3
  4. data/lib/dry/schema/constants.rb +8 -0
  5. data/lib/dry/schema/dsl.rb +7 -13
  6. data/lib/dry/schema/extensions/hints.rb +3 -0
  7. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +5 -0
  8. data/lib/dry/schema/extensions/hints/message_set_methods.rb +19 -1
  9. data/lib/dry/schema/extensions/hints/result_methods.rb +9 -2
  10. data/lib/dry/schema/extensions/monads.rb +10 -0
  11. data/lib/dry/schema/json.rb +1 -1
  12. data/lib/dry/schema/key.rb +3 -9
  13. data/lib/dry/schema/key_coercer.rb +1 -1
  14. data/lib/dry/schema/key_map.rb +2 -3
  15. data/lib/dry/schema/macros/array.rb +1 -2
  16. data/lib/dry/schema/macros/dsl.rb +81 -8
  17. data/lib/dry/schema/macros/each.rb +1 -1
  18. data/lib/dry/schema/macros/filled.rb +2 -1
  19. data/lib/dry/schema/macros/hash.rb +1 -1
  20. data/lib/dry/schema/macros/key.rb +32 -13
  21. data/lib/dry/schema/macros/maybe.rb +1 -1
  22. data/lib/dry/schema/macros/optional.rb +1 -1
  23. data/lib/dry/schema/macros/required.rb +1 -1
  24. data/lib/dry/schema/macros/schema.rb +1 -1
  25. data/lib/dry/schema/macros/value.rb +1 -1
  26. data/lib/dry/schema/message.rb +30 -1
  27. data/lib/dry/schema/message/or.rb +2 -0
  28. data/lib/dry/schema/message_set.rb +47 -2
  29. data/lib/dry/schema/messages/i18n.rb +4 -1
  30. data/lib/dry/schema/messages/namespaced.rb +1 -0
  31. data/lib/dry/schema/messages/template.rb +3 -3
  32. data/lib/dry/schema/messages/yaml.rb +10 -2
  33. data/lib/dry/schema/namespaced_rule.rb +12 -0
  34. data/lib/dry/schema/params.rb +1 -1
  35. data/lib/dry/schema/path.rb +35 -7
  36. data/lib/dry/schema/predicate.rb +13 -0
  37. data/lib/dry/schema/predicate_inferrer.rb +6 -9
  38. data/lib/dry/schema/processor.rb +7 -6
  39. data/lib/dry/schema/result.rb +6 -3
  40. data/lib/dry/schema/value_coercer.rb +1 -1
  41. data/lib/dry/schema/version.rb +1 -1
  42. metadata +7 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2ca1bd28c7d47ae6b6c0ed7af0a2fee06df7a852275cbbd5066c991cc5ebba2
4
- data.tar.gz: c038aa4babb93392d3fedfc26f07155dd194a5bf03c9c6d307a108159ac2f291
3
+ metadata.gz: 63100127599f36457192bd183784388f1ca2328c0bd91f4e4d2adb2498b57bb2
4
+ data.tar.gz: 82c6cad8a1bda4c89723a2ccccce7eab8bb56c9e90b0ce385cc4d632993be90c
5
5
  SHA512:
6
- metadata.gz: 32388257509b79914faa0b8ef3e8d8d15e9f25c84c1c882998acc240c67a5c43a35a2ac20a9b253b4f41456aba2f94cc80bfaa1edec36783ff55f90bc95a9363
7
- data.tar.gz: c6cd678fd48fcce00371789193446e1bdc19f01f84ccdc6c402fada537d6972bfafe6db5cf3e1a0d6aa2bb0ca9bf0bed98099081416ff4f3beac0eca8d02ef4e
6
+ metadata.gz: dbef0b527a8e4742b3845d8bebf45b88d960fa969956599f6504ce785fb9d2b7461ad8cc94e2e4b238956dc79641007d54be3b435f73551f2219f01db8b3f60d
7
+ data.tar.gz: c01c98b4600c7fcec9efa3f38f0cd6a27b86191804a7502b80da81d564282cb581ac5bc69c5c006cebb4826d0f7d6653230612a80f530a301494d1fe5af01203
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # 1.0.0 2019-05-03
2
+
3
+ ### Changed
4
+
5
+ * [BREAKING] `Result#to_hash` was removed (solnic)
6
+
7
+ ### Fixed
8
+
9
+ * Setting `:any` as the type spec no longer crashes (solnic)
10
+ * `Result#error?` handles paths to array elements correctly (solnic)
11
+
12
+ [Compare v0.6.0...v1.0.0](https://github.com/dry-rb/dry-schema/compare/v0.6.0...v1.0.0)
13
+
1
14
  # 0.6.0 2019-04-24
2
15
 
3
16
  ### Changed
data/lib/dry/schema.rb CHANGED
@@ -8,31 +8,64 @@ require 'dry/schema/params'
8
8
  require 'dry/schema/json'
9
9
 
10
10
  module Dry
11
+ # Main interface
12
+ #
13
+ # @api public
11
14
  module Schema
12
15
  extend Dry::Core::Extensions
13
16
 
14
17
  # Define a schema
15
18
  #
19
+ # @example
20
+ # Dry::Schema.define do
21
+ # required(:name).filled(:string)
22
+ # required(:age).value(:integer, gt?: 0)
23
+ # end
24
+ #
25
+ # @param [Hash] options
26
+ #
16
27
  # @return [Processor]
17
28
  #
29
+ # @see DSL.new
30
+ #
18
31
  # @api public
19
32
  def self.define(**options, &block)
20
33
  DSL.new(options, &block).call
21
34
  end
22
35
 
23
- # Define a param schema
36
+ # Define a schema suitable for HTTP params
37
+ #
38
+ # This schema type uses `Types::Params` for coercion by default
39
+ #
40
+ # @example
41
+ # Dry::Schema.Params do
42
+ # required(:name).filled(:string)
43
+ # required(:age).value(:integer, gt?: 0)
44
+ # end
24
45
  #
25
46
  # @return [Params]
26
47
  #
48
+ # @see Schema#define
49
+ #
27
50
  # @api public
28
51
  def self.Params(**options, &block)
29
52
  define(**options, processor_type: Params, &block)
30
53
  end
31
54
  singleton_class.send(:alias_method, :Form, :Params)
32
55
 
33
- # Define a JSON schema
56
+ # Define a schema suitable for JSON data
57
+ #
58
+ # This schema type uses `Types::JSON` for coercion by default
59
+ #
60
+ # @example
61
+ # Dry::Schema.JSON do
62
+ # required(:name).filled(:string)
63
+ # required(:age).value(:integer, gt?: 0)
64
+ # end
65
+ #
66
+ # @return [Params]
34
67
  #
35
- # @return [JSON]
68
+ # @see Schema#define
36
69
  #
37
70
  # @api public
38
71
  def self.JSON(**options, &block)
@@ -4,6 +4,9 @@ require 'pathname'
4
4
  require 'dry/core/constants'
5
5
 
6
6
  module Dry
7
+ # Common constants used across the library
8
+ #
9
+ # @api public
7
10
  module Schema
8
11
  include Core::Constants
9
12
 
@@ -11,11 +14,16 @@ module Dry
11
14
  QUESTION_MARK = '?'
12
15
  DOT = '.'
13
16
 
17
+ # Path to the default set of localized messages bundled within the gem
14
18
  DEFAULT_MESSAGES_PATH = Pathname(__dir__).join('../../../config/errors.yml').realpath.freeze
19
+
20
+ # Default namespace used for localized messages in YAML files
15
21
  DEFAULT_MESSAGES_ROOT = 'dry_schema'
16
22
 
23
+ # An error raised when DSL is used in an incorrect way
17
24
  InvalidSchemaError = Class.new(StandardError)
18
25
 
26
+ # An error raised when a localized message cannot be found
19
27
  MissingMessageError = Class.new(StandardError) do
20
28
  # @api private
21
29
  def initialize(path)
@@ -52,28 +52,22 @@ module Dry
52
52
 
53
53
  include ::Dry::Equalizer(:options)
54
54
 
55
- # @!attribute [r] compiler
56
- # @return [Compiler] The rule compiler object
55
+ # @return [Compiler] The rule compiler object
57
56
  option :compiler, default: -> { Compiler.new }
58
57
 
59
- # @!attribute [r] processor_type
60
- # @return [Compiler] The type of the processor (Params, JSON, or a custom sub-class)
58
+ # @return [Compiler] The type of the processor (Params, JSON, or a custom sub-class)
61
59
  option :processor_type, default: -> { Processor }
62
60
 
63
- # @!attribute [r] macros
64
- # @return [Array] An array with macros defined within the DSL
61
+ # @return [Array] An array with macros defined within the DSL
65
62
  option :macros, default: -> { EMPTY_ARRAY.dup }
66
63
 
67
- # @!attribute [r] types
68
- # @return [Compiler] A key=>type map defined within the DSL
64
+ # @return [Compiler] A key=>type map defined within the DSL
69
65
  option :types, default: -> { EMPTY_HASH.dup }
70
66
 
71
- # @!attribute [r] parent
72
- # @return [DSL] An optional parent DSL object that will be used to merge keys and rules
67
+ # @return [DSL] An optional parent DSL object that will be used to merge keys and rules
73
68
  option :parent, optional: true
74
69
 
75
- # @!attribute [r] config
76
- # @return [Config] Configuration object exposed via `#configure` method
70
+ # @return [Config] Configuration object exposed via `#configure` method
77
71
  option :config, optional: true, default: proc { parent ? parent.config.dup : Config.new }
78
72
 
79
73
  # Build a new DSL object and evaluate provided block
@@ -249,7 +243,7 @@ module Dry
249
243
 
250
244
  # Resolve type object from the provided spec
251
245
  #
252
- # @param [Symbol, Array<Symbol>, Dry::Types::Type]
246
+ # @param [Symbol, Array<Symbol>, Dry::Types::Type] spec
253
247
  #
254
248
  # @return [Dry::Types::Type]
255
249
  #
@@ -17,6 +17,8 @@ module Dry
17
17
  #
18
18
  # @api public
19
19
  class Message
20
+ # Hints extension for Or messages
21
+ #
20
22
  # @see Message::Or
21
23
  #
22
24
  # @api public
@@ -43,6 +45,7 @@ module Dry
43
45
  end
44
46
  end
45
47
 
48
+ # Hints extensions
46
49
  module Extensions
47
50
  Compiler.prepend(Hints::CompilerMethods)
48
51
  MessageCompiler.prepend(Hints::MessageCompilerMethods)
@@ -4,6 +4,9 @@ module Dry
4
4
  module Schema
5
5
  module Extensions
6
6
  module Hints
7
+ # Adds support for processing [:hint, ...] nodes produced by dry-logic
8
+ #
9
+ # @api private
7
10
  module MessageCompilerMethods
8
11
  HINT_TYPE_EXCLUSION = %i[
9
12
  key? nil? bool? str? int? float? decimal?
@@ -12,8 +15,10 @@ module Dry
12
15
 
13
16
  HINT_OTHER_EXCLUSION = %i[format? filled?].freeze
14
17
 
18
+ # @api private
15
19
  attr_reader :hints
16
20
 
21
+ # @api private
17
22
  def initialize(*args)
18
23
  super
19
24
  @hints = @options.fetch(:hints, true)
@@ -4,8 +4,19 @@ module Dry
4
4
  module Schema
5
5
  module Extensions
6
6
  module Hints
7
+ # Hint extensions for MessageSet
8
+ #
9
+ # @api public
7
10
  module MessageSetMethods
8
- attr_reader :hints, :failures
11
+ # Filtered message hints from all messages
12
+ #
13
+ # @return [Array<Message::Hint>]
14
+ attr_reader :hints
15
+
16
+ # Configuration option to enable/disable showing errors
17
+ #
18
+ # @return [Boolean]
19
+ attr_reader :failures
9
20
 
10
21
  # @api private
11
22
  def initialize(messages, options = EMPTY_HASH)
@@ -14,6 +25,13 @@ module Dry
14
25
  @failures = options.fetch(:failures, true)
15
26
  end
16
27
 
28
+ # Dump message set to a hash with either all messages or just hints
29
+ #
30
+ # @see MessageSet#to_h
31
+ # @see ResultMethods#hints
32
+ #
33
+ # @return [Hash<Symbol=>Array<String>>]
34
+ #
17
35
  # @api public
18
36
  def to_h
19
37
  @to_h ||= failures ? messages_map : messages_map(hints)
@@ -4,9 +4,16 @@ module Dry
4
4
  module Schema
5
5
  module Extensions
6
6
  module Hints
7
+ # Get errors exclusively without hints
8
+ #
9
+ # @api public
7
10
  module ResultMethods
11
+ # Return error messages exclusively
12
+ #
8
13
  # @see Result#errors
9
14
  #
15
+ # @return [MessageSet]
16
+ #
10
17
  # @api public
11
18
  def errors(options = EMPTY_HASH)
12
19
  message_set(options.merge(hints: false))
@@ -16,7 +23,7 @@ module Dry
16
23
  #
17
24
  # @see #message_set
18
25
  #
19
- # @return [Hash<Symbol=>Array>]
26
+ # @return [MessageSet]
20
27
  #
21
28
  # @api public
22
29
  def messages(options = EMPTY_HASH)
@@ -27,7 +34,7 @@ module Dry
27
34
  #
28
35
  # @see #message_set
29
36
  #
30
- # @return [Hash<Symbol=>Array>]
37
+ # @return [MessageSet]
31
38
  #
32
39
  # @api public
33
40
  def hints(options = EMPTY_HASH)
@@ -4,9 +4,19 @@ require 'dry/monads/result'
4
4
 
5
5
  module Dry
6
6
  module Schema
7
+ # Monad extension for Result
8
+ #
9
+ # @api public
7
10
  class Result
8
11
  include Dry::Monads::Result::Mixin
9
12
 
13
+ # Turn result into a monad
14
+ #
15
+ # This makes result objects work with dry-monads (or anything with a compatible interface)
16
+ #
17
+ # @return [Dry::Monads::Success,Dry::Monads::Failure]
18
+ #
19
+ # @api public
10
20
  def to_monad
11
21
  if success?
12
22
  Success(self)
@@ -7,7 +7,7 @@ module Dry
7
7
  # JSON schema type
8
8
  #
9
9
  # @see Processor
10
- # @see Schema.JSON
10
+ # @see Schema#JSON
11
11
  #
12
12
  # @api public
13
13
  class JSON < Processor
@@ -12,19 +12,13 @@ module Dry
12
12
 
13
13
  include Dry.Equalizer(:name, :coercer)
14
14
 
15
- # @!attribute[r] id
16
- # @return [Symbol] The key identifier
17
- # @api public
15
+ # @return [Symbol] The key identifier
18
16
  attr_reader :id
19
17
 
20
- # @!attribute[r] name
21
- # @return [Symbol, String, Object] The actual key name expected in an input hash
22
- # @api public
18
+ # @return [Symbol, String, Object] The actual key name expected in an input hash
23
19
  attr_reader :name
24
20
 
25
- # @!attribute[r] id
26
- # @return [Proc, #call] A key name coercer function
27
- # @api public
21
+ # @return [Proc, #call] A key name coercer function
28
22
  attr_reader :coercer
29
23
 
30
24
  # @api private
@@ -33,7 +33,7 @@ module Dry
33
33
 
34
34
  # @api private
35
35
  def call(source)
36
- key_map.write(Hash(source))
36
+ key_map.write(source.to_h)
37
37
  end
38
38
  alias_method :[], :call
39
39
  end
@@ -21,8 +21,7 @@ module Dry
21
21
  include Dry.Equalizer(:keys)
22
22
  include Enumerable
23
23
 
24
- # @!attribute[r] keys
25
- # @return [Array<Key>] A list of defined key objects
24
+ # @return [Array<Key>] A list of defined key objects
26
25
  attr_reader :keys
27
26
 
28
27
  # Coerce a list of key specs into a key map
@@ -41,7 +40,7 @@ module Dry
41
40
 
42
41
  # Build new, or returned a cached instance of a key map
43
42
  #
44
- # @param [Array<Symbol, Array, Hash<Symbol=>Array>>
43
+ # @param [Array<Symbol>, Array, Hash<Symbol=>Array>] args
45
44
  #
46
45
  # @return [KeyMap]
47
46
  def self.new(*args)
@@ -7,7 +7,7 @@ module Dry
7
7
  module Macros
8
8
  # Macro used to specify predicates for each element of an array
9
9
  #
10
- # @api public
10
+ # @api private
11
11
  class Array < DSL
12
12
  # @api private
13
13
  def value(*args, **opts, &block)
@@ -32,7 +32,6 @@ module Dry
32
32
  def to_ast(*)
33
33
  [:and, [trace.array?.to_ast, [:each, trace.to_ast]]]
34
34
  end
35
-
36
35
  alias_method :ast, :to_ast
37
36
  end
38
37
  end
@@ -16,15 +16,37 @@ module Dry
16
16
  undef :eql?
17
17
  undef :nil?
18
18
 
19
- # @api private
19
+ # @!attribute [r] chain
20
+ # Indicate if the macro should append its rules to the provided trace
21
+ # @return [Boolean]
22
+ # @api private
20
23
  option :chain, default: -> { true }
21
24
 
22
25
  # @!attribute [r] predicate_inferrer
26
+ # PredicateInferrer is used to infer predicate type-check from a type spec
23
27
  # @return [PredicateInferrer]
24
28
  # @api private
25
29
  option :predicate_inferrer, default: proc { PredicateInferrer.new(compiler.predicates) }
26
30
 
27
- # Specify predicates that should be applied to a value
31
+ # @overload value(*predicates, **predicate_opts)
32
+ # Set predicates without and with arguments
33
+ #
34
+ # @param [Array<Symbol>] predicates
35
+ # @param [Hash] predicate_opts
36
+ #
37
+ # @example with a predicate
38
+ # required(:name).value(:filled?)
39
+ #
40
+ # @example with a predicate with arguments
41
+ # required(:name).value(min_size?: 2)
42
+ #
43
+ # @example with a predicate with and without arguments
44
+ # required(:name).value(:filled?, min_size?: 2)
45
+ #
46
+ # @example with a block
47
+ # required(:name).value { filled? & min_size?(2) }
48
+ #
49
+ # @return [Macros::Core]
28
50
  #
29
51
  # @api public
30
52
  def value(*predicates, **opts, &block)
@@ -35,6 +57,14 @@ module Dry
35
57
 
36
58
  # Prepends `:filled?` predicate
37
59
  #
60
+ # @example with a type spec
61
+ # required(:name).filled(:string)
62
+ #
63
+ # @example with a type spec and a predicate
64
+ # required(:name).filled(:string, format?: /\w+/)
65
+ #
66
+ # @return [Macros::Core]
67
+ #
38
68
  # @api public
39
69
  def filled(*args, &block)
40
70
  append_macro(Macros::Filled) do |macro|
@@ -42,7 +72,18 @@ module Dry
42
72
  end
43
73
  end
44
74
 
45
- # Specify a nested hash without enforced hash? type-check
75
+ # Specify a nested hash without enforced `hash?` type-check
76
+ #
77
+ # This is a simpler building block than `hash` macro, use it
78
+ # when you want to provide `hash?` type-check with other rules
79
+ # manually.
80
+ #
81
+ # @example
82
+ # required(:tags).value(:hash, min_size?: 1).schema do
83
+ # required(:name).value(:string)
84
+ # end
85
+ #
86
+ # @return [Macros::Core]
46
87
  #
47
88
  # @api public
48
89
  def schema(*args, &block)
@@ -51,9 +92,12 @@ module Dry
51
92
  end
52
93
  end
53
94
 
54
- # Specify a nested hash with enforced hash? type-check
95
+ # Specify a nested hash with enforced `hash?` type-check
55
96
  #
56
- # @see #schema
97
+ # @example
98
+ # required(:tags).hash do
99
+ # required(:name).value(:string)
100
+ # end
57
101
  #
58
102
  # @api public
59
103
  def hash(*args, &block)
@@ -64,6 +108,20 @@ module Dry
64
108
 
65
109
  # Specify predicates that should be applied to each element of an array
66
110
  #
111
+ # This is a simpler building block than `array` macro, use it
112
+ # when you want to provide `array?` type-check with other rules
113
+ # manually.
114
+ #
115
+ # @example a list of strings
116
+ # required(:tags).value(:array, min_size?: 2).each(:str?)
117
+ #
118
+ # @example a list of hashes
119
+ # required(:tags).value(:array, min_size?: 2).each(:hash) do
120
+ # required(:name).filled(:string)
121
+ # end
122
+ #
123
+ # @return [Macros::Core]
124
+ #
67
125
  # @api public
68
126
  def each(*args, &block)
69
127
  append_macro(Macros::Each) do |macro|
@@ -71,7 +129,17 @@ module Dry
71
129
  end
72
130
  end
73
131
 
74
- # Like `each`, but prepends `array?` check
132
+ # Like `each` but sets `array?` type-check
133
+ #
134
+ # @example a list of strings
135
+ # required(:tags).array(:str?)
136
+ #
137
+ # @example a list of hashes
138
+ # required(:tags).array(:hash) do
139
+ # required(:name).filled(:string)
140
+ # end
141
+ #
142
+ # @return [Macros::Core]
75
143
  #
76
144
  # @api public
77
145
  def array(*args, &block)
@@ -82,7 +150,10 @@ module Dry
82
150
 
83
151
  # Set type spec
84
152
  #
85
- # @param [Symbol, Array, Dry::Types::Type]
153
+ # @example
154
+ # required(:name).type(:string).value(min_size?: 2)
155
+ #
156
+ # @param [Symbol, Array, Dry::Types::Type] spec
86
157
  #
87
158
  # @return [Macros::Key]
88
159
  #
@@ -129,13 +200,15 @@ module Dry
129
200
 
130
201
  type_predicates = predicate_inferrer[resolved_type]
131
202
 
132
- unless predicates.include?(type_predicates)
203
+ unless type_predicates.empty? || predicates.include?(type_predicates)
133
204
  if type_predicates.is_a?(::Array) && type_predicates.size.equal?(1)
134
205
  predicates.unshift(type_predicates[0])
135
206
  else
136
207
  predicates.unshift(type_predicates)
137
208
  end
138
209
  end
210
+
211
+ return self if predicates.empty?
139
212
  end
140
213
 
141
214
  yield(*predicates, type_spec: type_spec)
@@ -7,7 +7,7 @@ module Dry
7
7
  module Macros
8
8
  # Macro used to specify predicates for each element of an array
9
9
  #
10
- # @api public
10
+ # @api private
11
11
  class Each < DSL
12
12
  # @api private
13
13
  def value(*args, **opts)
@@ -7,8 +7,9 @@ module Dry
7
7
  module Macros
8
8
  # Macro used to prepend `:filled?` predicate
9
9
  #
10
- # @api public
10
+ # @api private
11
11
  class Filled < Value
12
+ # @api private
12
13
  def call(*predicates, **opts, &block)
13
14
  if predicates.include?(:empty?)
14
15
  raise ::Dry::Schema::InvalidSchemaError, 'Using filled with empty? predicate is invalid'
@@ -7,7 +7,7 @@ module Dry
7
7
  module Macros
8
8
  # Macro used to specify a nested schema
9
9
  #
10
- # @api public
10
+ # @api private
11
11
  class Hash < Schema
12
12
  # @api private
13
13
  def call(*args, &block)
@@ -8,21 +8,20 @@ require 'dry/schema/constants'
8
8
  module Dry
9
9
  module Schema
10
10
  module Macros
11
- # Base macro for specifying rules applied to a value found under the key
12
- #
13
- # @see DSL#key
11
+ # Base macro for specifying rules applied to a value found under a key
14
12
  #
15
13
  # @api public
16
14
  class Key < DSL
17
- # @!attribute [r] filter_schema
18
- # @return [Schema::DSL]
19
- # @api private
15
+ # @return [Schema::DSL]
16
+ # @api private
20
17
  option :filter_schema, optional: true, default: proc { schema_dsl&.new }
21
18
 
22
- # Specify predicates that should be used to filter out values
23
- # before coercion is applied
19
+ # Specify predicates that should be applied before coercion
24
20
  #
25
- # @see Macros::DSL#value
21
+ # @example check format before coercing to a date
22
+ # required(:publish_date).filter(format?: /\d{4}-\d{2}-\d{2}).value(:date)
23
+ #
24
+ # @see Macros::Key#value
26
25
  #
27
26
  # @return [Macros::Key]
28
27
  #
@@ -32,12 +31,26 @@ module Dry
32
31
  self
33
32
  end
34
33
 
35
- # Set type specification and predicates
34
+ # @overload value(type_spec, *predicates, **predicate_opts)
35
+ # Set type specification and predicates
36
36
  #
37
- # @see Macros::DSL#value
37
+ # @param [Symbol,Types::Type,Array] type_spec
38
+ # @param [Array<Symbol>] predicates
39
+ # @param [Hash] predicate_opts
40
+ #
41
+ # @example with a predicate
42
+ # required(:name).value(:string, :filled?)
43
+ #
44
+ # @example with a predicate with arguments
45
+ # required(:name).value(:string, min_size?: 2)
46
+ #
47
+ # @example with a block
48
+ # required(:name).value(:string) { filled? & min_size?(2) }
38
49
  #
39
50
  # @return [Macros::Key]
40
51
  #
52
+ # @see Macros::DSL#value
53
+ #
41
54
  # @api public
42
55
  def value(*args, **opts, &block)
43
56
  extract_type_spec(*args) do |*predicates, type_spec:|
@@ -47,7 +60,10 @@ module Dry
47
60
 
48
61
  # Set type specification and predicates for a filled value
49
62
  #
50
- # @see Macros::DSL#value
63
+ # @example
64
+ # required(:name).filled(:string)
65
+ #
66
+ # @see Macros::Key#value
51
67
  #
52
68
  # @return [Macros::Key]
53
69
  #
@@ -60,7 +76,10 @@ module Dry
60
76
 
61
77
  # Set type specification and predicates for a maybe value
62
78
  #
63
- # @see Macros::DSL#value
79
+ # @example
80
+ # required(:name).maybe(:string)
81
+ #
82
+ # @see Macros::Key#value
64
83
  #
65
84
  # @return [Macros::Key]
66
85
  #
@@ -7,7 +7,7 @@ module Dry
7
7
  module Macros
8
8
  # Macro used to specify predicates for a value that can be `nil`
9
9
  #
10
- # @api public
10
+ # @api private
11
11
  class Maybe < DSL
12
12
  # @api private
13
13
  def call(*args, **opts, &block)
@@ -7,7 +7,7 @@ module Dry
7
7
  module Macros
8
8
  # A Key specialization used for keys that can be skipped
9
9
  #
10
- # @api public
10
+ # @api private
11
11
  class Optional < Key
12
12
  # @api private
13
13
  def operation
@@ -7,7 +7,7 @@ module Dry
7
7
  module Macros
8
8
  # A Key specialization used for keys that must be present
9
9
  #
10
- # @api public
10
+ # @api private
11
11
  class Required < Key
12
12
  # @api private
13
13
  def operation
@@ -7,7 +7,7 @@ module Dry
7
7
  module Macros
8
8
  # Macro used to specify a nested schema
9
9
  #
10
- # @api public
10
+ # @api private
11
11
  class Schema < Value
12
12
  # @api private
13
13
  def call(*args, &block)
@@ -7,7 +7,7 @@ module Dry
7
7
  module Macros
8
8
  # A macro used for specifying predicates to be applied to values from a hash
9
9
  #
10
- # @api public
10
+ # @api private
11
11
  class Value < DSL
12
12
  # @api private
13
13
  def call(*predicates, **opts, &block)
@@ -16,16 +16,34 @@ module Dry
16
16
 
17
17
  extend Dry::Initializer
18
18
 
19
+ # @!attribute [r] text
20
+ # Message text representation created from a localized template
21
+ # @return [String]
19
22
  option :text
20
23
 
24
+ # @!attribute [r] path
25
+ # Path to the value
26
+ # @return [String]
21
27
  option :path
22
28
 
29
+ # @!attribute [r] predicate
30
+ # Predicate identifier that was used to produce a message
31
+ # @return [Symbol]
23
32
  option :predicate
24
33
 
34
+ # @!attribute [r] args
35
+ # Optional list of arguments used by the predicate
36
+ # @return [Array]
25
37
  option :args, default: proc { EMPTY_ARRAY }
26
38
 
39
+ # @!attribute [r] input
40
+ # The input value
41
+ # @return [Object]
27
42
  option :input
28
43
 
44
+ # @!attribute [r] meta
45
+ # Arbitrary meta data
46
+ # @return [Hash]
29
47
  option :meta, optional: true, default: proc { EMPTY_HASH }
30
48
 
31
49
  # Dump the message to a representation suitable for the message set hash
@@ -38,16 +56,27 @@ module Dry
38
56
  end
39
57
  alias to_s dump
40
58
 
59
+ # See if another message is the same
60
+ #
61
+ # If a string is passed, it will be compared with the text
62
+ #
63
+ # @param [Message,String]
64
+ #
65
+ # @return [Boolean]
66
+ #
41
67
  # @api private
42
68
  def eql?(other)
43
69
  other.is_a?(String) ? text == other : super
44
70
  end
45
71
 
72
+ # See which message is higher in the hierarchy
73
+ #
74
+ # @api private
46
75
  def <=>(other)
47
76
  l_path = Path[path]
48
77
  r_path = Path[other.path]
49
78
 
50
- unless l_path.include?(r_path)
79
+ unless l_path.same_root?(r_path)
51
80
  raise ArgumentError, 'Cannot compare messages from different root paths'
52
81
  end
53
82
 
@@ -32,6 +32,8 @@ module Dry
32
32
  @path = left.path
33
33
  end
34
34
 
35
+ # Dump a message into a string
36
+ #
35
37
  # @see Message#dump
36
38
  #
37
39
  # @return [String]
@@ -13,7 +13,21 @@ module Dry
13
13
  include Enumerable
14
14
  include Dry::Equalizer(:messages, :options)
15
15
 
16
- attr_reader :messages, :placeholders, :options
16
+ # A list of compiled message objects
17
+ #
18
+ # @return [Array<Message>]
19
+ attr_reader :messages
20
+
21
+ # An internal hash that is filled in with dumped messages
22
+ # when a message set is coerced to a hash
23
+ #
24
+ # @return [Hash<Symbol=>[Array,Hash]>]
25
+ attr_reader :placeholders
26
+
27
+ # Options hash
28
+ #
29
+ # @return [Hash]
30
+ attr_reader :options
17
31
 
18
32
  # @api private
19
33
  def self.[](messages, options = EMPTY_HASH)
@@ -27,6 +41,15 @@ module Dry
27
41
  initialize_placeholders!
28
42
  end
29
43
 
44
+ # Iterate over messages
45
+ #
46
+ # @example
47
+ # result.errors.each do |message|
48
+ # puts message.text
49
+ # end
50
+ #
51
+ # @return [Array]
52
+ #
30
53
  # @api public
31
54
  def each(&block)
32
55
  return self if empty?
@@ -35,23 +58,45 @@ module Dry
35
58
  messages.each(&block)
36
59
  end
37
60
 
61
+ # Dump message set to a hash
62
+ #
63
+ # @return [Hash<Symbol=>Array<String>>]
64
+ #
38
65
  # @api public
39
66
  def to_h
40
67
  @to_h ||= messages_map
41
68
  end
42
69
  alias_method :to_hash, :to_h
43
70
 
71
+ # Get a list of message texts for the given key
72
+ #
73
+ # @param [Symbol] key
74
+ #
75
+ # @return [Array<String>]
76
+ #
44
77
  # @api public
45
78
  def [](key)
46
79
  to_h[key]
47
80
  end
48
81
 
82
+ # Get a list of message texts for the given key
83
+ #
84
+ # @param [Symbol] key
85
+ #
86
+ # @return [Array<String>]
87
+ #
88
+ # @raise KeyError
89
+ #
49
90
  # @api public
50
91
  def fetch(key)
51
92
  self[key] || raise(KeyError, "+#{key}+ message was not found")
52
93
  end
53
94
 
54
- # @api private
95
+ # Check if a message set is empty
96
+ #
97
+ # @return [Boolean]
98
+ #
99
+ # @api public
55
100
  def empty?
56
101
  @empty ||= messages.empty?
57
102
  end
@@ -9,6 +9,9 @@ module Dry
9
9
  #
10
10
  # @api public
11
11
  class Messages::I18n < Messages::Abstract
12
+ # Translation function
13
+ #
14
+ # @return [Method]
12
15
  attr_reader :t
13
16
 
14
17
  # @api private
@@ -82,7 +85,7 @@ module Dry
82
85
  def store_translations(data)
83
86
  locales = data.keys.map(&:to_sym)
84
87
 
85
- I18n.available_locales += locales
88
+ I18n.available_locales |= locales
86
89
 
87
90
  locales.each do |locale|
88
91
  I18n.backend.store_translations(locale, data[locale.to_s])
@@ -59,6 +59,7 @@ module Dry
59
59
  super(tokens.merge(root: "#{tokens[:root]}.#{namespace}")) + super
60
60
  end
61
61
 
62
+ # @api private
62
63
  def rule_lookup_paths(tokens)
63
64
  base_paths = messages.rule_lookup_paths(tokens)
64
65
  base_paths.map { |key| key.gsub('dry_schema', "dry_schema.#{namespace}") } + base_paths
@@ -17,15 +17,15 @@ module Dry
17
17
  TOKEN_REGEXP = /%{([\w\d]*)}/
18
18
 
19
19
  # !@attribute [r] text
20
- # @return [String]
20
+ # @return [String]
21
21
  attr_reader :text
22
22
 
23
23
  # !@attribute [r] tokens
24
- # @return [Hash]
24
+ # @return [Hash]
25
25
  attr_reader :tokens
26
26
 
27
27
  # !@attribute [r] evaluator
28
- # @return [Proc]
28
+ # @return [Proc]
29
29
  attr_reader :evaluator
30
30
 
31
31
  # @api private
@@ -17,7 +17,15 @@ module Dry
17
17
 
18
18
  include Dry::Equalizer(:data)
19
19
 
20
- attr_reader :data, :t
20
+ # Loaded localized message templates
21
+ #
22
+ # @return [Hash]
23
+ attr_reader :data
24
+
25
+ # Translation function
26
+ #
27
+ # @return [Proc]
28
+ attr_reader :t
21
29
 
22
30
  # @api private
23
31
  def self.build(options = EMPTY_HASH)
@@ -81,7 +89,7 @@ module Dry
81
89
 
82
90
  # Merge messages from an additional path
83
91
  #
84
- # @param [String] path
92
+ # @param [String] overrides
85
93
  #
86
94
  # @return [Messages::I18n]
87
95
  #
@@ -2,21 +2,33 @@
2
2
 
3
3
  module Dry
4
4
  module Schema
5
+ # A special rule type that is configured under a specified namespace
6
+ #
7
+ # This is used internally to create rules that can be properly handled
8
+ # by the message compiler in situations where a schema reuses another schema
9
+ # but it is configured to use a message namespace
10
+ #
11
+ # @api private
5
12
  class NamespacedRule
13
+ # @api private
6
14
  attr_reader :rule
7
15
 
16
+ # @api private
8
17
  attr_reader :namespace
9
18
 
19
+ # @api private
10
20
  def initialize(namespace, rule)
11
21
  @namespace = namespace
12
22
  @rule = rule
13
23
  end
14
24
 
25
+ # @api private
15
26
  def call(input)
16
27
  result = rule.call(input)
17
28
  Logic::Result.new(result.success?) { [:namespace, [namespace, result.to_ast]] }
18
29
  end
19
30
 
31
+ # @api private
20
32
  def ast(input=Undefined)
21
33
  [:namespace, [namespace, rule.ast(input)]]
22
34
  end
@@ -7,7 +7,7 @@ module Dry
7
7
  # Params schema type
8
8
  #
9
9
  # @see Processor
10
- # @see Schema.Params
10
+ # @see Schema#Params
11
11
  #
12
12
  # @api public
13
13
  class Params < Processor
@@ -8,15 +8,17 @@ module Dry
8
8
  #
9
9
  # @api private
10
10
  class Path
11
+ include Comparable
11
12
  include Enumerable
12
13
 
13
- # !@attribute [r] keys
14
- # @return [Array<Symbol>]
14
+ # @return [Array<Symbol>]
15
15
  attr_reader :keys
16
16
 
17
+ alias_method :root, :first
18
+
17
19
  # Coerce a spec into a path object
18
20
  #
19
- # @param [Symbol, String, Hash, Array<Symbol>] spec
21
+ # @param [Path, Symbol, String, Hash, Array<Symbol>] spec
20
22
  #
21
23
  # @return [Path]
22
24
  #
@@ -29,10 +31,10 @@ module Dry
29
31
  new(spec.split(DOT).map(&:to_sym))
30
32
  when Hash
31
33
  new(keys_from_hash(spec))
32
- when self
34
+ when Path
33
35
  spec
34
36
  else
35
- raise ArgumentError, '+spec+ must be either a Symbol, Array or Hash'
37
+ raise ArgumentError, '+spec+ must be either a Symbol, Array, Hash or a Path'
36
38
  end
37
39
  end
38
40
 
@@ -62,12 +64,38 @@ module Dry
62
64
 
63
65
  # @api private
64
66
  def include?(other)
65
- !find { |key| (idx = other.index(key)) && keys[idx].equal?(key) }.nil?
67
+ return false unless same_root?(other)
68
+ return false if index? && other.index? && !last.equal?(other.last)
69
+ self >= other
66
70
  end
67
71
 
68
72
  # @api private
69
73
  def <=>(other)
70
- keys.count <=> other.count
74
+ raise ArgumentError, "Can't compare paths from different branches" unless same_root?(other)
75
+
76
+ return 0 if keys.eql?(other.keys)
77
+
78
+ res =
79
+ map { |key| (idx = other.index(key)) && keys[idx].equal?(key) }
80
+ .compact
81
+ .reject { |value| value.equal?(false) }
82
+
83
+ res.size < count ? 1 : -1
84
+ end
85
+
86
+ # @api private
87
+ def last
88
+ keys.last
89
+ end
90
+
91
+ # @api private
92
+ def same_root?(other)
93
+ root.equal?(other.root)
94
+ end
95
+
96
+ # @api private
97
+ def index?
98
+ last.is_a?(Integer)
71
99
  end
72
100
  end
73
101
  end
@@ -21,6 +21,10 @@ module Dry
21
21
  @predicate = predicate
22
22
  end
23
23
 
24
+ # Dump negated predicate to an AST
25
+ #
26
+ # @return [Array]
27
+ #
24
28
  # @api private
25
29
  def to_ast(*args)
26
30
  [:not, predicate.to_ast(*args)]
@@ -53,6 +57,9 @@ module Dry
53
57
 
54
58
  # Negate a predicate
55
59
  #
60
+ # @example
61
+ # required(:name).value(:string) { !empty? }
62
+ #
56
63
  # @return [Negation]
57
64
  #
58
65
  # @api public
@@ -67,11 +74,17 @@ module Dry
67
74
  end
68
75
  end
69
76
 
77
+ # Compile predicate to a rule object
78
+ #
70
79
  # @api private
71
80
  def to_rule
72
81
  compiler.visit(to_ast)
73
82
  end
74
83
 
84
+ # Dump predicate to an AST
85
+ #
86
+ # @return [Array]
87
+ #
75
88
  # @api private
76
89
  def to_ast(*)
77
90
  [:predicate, [name, compiler.predicates.arg_list(name, *args)]]
@@ -28,9 +28,8 @@ module Dry
28
28
  #
29
29
  # @api private
30
30
  class Compiler
31
- # @!attribute [r] registry
32
- # @return [PredicateRegistry]
33
- # @api private
31
+ # @return [PredicateRegistry]
32
+ # @api private
34
33
  attr_reader :registry
35
34
 
36
35
  # @api private
@@ -72,9 +71,8 @@ module Dry
72
71
  end
73
72
 
74
73
  # @api private
75
- def visit_safe(node)
76
- other, * = node
77
- visit(other)
74
+ def visit_lax(node)
75
+ visit(node)
78
76
  end
79
77
 
80
78
  # @api private
@@ -114,9 +112,8 @@ module Dry
114
112
  end
115
113
  end
116
114
 
117
- # @!attribute [r] compiler
118
- # @return [Compiler]
119
- # @api private
115
+ # @return [Compiler]
116
+ # @api private
120
117
  attr_reader :compiler
121
118
 
122
119
  # @api private
@@ -33,16 +33,17 @@ module Dry
33
33
  param :steps, default: -> { EMPTY_ARRAY.dup }
34
34
 
35
35
  class << self
36
- # @!attribute [r] definition
37
- # Return DSL configured via #define
38
- # @return [DSL]
39
- # @api private
36
+ # Return DSL configured via #define
37
+ #
38
+ # @return [DSL]
39
+ # @api private
40
40
  attr_reader :definition
41
41
 
42
42
  # Define a schema for your processor class
43
43
  #
44
- # @see Params
45
- # @see JSON
44
+ # @see Schema#define
45
+ # @see Schema#Params
46
+ # @see Schema#JSON
46
47
  #
47
48
  # @return [Class]
48
49
  #
@@ -19,8 +19,11 @@ module Dry
19
19
 
20
20
  # @api private
21
21
  param :output
22
+
23
+ # Dump result to a hash returning processed and validated data
24
+ #
25
+ # @return [Hash]
22
26
  alias_method :to_h, :output
23
- alias_method :to_hash, :output
24
27
 
25
28
  # @api private
26
29
  param :results, default: -> { EMPTY_ARRAY.dup }
@@ -72,7 +75,7 @@ module Dry
72
75
 
73
76
  # Check if there's an error for the provided spec
74
77
  #
75
- # @param [Symbol, Hash<Symbol=>Symbol>] name
78
+ # @param [Symbol, Hash<Symbol=>Symbol>] spec
76
79
  #
77
80
  # @return [Boolean]
78
81
  #
@@ -103,7 +106,7 @@ module Dry
103
106
  #
104
107
  # @see #message_set
105
108
  #
106
- # @return [Hash<Symbol=>Array>]
109
+ # @return [MessageSet]
107
110
  #
108
111
  # @api public
109
112
  def errors(options = EMPTY_HASH)
@@ -18,7 +18,7 @@ module Dry
18
18
  # @api private
19
19
  def call(input)
20
20
  if input.success?
21
- type_schema[Hash(input)]
21
+ type_schema[input.to_h]
22
22
  else
23
23
  type_schema.each_with_object(EMPTY_HASH.dup) do |key, hash|
24
24
  name = key.name
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Schema
5
- VERSION = '0.6.0'
5
+ VERSION = '1.0.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-24 00:00:00.000000000 Z
11
+ date: 2019-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -156,7 +156,10 @@ dependencies:
156
156
  - - ">="
157
157
  - !ruby/object:Gem::Version
158
158
  version: '0'
159
- description:
159
+ description: |
160
+ dry-schema provides a DSL for defining schemas with keys and rules that should be applied to
161
+ values. It supports coercion, input sanitization, custom types and localized error messages
162
+ (with or without I18n gem). It's also used as the schema engine in dry-validation.
160
163
  email:
161
164
  - piotr.solnica@gmail.com
162
165
  executables: []
@@ -244,5 +247,5 @@ requirements: []
244
247
  rubygems_version: 3.0.3
245
248
  signing_key:
246
249
  specification_version: 4
247
- summary: Schema coercion and validation
250
+ summary: Coercion and validation for data structures
248
251
  test_files: []