dry-schema 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 149b82a73e8cd4d31c55fe1cf2bee1a7ebfb2afd696677522293da435f0224b1
4
- data.tar.gz: ca76ca603a34906e61cfbd075e9667c97ee191591520e89b8a5d2e949c124426
3
+ metadata.gz: af5d9afb30c697c7ff8e028dff66ed3a1e28a8d0ed9c6f03ac4bd0e0ff73a364
4
+ data.tar.gz: 94c6bb4243dfc12bd478e715e716deb1fd9b278761d1822324ed7b8920b0de55
5
5
  SHA512:
6
- metadata.gz: 0b94f326035f884457e2b6ce84ed427d2ec31011529cb8c4a987ff8c26a1bf4b66581ccd1e14ebe91cd4a0efbd05c9c04be6c47cfe959fa0f08687515f02d364
7
- data.tar.gz: 356ae43e85fc3afdc0e8abd2628c06315ce5ed97d3ec74c40a3a32dd73b1300851c0f1f8c674abb73d3d8723292dfe0bf370ec5f5fcf1fd7f21fc5a8eb16633c
6
+ metadata.gz: cf8a06bf211c79bed5ccfc9e02800e47318535036a47b626c29438cae91e3388c380bd9b8b936391718f88c784318f9ef5e88d41a19d06240d9773d2ee7b8270
7
+ data.tar.gz: 7eaa15a99a94bdcd301fed7d636879a0ef7241d7cf5721bd87ef053c8684ece4b65a0a6aa6fb2f10d7d0947bb66b27548f575749b83190ea2db3bab04f4f08d2
@@ -1,3 +1,34 @@
1
+ # 0.4.0 2019-03-26
2
+
3
+ ### Added
4
+
5
+ * Schemas are now compatible with procs via `#to_proc` (issue #53) (solnic)
6
+ * Support for configuring `top_namespace` for localized messages (solnic)
7
+ * Support for configuring more than one load path for localized messages (solnic)
8
+ * Support for inferring predicates from arbitrary types (issue #101) (solnic)
9
+
10
+ ### Fixed
11
+
12
+ * Handling of messages for `optional` keys without value rules works correctly (issue #87) (solnic)
13
+ * Message structure for `optional` keys with an array of hashes no longer duplicates keys (issue #89) (solnic)
14
+ * Inferring `:date_time?` predicate works correctly with `DateTime` types (issue #97) (solnic)
15
+
16
+ ### Changed
17
+
18
+ * [BREAKING] Updated to work with `dry-types 0.15.0` (flash-gordon)
19
+ * [BREAKING] `Result#{errors,messages,hints}` returns `MessageSet` object now which is an enumerable coercible to a hash (solnic)
20
+ * [BREAKING] `Messages` backend classes no longer use global configuration (solnic)
21
+ * [BREAKING] Passing a non-symbol key name in the DSL will raise `ArgumentError` (issue #29) (solnic)
22
+ * [BREAKING] Configuration for message backends is now nested under `messages` key with following settings:
23
+ * `messages.backend` - previously `messages`
24
+ * `messages.load_paths` - previously `messages_path`
25
+ * `messages.namespace` - previously `namespace`
26
+ * `messages.top_namespace` - **new setting** see above
27
+ * [BREAKING] `Messages::I18n` uses `I18.store_translations` instead of messing with `I18n.load_path` (solnic)
28
+ * Schemas (`Params` and `JSON`) have nicer inspect (solnic)
29
+
30
+ [Compare v0.3.0...v0.4.0](https://github.com/dry-rb/dry-schema/compare/v0.3.0...v0.4.0)
31
+
1
32
  # 0.3.0 2018-03-04
2
33
 
3
34
  ### Fixed
@@ -1,7 +1,7 @@
1
1
  en:
2
2
  dry_schema:
3
+ or: "or"
3
4
  errors:
4
- or: "or"
5
5
  array?: "must be an array"
6
6
 
7
7
  empty?: "must be empty"
@@ -38,15 +38,6 @@ module Dry
38
38
  def self.JSON(**options, &block)
39
39
  define(**options, processor_type: JSON, &block)
40
40
  end
41
-
42
- # Return configured paths to message files
43
- #
44
- # @return [Array<String>]
45
- #
46
- # @api public
47
- def self.messages_paths
48
- Messages::Abstract.config.paths
49
- end
50
41
  end
51
42
  end
52
43
 
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
3
+ require 'dry/equalizer'
4
4
  require 'dry/configurable'
5
5
 
6
+ require 'dry/schema/constants'
6
7
  require 'dry/schema/predicate_registry'
7
8
 
8
9
  module Dry
@@ -12,42 +13,35 @@ module Dry
12
13
  # @see DSL#configure
13
14
  #
14
15
  # @api public
15
- class Config < SimpleDelegator
16
- extend Dry::Configurable
16
+ class Config
17
+ include Dry::Configurable
18
+ include Dry::Equalizer(:predicates, :messages)
17
19
 
18
- setting :predicates, Schema::PredicateRegistry.new
19
- setting :messages, :yaml
20
- setting :messages_file
21
- setting :namespace
22
- setting :rules, {}
20
+ setting(:predicates, Schema::PredicateRegistry.new)
23
21
 
24
- # Build a new config object with defaults filled in
25
- #
26
- # @api private
27
- def self.new
28
- super(struct.new(*settings.map { |key| config.public_send(key) }))
22
+ setting(:messages) do
23
+ setting(:backend, :yaml)
24
+ setting(:namespace)
25
+ setting(:load_paths, Set[DEFAULT_MESSAGES_PATH], &:dup)
26
+ setting(:top_namespace, DEFAULT_MESSAGES_ROOT)
29
27
  end
30
28
 
31
- # Build a struct with defined settings
29
+ # Return configured predicate registry
32
30
  #
33
- # @return [Struct]
31
+ # @return [Schema::PredicateRegistry]
34
32
  #
35
- # @api private
36
- def self.struct
37
- ::Struct.new(*settings)
33
+ # @api public
34
+ def predicates
35
+ config.predicates
38
36
  end
39
37
 
40
- # Expose configurable object to the provided block
41
- #
42
- # This method is used by `DSL#configure`
38
+ # Return configuration for message backend
43
39
  #
44
- # @return [Config]
40
+ # @return [Dry::Configurable::Config]
45
41
  #
46
- # @api private
47
- def configure(&block)
48
- yield(__getobj__)
49
- values.freeze
50
- freeze
42
+ # @api public
43
+ def messages
44
+ config.messages
51
45
  end
52
46
  end
53
47
  end
@@ -1,15 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
3
4
  require 'dry/core/constants'
4
5
 
5
6
  module Dry
6
7
  module Schema
7
8
  include Core::Constants
8
9
 
9
- InvalidSchemaError = Class.new(StandardError)
10
- MissingMessageError = Class.new(StandardError)
11
-
10
+ LIST_SEPARATOR = ', '
12
11
  QUESTION_MARK = '?'
13
12
  DOT = '.'
13
+
14
+ DEFAULT_MESSAGES_PATH = Pathname(__dir__).join('../../../config/errors.yml').realpath.freeze
15
+ DEFAULT_MESSAGES_ROOT = 'dry_schema'
16
+
17
+ InvalidSchemaError = Class.new(StandardError)
18
+
19
+ MissingMessageError = Class.new(StandardError) do
20
+ # @api private
21
+ def initialize(path)
22
+ *rest, rule = path
23
+ super(<<~STR)
24
+ Message template for #{rule.inspect} under #{rest.join(DOT).inspect} was not found
25
+ STR
26
+ end
27
+ end
14
28
  end
15
29
  end
@@ -103,7 +103,7 @@ module Dry
103
103
  # @example
104
104
  # Dry::Schema.define do
105
105
  # configure do |config|
106
- # config.messages = :i18n
106
+ # config.messages.backend = :i18n
107
107
  # end
108
108
  # end
109
109
  #
@@ -162,6 +162,8 @@ module Dry
162
162
  #
163
163
  # @api public
164
164
  def key(name, macro:, &block)
165
+ raise ArgumentError, "Key +#{name}+ is not a symbol" unless name.is_a?(::Symbol)
166
+
165
167
  set_type(name, Types::Any)
166
168
 
167
169
  macro = macro.new(
@@ -214,7 +216,11 @@ module Dry
214
216
  #
215
217
  # @api private
216
218
  def type_schema
217
- type_registry['hash'].schema(types.merge(parent_types)).safe
219
+ if parent
220
+ parent.type_schema.schema(types)
221
+ else
222
+ type_registry['hash'].schema(types).safe
223
+ end
218
224
  end
219
225
 
220
226
  # Return a new DSL instance using the same processor type
@@ -236,18 +242,11 @@ module Dry
236
242
  # @api private
237
243
  def set_type(name, spec)
238
244
  type = resolve_type(spec)
239
- meta = { omittable: true, maybe: maybe?(type) }
245
+ meta = { required: false, maybe: type.optional? }
240
246
 
241
247
  types[name] = type.meta(meta)
242
248
  end
243
249
 
244
- # Check if the given type is a maybe sum
245
- #
246
- # @api private
247
- def maybe?(type)
248
- type.is_a?(Dry::Types::Sum) && type.left.primitive.equal?(NilClass)
249
- end
250
-
251
250
  protected
252
251
 
253
252
  # Build a rule applier
@@ -265,16 +264,14 @@ module Dry
265
264
  #
266
265
  # @api protected
267
266
  def rules
268
- parent_rules.merge(macros.map { |m| [m.name, m.to_rule] }.to_h)
267
+ parent_rules.merge(macros.map { |m| [m.name, m.to_rule] }.to_h.compact)
269
268
  end
270
269
 
271
270
  # Build a key map from defined types
272
271
  #
273
272
  # @api protected
274
273
  def key_map(types = self.types)
275
- keys = types.keys.each_with_object([]) { |key_name, arr|
276
- arr << key_spec(key_name, types[key_name])
277
- }
274
+ keys = types.map { |key, type| key_spec(key, type) }
278
275
  km = KeyMap.new(keys)
279
276
 
280
277
  if key_map_type
@@ -338,8 +335,8 @@ module Dry
338
335
  #
339
336
  # @api private
340
337
  def key_spec(name, type)
341
- if type.respond_to?(:member_types)
342
- { name => key_map(type.member_types) }
338
+ if type.respond_to?(:keys)
339
+ { name => key_map(type.name_key_map) }
343
340
  elsif type.respond_to?(:member)
344
341
  kv = key_spec(name, type.member)
345
342
  kv.equal?(name) ? name : kv.flatten(1)
@@ -369,12 +366,6 @@ module Dry
369
366
  parent&.rules || EMPTY_HASH
370
367
  end
371
368
 
372
- # @api private
373
- def parent_types
374
- # TODO: this is awful, it'd be nice if we had `Dry::Types::Hash::Schema#merge`
375
- parent&.type_schema&.member_types || EMPTY_HASH
376
- end
377
-
378
369
  # @api private
379
370
  def parent_key_map
380
371
  parent&.key_map || EMPTY_ARRAY
@@ -11,19 +11,14 @@ module Dry
11
11
  def initialize(messages, options = EMPTY_HASH)
12
12
  super
13
13
  @hints = messages.select(&:hint?)
14
+ @failures = options.fetch(:failures, true)
14
15
  end
15
16
 
16
17
  # @api public
17
18
  def to_h
18
- failures? ? messages_map : messages_map(hints)
19
+ failures ? messages_map : messages_map(hints)
19
20
  end
20
21
  alias_method :to_hash, :to_h
21
- alias_method :dump, :to_h
22
-
23
- # @api private
24
- def failures?
25
- options[:failures].equal?(true)
26
- end
27
22
  end
28
23
  end
29
24
  end
@@ -9,7 +9,7 @@ module Dry
9
9
  #
10
10
  # @api public
11
11
  def errors(options = EMPTY_HASH)
12
- message_set(options.merge(hints: false)).dump
12
+ message_set(options.merge(hints: false))
13
13
  end
14
14
 
15
15
  # Get all messages including hints
@@ -20,7 +20,7 @@ module Dry
20
20
  #
21
21
  # @api public
22
22
  def messages(options = EMPTY_HASH)
23
- message_set(options).dump
23
+ message_set(options)
24
24
  end
25
25
 
26
26
  # Get hints exclusively without errors
@@ -31,7 +31,7 @@ module Dry
31
31
  #
32
32
  # @api public
33
33
  def hints(options = EMPTY_HASH)
34
- message_set(options.merge(failures: false)).dump
34
+ message_set(options.merge(failures: false))
35
35
  end
36
36
  end
37
37
  end
@@ -11,7 +11,7 @@ module Dry
11
11
  if success?
12
12
  Success(output)
13
13
  else
14
- Failure(message_set(options).dump)
14
+ Failure(message_set(options).to_h)
15
15
  end
16
16
  end
17
17
  end
@@ -13,7 +13,6 @@ module Dry
13
13
  def call(*args, &block)
14
14
  trace << hash?
15
15
  super(*args, &block)
16
- self
17
16
  end
18
17
  end
19
18
  end
@@ -14,8 +14,18 @@ module Dry
14
14
  #
15
15
  # @api public
16
16
  class Key < DSL
17
+ # @!attribute [r] filter_schema
18
+ # @return [Schema::DSL]
19
+ # @api private
17
20
  option :filter_schema, optional: true, default: proc { schema_dsl&.new }
18
21
 
22
+ # @!attribute [r] predicate_inferrer
23
+ # @return [PredicateInferrer]
24
+ # @api private
25
+ option :predicate_inferrer, default: proc {
26
+ PredicateInferrer.new(compiler.predicates)
27
+ }
28
+
19
29
  # Specify predicates that should be used to filter out values
20
30
  # before coercion is applied
21
31
  #
@@ -116,16 +126,11 @@ module Dry
116
126
 
117
127
  if type_spec
118
128
  type(nullable && !type_spec.is_a?(::Array) ? [:nil, type_spec] : type_spec)
119
- type_predicates = PredicateInferrer[schema_dsl.types[name]]
120
129
 
121
- unless predicates.include?(type_predicates)
122
- type_predicates.each do |pred|
123
- unless compiler.supports?(pred)
124
- raise ArgumentError, "Predicate +#{pred.inspect}+ inferred from #{type_spec.inspect} type spec is not supported"
125
- end
126
- end
130
+ type_predicates = predicate_inferrer[schema_dsl.types[name]]
127
131
 
128
- if type_predicates.size.equal?(1)
132
+ unless predicates.include?(type_predicates)
133
+ if type_predicates.is_a?(::Array) && type_predicates.size.equal?(1)
129
134
  predicates.unshift(type_predicates[0])
130
135
  else
131
136
  predicates.unshift(type_predicates)
@@ -13,6 +13,11 @@ module Dry
13
13
  def operation
14
14
  :then
15
15
  end
16
+
17
+ # @api private
18
+ def to_rule
19
+ super unless trace.captures.empty?
20
+ end
16
21
  end
17
22
  end
18
23
  end
@@ -27,7 +27,7 @@ module Dry
27
27
  end
28
28
 
29
29
  final_type =
30
- if schema_dsl.maybe?(parent_type)
30
+ if parent_type.optional?
31
31
  schema_type.optional
32
32
  else
33
33
  schema_type
@@ -11,7 +11,7 @@ module Dry
11
11
  class Message
12
12
  include Dry::Equalizer(:predicate, :path, :text, :options)
13
13
 
14
- attr_reader :predicate, :path, :text, :rule, :args, :options
14
+ attr_reader :predicate, :path, :text, :args, :options
15
15
 
16
16
  # A message sub-type used by OR operations
17
17
  #
@@ -43,7 +43,7 @@ module Dry
43
43
  #
44
44
  # @api public
45
45
  def to_s
46
- uniq.join(" #{messages[:or].()} ")
46
+ uniq.join(" #{messages[:or]} ")
47
47
  end
48
48
 
49
49
  # @api private
@@ -67,15 +67,10 @@ module Dry
67
67
  # @api private
68
68
  def initialize(predicate, path, text, options)
69
69
  @predicate = predicate
70
- @path = path.dup
70
+ @path = path
71
71
  @text = text
72
72
  @options = options
73
- @rule = options[:rule]
74
73
  @args = options[:args] || EMPTY_ARRAY
75
-
76
- if predicate == :key?
77
- @path << rule
78
- end
79
74
  end
80
75
 
81
76
  # Return a string representation of the message
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dry/initializer'
4
+
3
5
  require 'dry/schema/constants'
4
6
  require 'dry/schema/message'
5
7
  require 'dry/schema/message_set'
@@ -11,28 +13,43 @@ module Dry
11
13
  #
12
14
  # @api private
13
15
  class MessageCompiler
14
- attr_reader :messages, :options, :locale, :default_lookup_options
16
+ extend Dry::Initializer
17
+
18
+ resolve_key_predicate = proc { |node, opts|
19
+ *arg_vals, val = node.map(&:last)
20
+ [[*opts.path, arg_vals[0]], arg_vals[1..arg_vals.size - 1], val]
21
+ }
22
+
23
+ resolve_predicate = proc { |node, opts|
24
+ [Array(opts.path), *node.map(&:last)]
25
+ }
26
+
27
+ DEFAULT_PREDICATE_RESOLVERS = Hash
28
+ .new(resolve_predicate).update(key?: resolve_key_predicate).freeze
15
29
 
16
30
  EMPTY_OPTS = VisitorOpts.new
17
- LIST_SEPARATOR = ', '
18
31
 
19
- # @api private
20
- def initialize(messages, options = {})
21
- @messages = messages
22
- @options = options
23
- @full = @options.fetch(:full, false)
24
- @locale = @options[:locale]
25
- @default_lookup_options = @locale ? { locale: locale } : EMPTY_HASH
26
- end
32
+ param :messages
33
+
34
+ option :full, default: -> { false }
35
+ option :locale, default: -> { :en }
36
+ option :predicate_resolvers, default: -> { DEFAULT_PREDICATE_RESOLVERS }
37
+
38
+ attr_reader :options
39
+
40
+ attr_reader :default_lookup_options
27
41
 
28
42
  # @api private
29
- def full?
30
- @full
43
+ def initialize(messages, options = EMPTY_HASH)
44
+ super
45
+ @options = options
46
+ @default_lookup_options = options[:locale] ? { locale: locale } : EMPTY_HASH
31
47
  end
32
48
 
33
49
  # @api private
34
50
  def with(new_options)
35
51
  return self if new_options.empty?
52
+
36
53
  self.class.new(messages, options.merge(new_options))
37
54
  end
38
55
 
@@ -56,7 +73,7 @@ module Dry
56
73
  end
57
74
 
58
75
  # @api private
59
- def visit_hint(node, opts)
76
+ def visit_hint(*)
60
77
  nil
61
78
  end
62
79
 
@@ -81,7 +98,7 @@ module Dry
81
98
  left, right = node.map { |n| visit(n, opts) }
82
99
 
83
100
  if [left, right].flatten.map(&:path).uniq.size == 1
84
- Message::Or.new(left, right, -> k { messages[k, default_lookup_options] })
101
+ Message::Or.new(left, right, proc { |k| messages.translate(k, default_lookup_options) })
85
102
  elsif right.is_a?(Array)
86
103
  right
87
104
  else
@@ -97,33 +114,23 @@ module Dry
97
114
 
98
115
  # @api private
99
116
  def visit_predicate(node, opts)
100
- base_opts = opts.dup
101
117
  predicate, args = node
102
118
 
103
- *arg_vals, val = args.map(&:last)
104
119
  tokens = message_tokens(args)
120
+ path, *arg_vals, input = predicate_resolvers[predicate].(args, opts)
105
121
 
106
- input = val != Undefined ? val : nil
107
-
108
- options = base_opts.update(lookup_options(arg_vals: arg_vals, input: input))
109
- msg_opts = options.update(tokens)
110
-
111
- rule = msg_opts[:rule]
112
- path = msg_opts[:path]
113
-
114
- template = messages[rule] || messages[predicate, msg_opts]
122
+ options = opts.dup.update(
123
+ path: path.last, **tokens, **lookup_options(arg_vals: arg_vals, input: input)
124
+ ).to_h
115
125
 
116
- unless template
117
- raise MissingMessageError, "message for #{predicate} was not found"
118
- end
126
+ template = messages[predicate, options] || raise(MissingMessageError, path)
119
127
 
120
- text = message_text(rule, template, tokens, options)
128
+ text = message_text(template, tokens, options)
121
129
 
122
130
  message_type(options)[
123
131
  predicate, path, text,
124
132
  args: arg_vals,
125
- input: input,
126
- rule: rule || msg_opts[:name]
133
+ input: input
127
134
  ]
128
135
  end
129
136
 
@@ -156,28 +163,26 @@ module Dry
156
163
  end
157
164
 
158
165
  # @api private
159
- def lookup_options(arg_vals: [], input: nil)
166
+ def lookup_options(arg_vals:, input:)
160
167
  default_lookup_options.merge(
161
168
  arg_type: arg_vals.size == 1 && arg_vals[0].class,
162
- val_type: input.class
169
+ val_type: input.equal?(Undefined) ? NilClass : input.class
163
170
  )
164
171
  end
165
172
 
166
173
  # @api private
167
- def message_text(rule, template, tokens, opts)
174
+ def message_text(template, tokens, options)
168
175
  text = template[template.data(tokens)]
169
176
 
170
- if full?
171
- rule_name = rule ? (messages.rule(rule, opts) || rule) : (opts[:name] || opts[:path].last)
172
- "#{rule_name} #{text}"
173
- else
174
- text
175
- end
177
+ return text unless full
178
+
179
+ rule = options[:path]
180
+ "#{messages.rule(rule, options) || rule} #{text}"
176
181
  end
177
182
 
178
183
  # @api private
179
184
  def message_tokens(args)
180
- args.each_with_object({}) { |arg, hash|
185
+ args.each_with_object({}) do |arg, hash|
181
186
  case arg[1]
182
187
  when Array
183
188
  hash[arg[0]] = arg[1].join(LIST_SEPARATOR)
@@ -187,7 +192,7 @@ module Dry
187
192
  else
188
193
  hash[arg[0]] = arg[1]
189
194
  end
190
- }
195
+ end
191
196
  end
192
197
  end
193
198
  end