dry-schema 0.6.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []