omg-activemodel 8.0.0.alpha1
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 +7 -0
- data/CHANGELOG.md +67 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +266 -0
- data/lib/active_model/access.rb +16 -0
- data/lib/active_model/api.rb +99 -0
- data/lib/active_model/attribute/user_provided_default.rb +55 -0
- data/lib/active_model/attribute.rb +277 -0
- data/lib/active_model/attribute_assignment.rb +78 -0
- data/lib/active_model/attribute_methods.rb +592 -0
- data/lib/active_model/attribute_mutation_tracker.rb +189 -0
- data/lib/active_model/attribute_registration.rb +117 -0
- data/lib/active_model/attribute_set/builder.rb +182 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
- data/lib/active_model/attribute_set.rb +118 -0
- data/lib/active_model/attributes.rb +165 -0
- data/lib/active_model/callbacks.rb +155 -0
- data/lib/active_model/conversion.rb +121 -0
- data/lib/active_model/deprecator.rb +7 -0
- data/lib/active_model/dirty.rb +416 -0
- data/lib/active_model/error.rb +208 -0
- data/lib/active_model/errors.rb +547 -0
- data/lib/active_model/forbidden_attributes_protection.rb +33 -0
- data/lib/active_model/gem_version.rb +17 -0
- data/lib/active_model/lint.rb +118 -0
- data/lib/active_model/locale/en.yml +38 -0
- data/lib/active_model/model.rb +78 -0
- data/lib/active_model/naming.rb +359 -0
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +24 -0
- data/lib/active_model/secure_password.rb +231 -0
- data/lib/active_model/serialization.rb +198 -0
- data/lib/active_model/serializers/json.rb +154 -0
- data/lib/active_model/translation.rb +78 -0
- data/lib/active_model/type/big_integer.rb +36 -0
- data/lib/active_model/type/binary.rb +62 -0
- data/lib/active_model/type/boolean.rb +48 -0
- data/lib/active_model/type/date.rb +78 -0
- data/lib/active_model/type/date_time.rb +88 -0
- data/lib/active_model/type/decimal.rb +107 -0
- data/lib/active_model/type/float.rb +64 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
- data/lib/active_model/type/helpers/mutable.rb +24 -0
- data/lib/active_model/type/helpers/numeric.rb +61 -0
- data/lib/active_model/type/helpers/time_value.rb +127 -0
- data/lib/active_model/type/helpers/timezone.rb +23 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/immutable_string.rb +71 -0
- data/lib/active_model/type/integer.rb +113 -0
- data/lib/active_model/type/registry.rb +37 -0
- data/lib/active_model/type/serialize_cast_value.rb +47 -0
- data/lib/active_model/type/string.rb +43 -0
- data/lib/active_model/type/time.rb +87 -0
- data/lib/active_model/type/value.rb +157 -0
- data/lib/active_model/type.rb +55 -0
- data/lib/active_model/validations/absence.rb +33 -0
- data/lib/active_model/validations/acceptance.rb +113 -0
- data/lib/active_model/validations/callbacks.rb +119 -0
- data/lib/active_model/validations/clusivity.rb +54 -0
- data/lib/active_model/validations/comparability.rb +18 -0
- data/lib/active_model/validations/comparison.rb +90 -0
- data/lib/active_model/validations/confirmation.rb +80 -0
- data/lib/active_model/validations/exclusion.rb +49 -0
- data/lib/active_model/validations/format.rb +112 -0
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +47 -0
- data/lib/active_model/validations/length.rb +130 -0
- data/lib/active_model/validations/numericality.rb +222 -0
- data/lib/active_model/validations/presence.rb +39 -0
- data/lib/active_model/validations/resolve_value.rb +26 -0
- data/lib/active_model/validations/validates.rb +175 -0
- data/lib/active_model/validations/with.rb +154 -0
- data/lib/active_model/validations.rb +489 -0
- data/lib/active_model/validator.rb +190 -0
- data/lib/active_model/version.rb +10 -0
- data/lib/active_model.rb +84 -0
- metadata +139 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Type
|
5
|
+
# = Active Model \Value \Type
|
6
|
+
#
|
7
|
+
# The base class for all attribute types. This class also serves as the
|
8
|
+
# default type for attributes that do not specify a type.
|
9
|
+
class Value
|
10
|
+
include SerializeCastValue
|
11
|
+
attr_reader :precision, :scale, :limit
|
12
|
+
|
13
|
+
# Initializes a type with three basic configuration settings: precision,
|
14
|
+
# limit, and scale. The Value base class does not define behavior for
|
15
|
+
# these settings. It uses them for equality comparison and hash key
|
16
|
+
# generation only.
|
17
|
+
def initialize(precision: nil, limit: nil, scale: nil)
|
18
|
+
super()
|
19
|
+
@precision = precision
|
20
|
+
@scale = scale
|
21
|
+
@limit = limit
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns true if this type can convert +value+ to a type that is usable
|
25
|
+
# by the database. For example a boolean type can return +true+ if the
|
26
|
+
# value parameter is a Ruby boolean, but may return +false+ if the value
|
27
|
+
# parameter is some other object.
|
28
|
+
def serializable?(value)
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the unique type name as a Symbol. Subclasses should override
|
33
|
+
# this method.
|
34
|
+
def type
|
35
|
+
end
|
36
|
+
|
37
|
+
# Converts a value from database input to the appropriate ruby type. The
|
38
|
+
# return value of this method will be returned from
|
39
|
+
# ActiveRecord::AttributeMethods::Read#read_attribute. The default
|
40
|
+
# implementation just calls Value#cast.
|
41
|
+
#
|
42
|
+
# +value+ The raw input, as provided from the database.
|
43
|
+
def deserialize(value)
|
44
|
+
cast(value)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Type casts a value from user input (e.g. from a setter). This value may
|
48
|
+
# be a string from the form builder, or a ruby object passed to a setter.
|
49
|
+
# There is currently no way to differentiate between which source it came
|
50
|
+
# from.
|
51
|
+
#
|
52
|
+
# The return value of this method will be returned from
|
53
|
+
# ActiveRecord::AttributeMethods::Read#read_attribute. See also:
|
54
|
+
# Value#cast_value.
|
55
|
+
#
|
56
|
+
# +value+ The raw input, as provided to the attribute setter.
|
57
|
+
def cast(value)
|
58
|
+
cast_value(value) unless value.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Casts a value from the ruby type to a type that the database knows how
|
62
|
+
# to understand. The returned value from this method should be a
|
63
|
+
# +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
|
64
|
+
# +nil+.
|
65
|
+
def serialize(value)
|
66
|
+
value
|
67
|
+
end
|
68
|
+
|
69
|
+
# Type casts a value for schema dumping. This method is private, as we are
|
70
|
+
# hoping to remove it entirely.
|
71
|
+
def type_cast_for_schema(value) # :nodoc:
|
72
|
+
value.inspect
|
73
|
+
end
|
74
|
+
|
75
|
+
# These predicates are not documented, as I need to look further into
|
76
|
+
# their use, and see if they can be removed entirely.
|
77
|
+
def binary? # :nodoc:
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
# Determines whether a value has changed for dirty checking. +old_value+
|
82
|
+
# and +new_value+ will always be type-cast. Types should not need to
|
83
|
+
# override this method.
|
84
|
+
def changed?(old_value, new_value, _new_value_before_type_cast)
|
85
|
+
old_value != new_value
|
86
|
+
end
|
87
|
+
|
88
|
+
# Determines whether the mutable value has been modified since it was
|
89
|
+
# read. Returns +false+ by default. If your type returns an object
|
90
|
+
# which could be mutated, you should override this method. You will need
|
91
|
+
# to either:
|
92
|
+
#
|
93
|
+
# - pass +new_value+ to Value#serialize and compare it to
|
94
|
+
# +raw_old_value+
|
95
|
+
#
|
96
|
+
# or
|
97
|
+
#
|
98
|
+
# - pass +raw_old_value+ to Value#deserialize and compare it to
|
99
|
+
# +new_value+
|
100
|
+
#
|
101
|
+
# +raw_old_value+ The original value, before being passed to
|
102
|
+
# +deserialize+.
|
103
|
+
#
|
104
|
+
# +new_value+ The current value, after type casting.
|
105
|
+
def changed_in_place?(raw_old_value, new_value)
|
106
|
+
false
|
107
|
+
end
|
108
|
+
|
109
|
+
def value_constructed_by_mass_assignment?(_value) # :nodoc:
|
110
|
+
false
|
111
|
+
end
|
112
|
+
|
113
|
+
def force_equality?(_value) # :nodoc:
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
def map(value) # :nodoc:
|
118
|
+
yield value
|
119
|
+
end
|
120
|
+
|
121
|
+
def ==(other)
|
122
|
+
self.class == other.class &&
|
123
|
+
precision == other.precision &&
|
124
|
+
scale == other.scale &&
|
125
|
+
limit == other.limit
|
126
|
+
end
|
127
|
+
alias eql? ==
|
128
|
+
|
129
|
+
def hash
|
130
|
+
[self.class, precision, scale, limit].hash
|
131
|
+
end
|
132
|
+
|
133
|
+
def assert_valid_value(_)
|
134
|
+
end
|
135
|
+
|
136
|
+
def serialized? # :nodoc:
|
137
|
+
false
|
138
|
+
end
|
139
|
+
|
140
|
+
def mutable? # :nodoc:
|
141
|
+
false
|
142
|
+
end
|
143
|
+
|
144
|
+
def as_json(*)
|
145
|
+
raise NoMethodError
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
# Convenience method for types which do not need separate type casting
|
150
|
+
# behavior for user and database inputs. Called by Value#cast for
|
151
|
+
# values except +nil+.
|
152
|
+
def cast_value(value) # :doc:
|
153
|
+
value
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model/type/helpers"
|
4
|
+
require "active_model/type/serialize_cast_value"
|
5
|
+
require "active_model/type/value"
|
6
|
+
|
7
|
+
require "active_model/type/big_integer"
|
8
|
+
require "active_model/type/binary"
|
9
|
+
require "active_model/type/boolean"
|
10
|
+
require "active_model/type/date"
|
11
|
+
require "active_model/type/date_time"
|
12
|
+
require "active_model/type/decimal"
|
13
|
+
require "active_model/type/float"
|
14
|
+
require "active_model/type/immutable_string"
|
15
|
+
require "active_model/type/integer"
|
16
|
+
require "active_model/type/string"
|
17
|
+
require "active_model/type/time"
|
18
|
+
|
19
|
+
require "active_model/type/registry"
|
20
|
+
|
21
|
+
module ActiveModel
|
22
|
+
module Type
|
23
|
+
@registry = Registry.new
|
24
|
+
|
25
|
+
class << self
|
26
|
+
attr_accessor :registry # :nodoc:
|
27
|
+
|
28
|
+
# Add a new type to the registry, allowing it to be referenced as a
|
29
|
+
# symbol by {attribute}[rdoc-ref:Attributes::ClassMethods#attribute].
|
30
|
+
def register(type_name, klass = nil, &block)
|
31
|
+
registry.register(type_name, klass, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def lookup(...) # :nodoc:
|
35
|
+
registry.lookup(...)
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_value # :nodoc:
|
39
|
+
@default_value ||= Value.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
register(:big_integer, Type::BigInteger)
|
44
|
+
register(:binary, Type::Binary)
|
45
|
+
register(:boolean, Type::Boolean)
|
46
|
+
register(:date, Type::Date)
|
47
|
+
register(:datetime, Type::DateTime)
|
48
|
+
register(:decimal, Type::Decimal)
|
49
|
+
register(:float, Type::Float)
|
50
|
+
register(:immutable_string, Type::ImmutableString)
|
51
|
+
register(:integer, Type::Integer)
|
52
|
+
register(:string, Type::String)
|
53
|
+
register(:time, Type::Time)
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Validations
|
5
|
+
# == \Active \Model Absence Validator
|
6
|
+
class AbsenceValidator < EachValidator # :nodoc:
|
7
|
+
def validate_each(record, attr_name, value)
|
8
|
+
record.errors.add(attr_name, :present, **options) if value.present?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module HelperMethods
|
13
|
+
# Validates that the specified attributes are blank (as defined by
|
14
|
+
# Object#present?).
|
15
|
+
#
|
16
|
+
# class Person < ActiveRecord::Base
|
17
|
+
# validates_absence_of :first_name
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# The first_name attribute must be in the object and it must be blank.
|
21
|
+
#
|
22
|
+
# Configuration options:
|
23
|
+
# * <tt>:message</tt> - A custom error message (default is: "must be blank").
|
24
|
+
#
|
25
|
+
# There is also a list of default options supported by every validator:
|
26
|
+
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
|
27
|
+
# See ActiveModel::Validations::ClassMethods#validates for more information.
|
28
|
+
def validates_absence_of(*attr_names)
|
29
|
+
validates_with AbsenceValidator, _merge_attributes(attr_names)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Validations
|
5
|
+
class AcceptanceValidator < EachValidator # :nodoc:
|
6
|
+
def initialize(options)
|
7
|
+
super({ allow_nil: true, accept: ["1", true] }.merge!(options))
|
8
|
+
setup!(options[:class])
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_each(record, attribute, value)
|
12
|
+
unless acceptable_option?(value)
|
13
|
+
record.errors.add(attribute, :accepted, **options.except(:accept, :allow_nil))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def setup!(klass)
|
19
|
+
define_attributes = LazilyDefineAttributes.new(attributes)
|
20
|
+
klass.include(define_attributes) unless klass.included_modules.include?(define_attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def acceptable_option?(value)
|
24
|
+
Array(options[:accept]).include?(value)
|
25
|
+
end
|
26
|
+
|
27
|
+
class LazilyDefineAttributes < Module
|
28
|
+
def initialize(attributes)
|
29
|
+
@attributes = attributes.map(&:to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
def included(klass)
|
33
|
+
@lock = Mutex.new
|
34
|
+
mod = self
|
35
|
+
|
36
|
+
define_method(:respond_to_missing?) do |method_name, include_private = false|
|
37
|
+
mod.define_on(klass)
|
38
|
+
super(method_name, include_private) || mod.matches?(method_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
define_method(:method_missing) do |method_name, *args, &block|
|
42
|
+
mod.define_on(klass)
|
43
|
+
if mod.matches?(method_name)
|
44
|
+
send(method_name, *args, &block)
|
45
|
+
else
|
46
|
+
super(method_name, *args, &block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def matches?(method_name)
|
52
|
+
attr_name = method_name.to_s.chomp("=")
|
53
|
+
attributes.any? { |name| name == attr_name }
|
54
|
+
end
|
55
|
+
|
56
|
+
def define_on(klass)
|
57
|
+
@lock&.synchronize do
|
58
|
+
return unless @lock
|
59
|
+
|
60
|
+
attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
|
61
|
+
attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
|
62
|
+
|
63
|
+
attr_reader(*attr_readers)
|
64
|
+
attr_writer(*attr_writers)
|
65
|
+
|
66
|
+
remove_method :respond_to_missing?
|
67
|
+
remove_method :method_missing
|
68
|
+
|
69
|
+
@lock = nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def ==(other)
|
74
|
+
self.class == other.class && attributes == other.attributes
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
attr_reader :attributes
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module HelperMethods
|
83
|
+
# Encapsulates the pattern of wanting to validate the acceptance of a
|
84
|
+
# terms of service check box (or similar agreement).
|
85
|
+
#
|
86
|
+
# class Person < ActiveRecord::Base
|
87
|
+
# validates_acceptance_of :terms_of_service
|
88
|
+
# validates_acceptance_of :eula, message: 'must be abided'
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# If the database column does not exist, the +terms_of_service+ attribute
|
92
|
+
# is entirely virtual. This check is performed only if +terms_of_service+
|
93
|
+
# is not +nil+.
|
94
|
+
#
|
95
|
+
# Configuration options:
|
96
|
+
# * <tt>:message</tt> - A custom error message (default is: "must be
|
97
|
+
# accepted").
|
98
|
+
# * <tt>:accept</tt> - Specifies a value that is considered accepted.
|
99
|
+
# Also accepts an array of possible values. The default value is
|
100
|
+
# an array ["1", true], which makes it easy to relate to an HTML
|
101
|
+
# checkbox. This should be set to, or include, +true+ if you are validating
|
102
|
+
# a database column, since the attribute is typecast from "1" to +true+
|
103
|
+
# before validation.
|
104
|
+
#
|
105
|
+
# There is also a list of default options supported by every validator:
|
106
|
+
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
|
107
|
+
# See ActiveModel::Validations::ClassMethods#validates for more information.
|
108
|
+
def validates_acceptance_of(*attr_names)
|
109
|
+
validates_with AcceptanceValidator, _merge_attributes(attr_names)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Validations
|
5
|
+
# = Active \Model \Validation \Callbacks
|
6
|
+
#
|
7
|
+
# Provides an interface for any class to have ClassMethods#before_validation and
|
8
|
+
# ClassMethods#after_validation callbacks.
|
9
|
+
#
|
10
|
+
# First, include +ActiveModel::Validations::Callbacks+ from the class you are
|
11
|
+
# creating:
|
12
|
+
#
|
13
|
+
# class MyModel
|
14
|
+
# include ActiveModel::Validations::Callbacks
|
15
|
+
#
|
16
|
+
# before_validation :do_stuff_before_validation
|
17
|
+
# after_validation :do_stuff_after_validation
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# Like other <tt>before_*</tt> callbacks if +before_validation+ throws
|
21
|
+
# +:abort+ then <tt>valid?</tt> will not be called.
|
22
|
+
module Callbacks
|
23
|
+
extend ActiveSupport::Concern
|
24
|
+
|
25
|
+
included do
|
26
|
+
include ActiveSupport::Callbacks
|
27
|
+
define_callbacks :validation,
|
28
|
+
skip_after_callbacks_if_terminated: true,
|
29
|
+
scope: [:kind, :name]
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
# Defines a callback that will get called right before validation.
|
34
|
+
#
|
35
|
+
# class Person
|
36
|
+
# include ActiveModel::Validations
|
37
|
+
# include ActiveModel::Validations::Callbacks
|
38
|
+
#
|
39
|
+
# attr_accessor :name
|
40
|
+
#
|
41
|
+
# validates_length_of :name, maximum: 6
|
42
|
+
#
|
43
|
+
# before_validation :remove_whitespaces
|
44
|
+
#
|
45
|
+
# private
|
46
|
+
# def remove_whitespaces
|
47
|
+
# name.strip!
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# person = Person.new
|
52
|
+
# person.name = ' bob '
|
53
|
+
# person.valid? # => true
|
54
|
+
# person.name # => "bob"
|
55
|
+
def before_validation(*args, &block)
|
56
|
+
options = args.extract_options!
|
57
|
+
|
58
|
+
set_options_for_callback(options)
|
59
|
+
|
60
|
+
set_callback(:validation, :before, *args, options, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Defines a callback that will get called right after validation.
|
64
|
+
#
|
65
|
+
# class Person
|
66
|
+
# include ActiveModel::Validations
|
67
|
+
# include ActiveModel::Validations::Callbacks
|
68
|
+
#
|
69
|
+
# attr_accessor :name, :status
|
70
|
+
#
|
71
|
+
# validates_presence_of :name
|
72
|
+
#
|
73
|
+
# after_validation :set_status
|
74
|
+
#
|
75
|
+
# private
|
76
|
+
# def set_status
|
77
|
+
# self.status = errors.empty?
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# person = Person.new
|
82
|
+
# person.name = ''
|
83
|
+
# person.valid? # => false
|
84
|
+
# person.status # => false
|
85
|
+
# person.name = 'bob'
|
86
|
+
# person.valid? # => true
|
87
|
+
# person.status # => true
|
88
|
+
def after_validation(*args, &block)
|
89
|
+
options = args.extract_options!
|
90
|
+
options = options.dup
|
91
|
+
options[:prepend] = true
|
92
|
+
|
93
|
+
set_options_for_callback(options)
|
94
|
+
|
95
|
+
set_callback(:validation, :after, *args, options, &block)
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def set_options_for_callback(options)
|
100
|
+
if options.key?(:on)
|
101
|
+
options[:on] = Array(options[:on])
|
102
|
+
options[:if] = [
|
103
|
+
->(o) {
|
104
|
+
options[:on].intersect?(Array(o.validation_context))
|
105
|
+
},
|
106
|
+
*options[:if]
|
107
|
+
]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
# Override run_validations! to include callbacks.
|
114
|
+
def run_validations!
|
115
|
+
_run_validation_callbacks { super }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model/validations/resolve_value"
|
4
|
+
require "active_support/core_ext/range"
|
5
|
+
|
6
|
+
module ActiveModel
|
7
|
+
module Validations
|
8
|
+
module Clusivity # :nodoc:
|
9
|
+
include ResolveValue
|
10
|
+
|
11
|
+
ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " \
|
12
|
+
"and must be supplied as the :in (or :within) option of the configuration hash"
|
13
|
+
|
14
|
+
def check_validity!
|
15
|
+
unless delimiter.respond_to?(:include?) || delimiter.respond_to?(:call) || delimiter.respond_to?(:to_sym)
|
16
|
+
raise ArgumentError, ERROR_MESSAGE
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def include?(record, value)
|
22
|
+
members = resolve_value(record, delimiter)
|
23
|
+
|
24
|
+
if value.is_a?(Array)
|
25
|
+
value.all? { |v| members.public_send(inclusion_method(members), v) }
|
26
|
+
else
|
27
|
+
members.public_send(inclusion_method(members), value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def delimiter
|
32
|
+
@delimiter ||= options[:in] || options[:within]
|
33
|
+
end
|
34
|
+
|
35
|
+
# After Ruby 2.2, <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
|
36
|
+
# possible values in the range for equality, which is slower but more accurate.
|
37
|
+
# <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
|
38
|
+
# endpoints, which is fast but is only accurate on Numeric, Time, Date,
|
39
|
+
# or DateTime ranges.
|
40
|
+
def inclusion_method(enumerable)
|
41
|
+
if enumerable.is_a? Range
|
42
|
+
case enumerable.begin || enumerable.end
|
43
|
+
when Numeric, Time, DateTime, Date
|
44
|
+
:cover?
|
45
|
+
else
|
46
|
+
:include?
|
47
|
+
end
|
48
|
+
else
|
49
|
+
:include?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Validations
|
5
|
+
module Comparability # :nodoc:
|
6
|
+
COMPARE_CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
|
7
|
+
equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
|
8
|
+
other_than: :!= }.freeze
|
9
|
+
|
10
|
+
def error_options(value, option_value)
|
11
|
+
options.except(*COMPARE_CHECKS.keys).merge!(
|
12
|
+
count: option_value,
|
13
|
+
value: value
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model/validations/comparability"
|
4
|
+
require "active_model/validations/resolve_value"
|
5
|
+
|
6
|
+
module ActiveModel
|
7
|
+
module Validations
|
8
|
+
class ComparisonValidator < EachValidator # :nodoc:
|
9
|
+
include Comparability
|
10
|
+
include ResolveValue
|
11
|
+
|
12
|
+
def check_validity!
|
13
|
+
unless options.keys.intersect?(COMPARE_CHECKS.keys)
|
14
|
+
raise ArgumentError, "Expected one of :greater_than, :greater_than_or_equal_to, "\
|
15
|
+
":equal_to, :less_than, :less_than_or_equal_to, or :other_than option to be supplied."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate_each(record, attr_name, value)
|
20
|
+
options.slice(*COMPARE_CHECKS.keys).each do |option, raw_option_value|
|
21
|
+
option_value = resolve_value(record, raw_option_value)
|
22
|
+
|
23
|
+
if value.nil? || value.blank?
|
24
|
+
return record.errors.add(attr_name, :blank, **error_options(value, option_value))
|
25
|
+
end
|
26
|
+
|
27
|
+
unless value.public_send(COMPARE_CHECKS[option], option_value)
|
28
|
+
record.errors.add(attr_name, option, **error_options(value, option_value))
|
29
|
+
end
|
30
|
+
rescue ArgumentError => e
|
31
|
+
record.errors.add(attr_name, e.message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module HelperMethods
|
37
|
+
# Validates the value of a specified attribute fulfills all
|
38
|
+
# defined comparisons with another value, proc, or attribute.
|
39
|
+
#
|
40
|
+
# class Person < ActiveRecord::Base
|
41
|
+
# validates_comparison_of :value, greater_than: 'the sum of its parts'
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# Configuration options:
|
45
|
+
# * <tt>:message</tt> - A custom error message (default is: "failed comparison").
|
46
|
+
# * <tt>:greater_than</tt> - Specifies the value must be greater than the
|
47
|
+
# supplied value. The default error message for this option is _"must be
|
48
|
+
# greater than %{count}"_.
|
49
|
+
# * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be
|
50
|
+
# greater than or equal to the supplied value. The default error message
|
51
|
+
# for this option is _"must be greater than or equal to %{count}"_.
|
52
|
+
# * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied
|
53
|
+
# value. The default error message for this option is _"must be equal to
|
54
|
+
# %{count}"_.
|
55
|
+
# * <tt>:less_than</tt> - Specifies the value must be less than the
|
56
|
+
# supplied value. The default error message for this option is _"must be
|
57
|
+
# less than %{count}"_.
|
58
|
+
# * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less
|
59
|
+
# than or equal to the supplied value. The default error message for
|
60
|
+
# this option is _"must be less than or equal to %{count}"_.
|
61
|
+
# * <tt>:other_than</tt> - Specifies the value must not be equal to the
|
62
|
+
# supplied value. The default error message for this option is _"must be
|
63
|
+
# other than %{count}"_.
|
64
|
+
#
|
65
|
+
# There is also a list of default options supported by every validator:
|
66
|
+
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
|
67
|
+
# See ActiveModel::Validations::ClassMethods#validates for more information.
|
68
|
+
#
|
69
|
+
# The validator requires at least one of the following checks to be supplied.
|
70
|
+
# Each will accept a proc, value, or a symbol which corresponds to a method:
|
71
|
+
#
|
72
|
+
# * <tt>:greater_than</tt>
|
73
|
+
# * <tt>:greater_than_or_equal_to</tt>
|
74
|
+
# * <tt>:equal_to</tt>
|
75
|
+
# * <tt>:less_than</tt>
|
76
|
+
# * <tt>:less_than_or_equal_to</tt>
|
77
|
+
# * <tt>:other_than</tt>
|
78
|
+
#
|
79
|
+
# For example:
|
80
|
+
#
|
81
|
+
# class Person < ActiveRecord::Base
|
82
|
+
# validates_comparison_of :birth_date, less_than_or_equal_to: -> { Date.today }
|
83
|
+
# validates_comparison_of :preferred_name, other_than: :given_name, allow_nil: true
|
84
|
+
# end
|
85
|
+
def validates_comparison_of(*attr_names)
|
86
|
+
validates_with ComparisonValidator, _merge_attributes(attr_names)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|