/
validations.rb
378 lines (347 loc) · 9.34 KB
/
validations.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
require 'dry-validation'
require 'hanami/utils/class_attribute'
require 'hanami/validations/namespace'
require 'hanami/validations/predicates'
require 'hanami/validations/inline_predicate'
require 'set'
Dry::Validation::Messages::Namespaced.configure do |config|
# rubocop:disable Lint/NestedPercentLiteral
#
# This is probably a false positive.
# See: https://github.com/bbatsov/rubocop/issues/5314
config.lookup_paths = config.lookup_paths + %w[
%<root>s.%<rule>s.%<predicate>s
].freeze
# rubocop:enable Lint/NestedPercentLiteral
end
# @since 0.1.0
module Hanami
# Hanami::Validations is a set of lightweight validations for Ruby objects.
#
# @since 0.1.0
#
# @example
# require 'hanami/validations'
#
# class Signup
# include Hanami::Validations
#
# validations do
# # ...
# end
# end
module Validations
# @since 0.6.0
# @api private
DEFAULT_MESSAGES_ENGINE = :yaml
# Override Ruby's hook for modules.
#
# @param base [Class] the target action
#
# @since 0.1.0
# @api private
#
# @see http://www.ruby-doc.org/core/Module.html#method-i-included
def self.included(base) # rubocop:disable Metrics/MethodLength
base.class_eval do
extend ClassMethods
include Utils::ClassAttribute
class_attribute :schema
class_attribute :_messages
class_attribute :_messages_path
class_attribute :_namespace
class_attribute :_predicates_module
class_attribute :_predicates
self._predicates = Set.new
end
end
# Validations DSL
#
# @since 0.1.0
module ClassMethods
# Define validation rules from the given block.
#
# @param blk [Proc] validation rules
#
# @since 0.6.0
#
# @see http://hanamirb.org/guides/validations/overview/
#
# @example Basic Example
# require 'hanami/validations'
#
# class Signup
# include Hanami::Validations
#
# validations do
# required(:name).filled
# end
# end
#
# result = Signup.new(name: "Luca").validate
#
# result.success? # => true
# result.messages # => []
# result.output # => {:name=>""}
#
# result = Signup.new(name: "").validate
#
# result.success? # => false
# result.messages # => {:name=>["must be filled"]}
# result.output # => {:name=>""}
def validations(&blk) # rubocop:disable Metrics/AbcSize
schema_predicates = _predicates_module || __predicates
base = _build(predicates: schema_predicates, &_base_rules)
schema = _build(predicates: schema_predicates, rules: base.rules, &blk)
schema.configure(&_schema_config)
schema.configure(&_schema_predicates)
schema.extend(__messages) unless _predicates.empty?
self.schema = schema.new
end
# Define an inline predicate
#
# @param name [Symbol] inline predicate name
# @param message [String] optional error message
# @param blk [Proc] predicate implementation
#
# @return nil
#
# @since 0.6.0
#
# @example Without Custom Message
# require 'hanami/validations'
#
# class Signup
# include Hanami::Validations
#
# predicate :foo? do |actual|
# actual == 'foo'
# end
#
# validations do
# required(:name).filled(:foo?)
# end
# end
#
# result = Signup.new(name: nil).call
# result.messages # => { :name => ['is invalid'] }
#
# @example With Custom Message
# require 'hanami/validations'
#
# class Signup
# include Hanami::Validations
#
# predicate :foo?, message: 'must be foo' do |actual|
# actual == 'foo'
# end
#
# validations do
# required(:name).filled(:foo?)
# end
# end
#
# result = Signup.new(name: nil).call
# result.messages # => { :name => ['must be foo'] }
def predicate(name, message: 'is invalid', &blk)
_predicates << InlinePredicate.new(name, message, &blk)
end
# Assign a set of shared predicates wrapped in a module
#
# @param mod [Module] a module with shared predicates
#
# @since 0.6.0
#
# @see Hanami::Validations::Predicates
#
# @example
# require 'hanami/validations'
#
# module MySharedPredicates
# include Hanami::Validations::Predicates
#
# predicate :foo? fo |actual|
# actual == 'foo'
# end
# end
#
# class MyValidator
# include Hanami::Validations
# predicates MySharedPredicates
#
# validations do
# required(:name).filled(:foo?)
# end
# end
def predicates(mod)
self._predicates_module = mod
end
# Define the type of engine for error messages.
#
# Accepted values are `:yaml` (default), `:i18n`.
#
# @param type [Symbol] the preferred engine
#
# @since 0.6.0
#
# @example
# require 'hanami/validations'
#
# class Signup
# include Hanami::Validations
#
# messages :i18n
# end
def messages(type)
self._messages = type
end
# Define the path where to find translation file
#
# @param path [String] path to translation file
#
# @since 0.6.0
#
# @example
# require 'hanami/validations'
#
# class Signup
# include Hanami::Validations
#
# messages_path 'config/messages.yml'
# end
def messages_path(path)
self._messages_path = path
end
# Namespace for error messages.
#
# @param name [String] namespace
#
# @since 0.6.0
#
# @example
# require 'hanami/validations'
#
# module MyApp
# module Validators
# class Signup
# include Hanami::Validations
#
# namespace 'signup'
# end
# end
# end
#
# # Instead of looking for error messages under the `my_app.validator.signup`
# # namespace, it will look just for `signup`.
# #
# # This helps to simplify YAML files where are stored error messages
def namespace(name = nil)
if name.nil?
Namespace.new(_namespace, self)
else
self._namespace = name.to_s
end
end
private
# @since 0.6.0
# @api private
def _build(options = {}, &blk)
options = { build: false }.merge(options)
Dry::Validation.__send__(_schema_type, options, &blk)
end
# @since 0.6.0
# @api private
def _schema_type
:Schema
end
# @since 0.6.0
# @api private
def _base_rules
lambda do
end
end
# @since 0.6.0
# @api private
def _schema_config
lambda do |config|
config.messages = _messages unless _messages.nil?
config.messages_file = _messages_path unless _messages_path.nil?
config.namespace = namespace
end
end
# @since 0.6.0
# @api private
def _schema_predicates
return if _predicates_module.nil? && _predicates.empty?
lambda do |config|
config.messages = _predicates_module&.messages || DEFAULT_MESSAGES_ENGINE
config.messages_file = _predicates_module.messages_path unless _predicates_module.nil?
end
end
# @since 0.6.0
# @api private
def __predicates
mod = Module.new { include Hanami::Validations::Predicates }
_predicates.each do |p|
mod.module_eval do
predicate(p.name, &p.to_proc)
end
end
mod
end
# @since 0.6.0
# @api private
def __messages # rubocop:disable Metrics/MethodLength
result = _predicates.each_with_object({}) do |p, ret|
ret[p.name] = p.message
end
# @api private
Module.new do
@@__messages = result # rubocop:disable Style/ClassVars
# @api private
def self.extended(base)
base.instance_eval do
def __messages
Hash[en: { errors: @@__messages }]
end
end
end
# @api private
def messages
engine = super
if engine.respond_to?(:merge)
engine
else
engine.messages
end.merge(__messages)
end
end
end
end
# Initialize a new instance of a validator
#
# @param input [#to_h] a set of input data
#
# @since 0.6.0
def initialize(input = {})
@input = input.to_h
end
# Validates the object.
#
# @return [Dry::Validations::Result]
#
# @since 0.2.4
def validate
self.class.schema.call(@input)
end
# Returns a Hash with the defined attributes as symbolized keys, and their
# relative values.
#
# @return [Hash]
#
# @since 0.1.0
def to_h
validate.output
end
end
end