dry-schema 0.3.0 → 0.4.0

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