dry-validation 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.rubocop_todo.yml +7 -0
- data/.travis.yml +29 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +11 -0
- data/LICENSE +20 -0
- data/README.md +297 -0
- data/Rakefile +12 -0
- data/config/errors.yml +35 -0
- data/dry-validation.gemspec +25 -0
- data/examples/basic.rb +21 -0
- data/examples/nested.rb +30 -0
- data/examples/rule_ast.rb +33 -0
- data/lib/dry-validation.rb +1 -0
- data/lib/dry/validation.rb +12 -0
- data/lib/dry/validation/error.rb +43 -0
- data/lib/dry/validation/error_compiler.rb +116 -0
- data/lib/dry/validation/messages.rb +71 -0
- data/lib/dry/validation/predicate.rb +39 -0
- data/lib/dry/validation/predicate_set.rb +22 -0
- data/lib/dry/validation/predicates.rb +88 -0
- data/lib/dry/validation/result.rb +64 -0
- data/lib/dry/validation/rule.rb +125 -0
- data/lib/dry/validation/rule_compiler.rb +57 -0
- data/lib/dry/validation/schema.rb +74 -0
- data/lib/dry/validation/schema/definition.rb +15 -0
- data/lib/dry/validation/schema/key.rb +39 -0
- data/lib/dry/validation/schema/rule.rb +28 -0
- data/lib/dry/validation/schema/value.rb +31 -0
- data/lib/dry/validation/version.rb +5 -0
- data/rakelib/rubocop.rake +18 -0
- data/spec/fixtures/errors.yml +4 -0
- data/spec/integration/custom_error_messages_spec.rb +35 -0
- data/spec/integration/custom_predicates_spec.rb +57 -0
- data/spec/integration/validation_spec.rb +118 -0
- data/spec/shared/predicates.rb +31 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/unit/error_compiler_spec.rb +165 -0
- data/spec/unit/predicate_spec.rb +37 -0
- data/spec/unit/predicates/empty_spec.rb +38 -0
- data/spec/unit/predicates/eql_spec.rb +21 -0
- data/spec/unit/predicates/exclusion_spec.rb +35 -0
- data/spec/unit/predicates/filled_spec.rb +38 -0
- data/spec/unit/predicates/format_spec.rb +21 -0
- data/spec/unit/predicates/gt_spec.rb +40 -0
- data/spec/unit/predicates/gteq_spec.rb +40 -0
- data/spec/unit/predicates/inclusion_spec.rb +35 -0
- data/spec/unit/predicates/int_spec.rb +34 -0
- data/spec/unit/predicates/key_spec.rb +29 -0
- data/spec/unit/predicates/lt_spec.rb +40 -0
- data/spec/unit/predicates/lteq_spec.rb +40 -0
- data/spec/unit/predicates/max_size_spec.rb +49 -0
- data/spec/unit/predicates/min_size_spec.rb +49 -0
- data/spec/unit/predicates/nil_spec.rb +28 -0
- data/spec/unit/predicates/size_spec.rb +49 -0
- data/spec/unit/predicates/str_spec.rb +32 -0
- data/spec/unit/rule/each_spec.rb +20 -0
- data/spec/unit/rule/key_spec.rb +27 -0
- data/spec/unit/rule/set_spec.rb +32 -0
- data/spec/unit/rule/value_spec.rb +42 -0
- data/spec/unit/rule_compiler_spec.rb +86 -0
- metadata +230 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require File.expand_path('../lib/dry/validation/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = 'dry-validation'
|
6
|
+
spec.version = Dry::Validation::VERSION
|
7
|
+
spec.authors = ['Andy Holland', 'Piotr Solnica']
|
8
|
+
spec.email = ['andyholland1991@aol.com', 'piotr.solnica@gmail.com']
|
9
|
+
spec.summary = 'A simple validation library'
|
10
|
+
spec.homepage = 'https://github.com/dryrb/dry-validation'
|
11
|
+
spec.license = 'MIT'
|
12
|
+
|
13
|
+
spec.files = `git ls-files -z`.split("\x0")
|
14
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
15
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
16
|
+
spec.require_paths = ['lib']
|
17
|
+
|
18
|
+
spec.add_runtime_dependency 'dry-configurable', '~> 0.1'
|
19
|
+
spec.add_runtime_dependency 'dry-container', '~> 0.2', '>= 0.2.6'
|
20
|
+
spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
|
21
|
+
|
22
|
+
spec.add_development_dependency 'bundler'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rspec'
|
25
|
+
end
|
data/examples/basic.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'dry-validation'
|
2
|
+
|
3
|
+
class Schema < Dry::Validation::Schema
|
4
|
+
key(:email) { |email| email.filled? }
|
5
|
+
|
6
|
+
key(:age) do |age|
|
7
|
+
age.int? & age.gt?(18)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
schema = Schema.new
|
12
|
+
|
13
|
+
errors = schema.messages(email: 'jane@doe.org', age: 19)
|
14
|
+
|
15
|
+
puts errors.inspect
|
16
|
+
# []
|
17
|
+
|
18
|
+
errors = schema.messages(email: nil, age: 19)
|
19
|
+
|
20
|
+
puts errors.inspect
|
21
|
+
# [[:email, ["email must be filled"]]]
|
data/examples/nested.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'dry-validation'
|
2
|
+
|
3
|
+
class Schema < Dry::Validation::Schema
|
4
|
+
key(:address) do |address|
|
5
|
+
address.key(:city) do |city|
|
6
|
+
city.min_size?(3)
|
7
|
+
end
|
8
|
+
|
9
|
+
address.key(:street) do |street|
|
10
|
+
street.filled?
|
11
|
+
end
|
12
|
+
|
13
|
+
address.key(:country) do |country|
|
14
|
+
country.key(:name, &:filled?)
|
15
|
+
country.key(:code, &:filled?)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
schema = Schema.new
|
21
|
+
|
22
|
+
errors = schema.messages({})
|
23
|
+
|
24
|
+
puts errors.inspect
|
25
|
+
#<Dry::Validation::Error::Set:0x007fc4f89c4360 @errors=[#<Dry::Validation::Error:0x007fc4f89c4108 @result=#<Dry::Validation::Result::Value success?=false input=nil rule=#<Dry::Validation::Rule::Key name=:address predicate=#<Dry::Validation::Predicate id=:key?>>>>]>
|
26
|
+
|
27
|
+
errors = schema.messages(address: { city: 'NYC' })
|
28
|
+
|
29
|
+
puts errors.inspect
|
30
|
+
#<Dry::Validation::Error::Set:0x007fd151189b18 @errors=[#<Dry::Validation::Error:0x007fd151188e20 @result=#<Dry::Validation::Result::Set success?=false input={:city=>"NYC"} rule=#<Dry::Validation::Rule::Set name=:address predicate=[#<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:city predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:city predicate=#<Dry::Validation::Predicate id=:min_size?>>>, #<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:street predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:street predicate=#<Dry::Validation::Predicate id=:filled?>>>, #<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:country predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Set name=:country predicate=[#<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:name predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:name predicate=#<Dry::Validation::Predicate id=:filled?>>>, #<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:code predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:code predicate=#<Dry::Validation::Predicate id=:filled?>>>]>>]>>>]>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'dry-validation'
|
2
|
+
|
3
|
+
ast = [
|
4
|
+
[
|
5
|
+
:and,
|
6
|
+
[
|
7
|
+
[:key, [:age, [:predicate, [:key?, []]]]],
|
8
|
+
[
|
9
|
+
:and,
|
10
|
+
[
|
11
|
+
[:val, [:age, [:predicate, [:filled?, []]]]],
|
12
|
+
[:val, [:age, [:predicate, [:gt?, [18]]]]]
|
13
|
+
]
|
14
|
+
]
|
15
|
+
]
|
16
|
+
]
|
17
|
+
]
|
18
|
+
|
19
|
+
compiler = Dry::Validation::RuleCompiler.new(Dry::Validation::Predicates)
|
20
|
+
|
21
|
+
rules = compiler.call(ast)
|
22
|
+
|
23
|
+
puts rules.inspect
|
24
|
+
# [
|
25
|
+
# #<Dry::Validation::Rule::Conjunction
|
26
|
+
# left=#<Dry::Validation::Rule::Key name=:age predicate=#<Dry::Validation::Predicate id=:key?>>
|
27
|
+
# right=#<Dry::Validation::Rule::Conjunction
|
28
|
+
# left=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:filled?>>
|
29
|
+
# right=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:gt?>>>>
|
30
|
+
# ]
|
31
|
+
|
32
|
+
puts rules.map(&:to_ary).inspect
|
33
|
+
# [[:and, [:key, [:age, [:predicate, [:key?, [:age]]]]], [[:and, [:val, [:age, [:predicate, [:filled?, []]]]], [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]]]
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'dry/validation'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Dry
|
2
|
+
module Validation
|
3
|
+
class Error
|
4
|
+
class Set
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :errors
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@errors = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def each(&block)
|
14
|
+
errors.each(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
errors.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def <<(error)
|
22
|
+
errors << error
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_ary
|
26
|
+
errors.map { |error| error.to_ary }
|
27
|
+
end
|
28
|
+
alias_method :to_a, :to_ary
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :result
|
32
|
+
|
33
|
+
def initialize(result)
|
34
|
+
@result = result
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_ary
|
38
|
+
[:error, result.to_ary]
|
39
|
+
end
|
40
|
+
alias_method :to_a, :to_ary
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Dry
|
2
|
+
module Validation
|
3
|
+
class ErrorCompiler
|
4
|
+
attr_reader :messages
|
5
|
+
|
6
|
+
def initialize(messages)
|
7
|
+
@messages = messages
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(ast)
|
11
|
+
ast.map { |node| visit(node) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def visit(node, *args)
|
15
|
+
__send__(:"visit_#{node[0]}", node[1], *args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def visit_error(error)
|
19
|
+
visit(error)
|
20
|
+
end
|
21
|
+
|
22
|
+
def visit_input(input, *args)
|
23
|
+
name, value, rules = input
|
24
|
+
[name, rules.map { |rule| visit(rule, name, value) }]
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_key(rule, name, value)
|
28
|
+
_, predicate = rule
|
29
|
+
visit(predicate, value, name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def visit_val(rule, name, value)
|
33
|
+
name, predicate = rule
|
34
|
+
visit(predicate, value, name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def visit_predicate(predicate, value, name)
|
38
|
+
messages.lookup(predicate[0], name, predicate[1][0]) % visit(predicate, value).merge(name: name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def visit_key?(*args, value)
|
42
|
+
{ name: args[0][0] }
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_empty?(*args, value)
|
46
|
+
{ value: value }
|
47
|
+
end
|
48
|
+
|
49
|
+
def visit_exclusion?(*args, value)
|
50
|
+
{ list: args[0][0].join(', ') }
|
51
|
+
end
|
52
|
+
|
53
|
+
def visit_inclusion?(*args, value)
|
54
|
+
{ list: args[0][0].join(', ') }
|
55
|
+
end
|
56
|
+
|
57
|
+
def visit_gt?(*args, value)
|
58
|
+
{ num: args[0][0], value: value }
|
59
|
+
end
|
60
|
+
|
61
|
+
def visit_gteq?(*args, value)
|
62
|
+
{ num: args[0][0], value: value }
|
63
|
+
end
|
64
|
+
|
65
|
+
def visit_lt?(*args, value)
|
66
|
+
{ num: args[0][0], value: value }
|
67
|
+
end
|
68
|
+
|
69
|
+
def visit_lteq?(*args, value)
|
70
|
+
{ num: args[0][0], value: value }
|
71
|
+
end
|
72
|
+
|
73
|
+
def visit_int?(*args, value)
|
74
|
+
{ num: args[0][0], value: value }
|
75
|
+
end
|
76
|
+
|
77
|
+
def visit_max_size?(*args, value)
|
78
|
+
{ num: args[0][0], value: value }
|
79
|
+
end
|
80
|
+
|
81
|
+
def visit_min_size?(*args, value)
|
82
|
+
{ num: args[0][0], value: value }
|
83
|
+
end
|
84
|
+
|
85
|
+
def visit_eql?(*args, value)
|
86
|
+
{ eql_value: args[0][0], value: value }
|
87
|
+
end
|
88
|
+
|
89
|
+
def visit_size?(*args, value)
|
90
|
+
num = args[0][0]
|
91
|
+
|
92
|
+
if num.is_a?(Range)
|
93
|
+
{ left: num.first, right: num.last, value: value }
|
94
|
+
else
|
95
|
+
{ num: args[0][0], value: value }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def visit_str?(*args, value)
|
100
|
+
{ value: value }
|
101
|
+
end
|
102
|
+
|
103
|
+
def visit_format?(*args, value)
|
104
|
+
{}
|
105
|
+
end
|
106
|
+
|
107
|
+
def visit_nil?(*args, value)
|
108
|
+
{}
|
109
|
+
end
|
110
|
+
|
111
|
+
def visit_filled?(*args)
|
112
|
+
{}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Validation
|
6
|
+
class Messages
|
7
|
+
DEFAULT_PATH = Pathname(__dir__).join('../../../config/errors.yml').freeze
|
8
|
+
|
9
|
+
attr_reader :data
|
10
|
+
|
11
|
+
def self.default
|
12
|
+
load(DEFAULT_PATH)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.load(path)
|
16
|
+
new(load_yaml(path))
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.load_yaml(path)
|
20
|
+
symbolize_keys(YAML.load_file(path))
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.symbolize_keys(hash)
|
24
|
+
hash.each_with_object({}) do |(k, v), r|
|
25
|
+
r[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Namespaced
|
30
|
+
attr_reader :namespace, :fallback
|
31
|
+
|
32
|
+
def initialize(namespace, fallback)
|
33
|
+
@namespace = namespace
|
34
|
+
@fallback = fallback
|
35
|
+
end
|
36
|
+
|
37
|
+
def lookup(*args)
|
38
|
+
namespace.lookup(*args) { fallback.lookup(*args) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(data)
|
43
|
+
@data = data
|
44
|
+
end
|
45
|
+
|
46
|
+
def merge(overrides)
|
47
|
+
if overrides.is_a?(Hash)
|
48
|
+
self.class.new(data.merge(overrides))
|
49
|
+
else
|
50
|
+
self.class.new(data.merge(Messages.load_yaml(overrides)))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def namespaced(namespace)
|
55
|
+
Namespaced.new(Messages.new(data[namespace]), self)
|
56
|
+
end
|
57
|
+
|
58
|
+
def lookup(identifier, key, arg, &block)
|
59
|
+
message = data.fetch(:attributes, {}).fetch(key, {}).fetch(identifier) do
|
60
|
+
data.fetch(identifier, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
if message.is_a?(Hash)
|
64
|
+
message.fetch(arg.class.name.downcase.to_sym, message.fetch(:default))
|
65
|
+
else
|
66
|
+
message
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Dry
|
2
|
+
module Validation
|
3
|
+
def self.Predicate(block)
|
4
|
+
case block
|
5
|
+
when Method then Predicate.new(block.name, &block)
|
6
|
+
else raise ArgumentError, 'predicate needs an :id'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Predicate
|
11
|
+
include Dry::Equalizer(:id)
|
12
|
+
|
13
|
+
attr_reader :id, :args, :fn
|
14
|
+
|
15
|
+
def initialize(id, *args, &block)
|
16
|
+
@id = id
|
17
|
+
@fn = block
|
18
|
+
@args = args
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(*args)
|
22
|
+
fn.(*args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def negation
|
26
|
+
self.class.new(:"not_#{id}") { |input| !fn.(input) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def curry(*args)
|
30
|
+
self.class.new(id, *args, &fn.curry.(*args))
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_ary
|
34
|
+
[:predicate, [id, args]]
|
35
|
+
end
|
36
|
+
alias_method :to_a, :to_ary
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'dry/validation/predicate'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Validation
|
5
|
+
module PredicateSet
|
6
|
+
module Methods
|
7
|
+
def predicate(name, &block)
|
8
|
+
register(name) { Predicate.new(name, &block) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def import(predicate_set)
|
12
|
+
merge(predicate_set)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.extended(other)
|
17
|
+
super
|
18
|
+
other.extend(Methods, Dry::Container::Mixin)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'dry/validation/predicate_set'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Validation
|
5
|
+
module Predicates
|
6
|
+
extend PredicateSet
|
7
|
+
|
8
|
+
def self.included(other)
|
9
|
+
super
|
10
|
+
other.extend(PredicateSet)
|
11
|
+
other.import(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
predicate(:nil?) do |input|
|
15
|
+
input.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
predicate(:key?) do |name, input|
|
19
|
+
input.key?(name)
|
20
|
+
end
|
21
|
+
|
22
|
+
predicate(:empty?) do |input|
|
23
|
+
case input
|
24
|
+
when String, Array, Hash then input.empty?
|
25
|
+
when nil then true
|
26
|
+
else
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
predicate(:filled?) do |input|
|
32
|
+
!self[:empty?].(input)
|
33
|
+
end
|
34
|
+
|
35
|
+
predicate(:int?) do |input|
|
36
|
+
input.is_a?(Fixnum)
|
37
|
+
end
|
38
|
+
|
39
|
+
predicate(:str?) do |input|
|
40
|
+
input.is_a?(String)
|
41
|
+
end
|
42
|
+
|
43
|
+
predicate(:lt?) do |num, input|
|
44
|
+
input < num
|
45
|
+
end
|
46
|
+
|
47
|
+
predicate(:gt?) do |num, input|
|
48
|
+
input > num
|
49
|
+
end
|
50
|
+
|
51
|
+
predicate(:lteq?) do |num, input|
|
52
|
+
!self[:gt?].(num, input)
|
53
|
+
end
|
54
|
+
|
55
|
+
predicate(:gteq?) do |num, input|
|
56
|
+
!self[:lt?].(num, input)
|
57
|
+
end
|
58
|
+
|
59
|
+
predicate(:size?) do |num, input|
|
60
|
+
input.size == num
|
61
|
+
end
|
62
|
+
|
63
|
+
predicate(:min_size?) do |num, input|
|
64
|
+
input.size >= num
|
65
|
+
end
|
66
|
+
|
67
|
+
predicate(:max_size?) do |num, input|
|
68
|
+
input.size <= num
|
69
|
+
end
|
70
|
+
|
71
|
+
predicate(:inclusion?) do |list, input|
|
72
|
+
list.include?(input)
|
73
|
+
end
|
74
|
+
|
75
|
+
predicate(:exclusion?) do |list, input|
|
76
|
+
!self[:inclusion?].(list, input)
|
77
|
+
end
|
78
|
+
|
79
|
+
predicate(:eql?) do |left, right|
|
80
|
+
left.eql?(right)
|
81
|
+
end
|
82
|
+
|
83
|
+
predicate(:format?) do |regex, input|
|
84
|
+
!regex.match(input).nil?
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|