activemodel 7.0.7 → 7.1.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +157 -142
- data/MIT-LICENSE +1 -1
- data/README.rdoc +9 -9
- data/lib/active_model/access.rb +16 -0
- data/lib/active_model/api.rb +5 -5
- data/lib/active_model/attribute/user_provided_default.rb +4 -0
- data/lib/active_model/attribute.rb +26 -1
- data/lib/active_model/attribute_assignment.rb +1 -1
- data/lib/active_model/attribute_methods.rb +102 -63
- data/lib/active_model/attribute_registration.rb +77 -0
- data/lib/active_model/attribute_set.rb +10 -1
- data/lib/active_model/attributes.rb +62 -45
- data/lib/active_model/callbacks.rb +5 -5
- data/lib/active_model/conversion.rb +14 -4
- data/lib/active_model/deprecator.rb +7 -0
- data/lib/active_model/dirty.rb +134 -13
- data/lib/active_model/error.rb +4 -3
- data/lib/active_model/errors.rb +37 -6
- data/lib/active_model/forbidden_attributes_protection.rb +2 -0
- data/lib/active_model/gem_version.rb +4 -4
- data/lib/active_model/lint.rb +1 -1
- data/lib/active_model/locale/en.yml +1 -0
- data/lib/active_model/model.rb +34 -2
- data/lib/active_model/naming.rb +29 -10
- data/lib/active_model/railtie.rb +4 -0
- data/lib/active_model/secure_password.rb +61 -23
- data/lib/active_model/serialization.rb +3 -3
- data/lib/active_model/serializers/json.rb +1 -1
- data/lib/active_model/translation.rb +18 -16
- data/lib/active_model/type/big_integer.rb +23 -1
- data/lib/active_model/type/binary.rb +7 -1
- data/lib/active_model/type/boolean.rb +11 -9
- data/lib/active_model/type/date.rb +28 -2
- data/lib/active_model/type/date_time.rb +45 -3
- data/lib/active_model/type/decimal.rb +39 -1
- data/lib/active_model/type/float.rb +30 -1
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
- data/lib/active_model/type/helpers/numeric.rb +6 -1
- data/lib/active_model/type/helpers/time_value.rb +28 -12
- data/lib/active_model/type/immutable_string.rb +37 -1
- data/lib/active_model/type/integer.rb +44 -1
- data/lib/active_model/type/serialize_cast_value.rb +47 -0
- data/lib/active_model/type/string.rb +9 -1
- data/lib/active_model/type/time.rb +48 -7
- data/lib/active_model/type/value.rb +17 -1
- data/lib/active_model/type.rb +1 -0
- data/lib/active_model/validations/absence.rb +1 -1
- data/lib/active_model/validations/acceptance.rb +1 -1
- data/lib/active_model/validations/callbacks.rb +4 -4
- data/lib/active_model/validations/clusivity.rb +5 -8
- data/lib/active_model/validations/comparability.rb +0 -11
- data/lib/active_model/validations/comparison.rb +15 -7
- data/lib/active_model/validations/format.rb +6 -7
- data/lib/active_model/validations/length.rb +10 -8
- data/lib/active_model/validations/numericality.rb +35 -23
- data/lib/active_model/validations/presence.rb +1 -1
- data/lib/active_model/validations/resolve_value.rb +26 -0
- data/lib/active_model/validations/validates.rb +4 -4
- data/lib/active_model/validations/with.rb +9 -2
- data/lib/active_model/validations.rb +44 -9
- data/lib/active_model/validator.rb +7 -5
- data/lib/active_model/version.rb +1 -1
- data/lib/active_model.rb +5 -1
- metadata +13 -8
@@ -16,8 +16,8 @@ module ActiveModel
|
|
16
16
|
|
17
17
|
module ClassMethods
|
18
18
|
# Adds methods to set and authenticate against a BCrypt password.
|
19
|
-
# This mechanism requires you to have a +XXX_digest+ attribute
|
20
|
-
#
|
19
|
+
# This mechanism requires you to have a +XXX_digest+ attribute,
|
20
|
+
# where +XXX+ is the attribute name of your desired password.
|
21
21
|
#
|
22
22
|
# The following validations are added automatically:
|
23
23
|
# * Password must be present on creation
|
@@ -29,10 +29,17 @@ module ActiveModel
|
|
29
29
|
# it). When this attribute has a +nil+ value, the validation will not be
|
30
30
|
# triggered.
|
31
31
|
#
|
32
|
-
#
|
33
|
-
#
|
32
|
+
# Additionally, a +XXX_challenge+ attribute is created. When set to a
|
33
|
+
# value other than +nil+, it will validate against the currently persisted
|
34
|
+
# password. This validation relies on dirty tracking, as provided by
|
35
|
+
# ActiveModel::Dirty; if dirty tracking methods are not defined, this
|
36
|
+
# validation will fail.
|
34
37
|
#
|
35
|
-
#
|
38
|
+
# All of the above validations can be omitted by passing
|
39
|
+
# <tt>validations: false</tt> as an argument. This allows complete
|
40
|
+
# customizability of validation behavior.
|
41
|
+
#
|
42
|
+
# To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile:
|
36
43
|
#
|
37
44
|
# gem 'bcrypt', '~> 3.1.7'
|
38
45
|
#
|
@@ -46,20 +53,30 @@ module ActiveModel
|
|
46
53
|
# has_secure_password :recovery_password, validations: false
|
47
54
|
# end
|
48
55
|
#
|
49
|
-
# user = User.new(name:
|
50
|
-
#
|
51
|
-
# user.password
|
52
|
-
# user.
|
53
|
-
# user.
|
54
|
-
# user.
|
56
|
+
# user = User.new(name: "david", password: "", password_confirmation: "nomatch")
|
57
|
+
#
|
58
|
+
# user.save # => false, password required
|
59
|
+
# user.password = "vr00m"
|
60
|
+
# user.save # => false, confirmation doesn't match
|
61
|
+
# user.password_confirmation = "vr00m"
|
62
|
+
# user.save # => true
|
63
|
+
#
|
64
|
+
# user.authenticate("notright") # => false
|
65
|
+
# user.authenticate("vr00m") # => user
|
66
|
+
# User.find_by(name: "david")&.authenticate("notright") # => false
|
67
|
+
# User.find_by(name: "david")&.authenticate("vr00m") # => user
|
68
|
+
#
|
55
69
|
# user.recovery_password = "42password"
|
56
|
-
# user.recovery_password_digest
|
57
|
-
# user.save
|
58
|
-
#
|
59
|
-
# user.
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
70
|
+
# user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
|
71
|
+
# user.save # => true
|
72
|
+
#
|
73
|
+
# user.authenticate_recovery_password("42password") # => user
|
74
|
+
#
|
75
|
+
# user.update(password: "pwn3d", password_challenge: "") # => false, challenge doesn't authenticate
|
76
|
+
# user.update(password: "nohack4u", password_challenge: "vr00m") # => true
|
77
|
+
#
|
78
|
+
# user.authenticate("vr00m") # => false, old password
|
79
|
+
# user.authenticate("nohack4u") # => user
|
63
80
|
#
|
64
81
|
# ===== Conditionally requiring a password
|
65
82
|
#
|
@@ -88,7 +105,7 @@ module ActiveModel
|
|
88
105
|
begin
|
89
106
|
require "bcrypt"
|
90
107
|
rescue LoadError
|
91
|
-
|
108
|
+
warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install."
|
92
109
|
raise
|
93
110
|
end
|
94
111
|
|
@@ -105,7 +122,24 @@ module ActiveModel
|
|
105
122
|
record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
|
106
123
|
end
|
107
124
|
|
108
|
-
|
125
|
+
validate do |record|
|
126
|
+
if challenge = record.public_send(:"#{attribute}_challenge")
|
127
|
+
digest_was = record.public_send(:"#{attribute}_digest_was") if record.respond_to?(:"#{attribute}_digest_was")
|
128
|
+
|
129
|
+
unless digest_was.present? && BCrypt::Password.new(digest_was).is_password?(challenge)
|
130
|
+
record.errors.add(:"#{attribute}_challenge")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes).
|
136
|
+
validate do |record|
|
137
|
+
password_value = record.public_send(attribute)
|
138
|
+
if password_value.present? && password_value.bytesize > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
|
139
|
+
record.errors.add(attribute, :password_too_long)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
109
143
|
validates_confirmation_of attribute, allow_blank: true
|
110
144
|
end
|
111
145
|
end
|
@@ -126,9 +160,7 @@ module ActiveModel
|
|
126
160
|
end
|
127
161
|
end
|
128
162
|
|
129
|
-
|
130
|
-
instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
|
131
|
-
end
|
163
|
+
attr_accessor :"#{attribute}_confirmation", :"#{attribute}_challenge"
|
132
164
|
|
133
165
|
# Returns +self+ if the password is correct, otherwise +false+.
|
134
166
|
#
|
@@ -145,6 +177,12 @@ module ActiveModel
|
|
145
177
|
attribute_digest.present? && BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
|
146
178
|
end
|
147
179
|
|
180
|
+
# Returns the salt, a small chunk of random data added to the password before it's hashed.
|
181
|
+
define_method("#{attribute}_salt") do
|
182
|
+
attribute_digest = public_send("#{attribute}_digest")
|
183
|
+
attribute_digest.present? ? BCrypt::Password.new(attribute_digest).salt : nil
|
184
|
+
end
|
185
|
+
|
148
186
|
alias_method :authenticate, :authenticate_password if attribute == :password
|
149
187
|
end
|
150
188
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
require "active_support/core_ext/enumerable"
|
4
4
|
|
5
5
|
module ActiveModel
|
6
|
-
#
|
6
|
+
# = Active \Model \Serialization
|
7
7
|
#
|
8
8
|
# Provides a basic serialization to a serializable_hash for your objects.
|
9
9
|
#
|
@@ -33,8 +33,8 @@ module ActiveModel
|
|
33
33
|
# at the private method +read_attribute_for_serialization+.
|
34
34
|
#
|
35
35
|
# ActiveModel::Serializers::JSON module automatically includes
|
36
|
-
# the
|
37
|
-
# explicitly include
|
36
|
+
# the +ActiveModel::Serialization+ module, so there is no need to
|
37
|
+
# explicitly include +ActiveModel::Serialization+.
|
38
38
|
#
|
39
39
|
# A minimal implementation including JSON would be:
|
40
40
|
#
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveModel
|
4
|
-
#
|
4
|
+
# = Active \Model \Translation
|
5
5
|
#
|
6
|
-
# Provides integration between your object and the Rails internationalization
|
6
|
+
# Provides integration between your object and the \Rails internationalization
|
7
7
|
# (i18n) framework.
|
8
8
|
#
|
9
9
|
# A minimal implementation could be:
|
@@ -16,7 +16,7 @@ module ActiveModel
|
|
16
16
|
# # => "My attribute"
|
17
17
|
#
|
18
18
|
# This also provides the required class methods for hooking into the
|
19
|
-
# Rails internationalization API, including being able to define a
|
19
|
+
# \Rails internationalization API, including being able to define a
|
20
20
|
# class-based +i18n_scope+ and +lookup_ancestors+ to find translations in
|
21
21
|
# parent classes.
|
22
22
|
module Translation
|
@@ -35,6 +35,8 @@ module ActiveModel
|
|
35
35
|
ancestors.select { |x| x.respond_to?(:model_name) }
|
36
36
|
end
|
37
37
|
|
38
|
+
MISSING_TRANSLATION = -(2**60) # :nodoc:
|
39
|
+
|
38
40
|
# Transforms attribute names into a more human format, such as "First name"
|
39
41
|
# instead of "first_name".
|
40
42
|
#
|
@@ -42,29 +44,29 @@ module ActiveModel
|
|
42
44
|
#
|
43
45
|
# Specify +options+ with additional translating options.
|
44
46
|
def human_attribute_name(attribute, options = {})
|
45
|
-
|
46
|
-
|
47
|
-
attribute
|
48
|
-
|
49
|
-
|
47
|
+
attribute = attribute.to_s
|
48
|
+
|
49
|
+
if attribute.include?(".")
|
50
|
+
namespace, _, attribute = attribute.rpartition(".")
|
51
|
+
namespace.tr!(".", "/")
|
50
52
|
|
51
|
-
if namespace
|
52
53
|
defaults = lookup_ancestors.map do |klass|
|
53
|
-
:"#{
|
54
|
+
:"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
|
54
55
|
end
|
55
|
-
defaults << :"#{
|
56
|
+
defaults << :"#{i18n_scope}.attributes.#{namespace}.#{attribute}"
|
56
57
|
else
|
57
58
|
defaults = lookup_ancestors.map do |klass|
|
58
|
-
:"#{
|
59
|
+
:"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
|
59
60
|
end
|
60
61
|
end
|
61
62
|
|
62
63
|
defaults << :"attributes.#{attribute}"
|
63
|
-
defaults << options
|
64
|
-
defaults <<
|
64
|
+
defaults << options[:default] if options[:default]
|
65
|
+
defaults << MISSING_TRANSLATION
|
65
66
|
|
66
|
-
|
67
|
-
|
67
|
+
translation = I18n.translate(defaults.shift, count: 1, **options, default: defaults)
|
68
|
+
translation = attribute.humanize if translation == MISSING_TRANSLATION
|
69
|
+
translation
|
68
70
|
end
|
69
71
|
end
|
70
72
|
end
|
@@ -4,7 +4,29 @@ require "active_model/type/integer"
|
|
4
4
|
|
5
5
|
module ActiveModel
|
6
6
|
module Type
|
7
|
-
|
7
|
+
# = Active Model \BigInteger \Type
|
8
|
+
#
|
9
|
+
# Attribute type for integers that can be serialized to an unlimited number
|
10
|
+
# of bytes. This type is registered under the +:big_integer+ key.
|
11
|
+
#
|
12
|
+
# class Person
|
13
|
+
# include ActiveModel::Attributes
|
14
|
+
#
|
15
|
+
# attribute :id, :big_integer
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# person = Person.new
|
19
|
+
# person.id = "18_000_000_000"
|
20
|
+
#
|
21
|
+
# person.id # => 18000000000
|
22
|
+
#
|
23
|
+
# All casting and serialization are performed in the same way as the
|
24
|
+
# standard ActiveModel::Type::Integer type.
|
25
|
+
class BigInteger < Integer
|
26
|
+
def serialize_cast_value(value) # :nodoc:
|
27
|
+
value
|
28
|
+
end
|
29
|
+
|
8
30
|
private
|
9
31
|
def max_value
|
10
32
|
::Float::INFINITY
|
@@ -2,7 +2,13 @@
|
|
2
2
|
|
3
3
|
module ActiveModel
|
4
4
|
module Type
|
5
|
-
|
5
|
+
# = Active Model \Binary \Type
|
6
|
+
#
|
7
|
+
# Attribute type for representation of binary data. This type is registered
|
8
|
+
# under the +:binary+ key.
|
9
|
+
#
|
10
|
+
# Non-string values are coerced to strings using their +to_s+ method.
|
11
|
+
class Binary < Value
|
6
12
|
def type
|
7
13
|
:binary
|
8
14
|
end
|
@@ -2,17 +2,15 @@
|
|
2
2
|
|
3
3
|
module ActiveModel
|
4
4
|
module Type
|
5
|
-
#
|
5
|
+
# = Active Model \Boolean \Type
|
6
6
|
#
|
7
|
-
# A class that behaves like a boolean type, including rules for coercion of
|
7
|
+
# A class that behaves like a boolean type, including rules for coercion of
|
8
|
+
# user input.
|
8
9
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# - "false", "f" , "0", +0+ or any other value in +FALSE_VALUES+ will be coerced to +false+
|
14
|
-
# - Empty strings are coerced to +nil+
|
15
|
-
# - All other values will be coerced to +true+
|
10
|
+
# - <tt>"false"</tt>, <tt>"f"</tt>, <tt>"0"</tt>, +0+ or any other value in
|
11
|
+
# +FALSE_VALUES+ will be coerced to +false+.
|
12
|
+
# - Empty strings are coerced to +nil+.
|
13
|
+
# - All other values will be coerced to +true+.
|
16
14
|
class Boolean < Value
|
17
15
|
FALSE_VALUES = [
|
18
16
|
false, 0,
|
@@ -33,6 +31,10 @@ module ActiveModel
|
|
33
31
|
cast(value)
|
34
32
|
end
|
35
33
|
|
34
|
+
def serialize_cast_value(value) # :nodoc:
|
35
|
+
value
|
36
|
+
end
|
37
|
+
|
36
38
|
private
|
37
39
|
def cast_value(value)
|
38
40
|
if value == ""
|
@@ -2,7 +2,28 @@
|
|
2
2
|
|
3
3
|
module ActiveModel
|
4
4
|
module Type
|
5
|
-
|
5
|
+
# = Active Model \Date \Type
|
6
|
+
#
|
7
|
+
# Attribute type for date representation. It is registered under the
|
8
|
+
# +:date+ key.
|
9
|
+
#
|
10
|
+
# class Person
|
11
|
+
# include ActiveModel::Attributes
|
12
|
+
#
|
13
|
+
# attribute :birthday, :date
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# person = Person.new
|
17
|
+
# person.birthday = "1989-07-13"
|
18
|
+
#
|
19
|
+
# person.birthday.class # => Date
|
20
|
+
# person.birthday.year # => 1989
|
21
|
+
# person.birthday.month # => 7
|
22
|
+
# person.birthday.day # => 13
|
23
|
+
#
|
24
|
+
# String values are parsed using the ISO 8601 date format. Any other values
|
25
|
+
# are cast using their +to_date+ method, if it exists.
|
26
|
+
class Date < Value
|
6
27
|
include Helpers::Timezone
|
7
28
|
include Helpers::AcceptsMultiparameterTime.new
|
8
29
|
|
@@ -34,7 +55,12 @@ module ActiveModel
|
|
34
55
|
end
|
35
56
|
|
36
57
|
def fallback_string_to_date(string)
|
37
|
-
|
58
|
+
parts = begin
|
59
|
+
::Date._parse(string, false)
|
60
|
+
rescue ArgumentError
|
61
|
+
end
|
62
|
+
|
63
|
+
new_date(*parts.values_at(:year, :mon, :mday)) if parts
|
38
64
|
end
|
39
65
|
|
40
66
|
def new_date(year, mon, mday)
|
@@ -2,12 +2,49 @@
|
|
2
2
|
|
3
3
|
module ActiveModel
|
4
4
|
module Type
|
5
|
-
|
5
|
+
# = Active Model \DateTime \Type
|
6
|
+
#
|
7
|
+
# Attribute type to represent dates and times. It is registered under the
|
8
|
+
# +:datetime+ key.
|
9
|
+
#
|
10
|
+
# class Event
|
11
|
+
# include ActiveModel::Attributes
|
12
|
+
#
|
13
|
+
# attribute :start, :datetime
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# event = Event.new
|
17
|
+
# event.start = "Wed, 04 Sep 2013 03:00:00 EAT"
|
18
|
+
#
|
19
|
+
# event.start.class # => Time
|
20
|
+
# event.start.year # => 2013
|
21
|
+
# event.start.month # => 9
|
22
|
+
# event.start.day # => 4
|
23
|
+
# event.start.hour # => 3
|
24
|
+
# event.start.min # => 0
|
25
|
+
# event.start.sec # => 0
|
26
|
+
# event.start.zone # => "EAT"
|
27
|
+
#
|
28
|
+
# String values are parsed using the ISO 8601 datetime format. Partial
|
29
|
+
# time-only formats are also accepted.
|
30
|
+
#
|
31
|
+
# event.start = "06:07:08+09:00"
|
32
|
+
# event.start.utc # => 1999-12-31 21:07:08 UTC
|
33
|
+
#
|
34
|
+
# The degree of sub-second precision can be customized when declaring an
|
35
|
+
# attribute:
|
36
|
+
#
|
37
|
+
# class Event
|
38
|
+
# include ActiveModel::Attributes
|
39
|
+
#
|
40
|
+
# attribute :start, :datetime, precision: 4
|
41
|
+
# end
|
42
|
+
class DateTime < Value
|
6
43
|
include Helpers::Timezone
|
7
|
-
include Helpers::TimeValue
|
8
44
|
include Helpers::AcceptsMultiparameterTime.new(
|
9
45
|
defaults: { 4 => 0, 5 => 0 }
|
10
46
|
)
|
47
|
+
include Helpers::TimeValue
|
11
48
|
|
12
49
|
def type
|
13
50
|
:datetime
|
@@ -28,7 +65,12 @@ module ActiveModel
|
|
28
65
|
end
|
29
66
|
|
30
67
|
def fallback_string_to_time(string)
|
31
|
-
time_hash =
|
68
|
+
time_hash = begin
|
69
|
+
::Date._parse(string)
|
70
|
+
rescue ArgumentError
|
71
|
+
end
|
72
|
+
return unless time_hash
|
73
|
+
|
32
74
|
time_hash[:sec_fraction] = microseconds(time_hash)
|
33
75
|
|
34
76
|
new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
|
@@ -4,7 +4,45 @@ require "bigdecimal/util"
|
|
4
4
|
|
5
5
|
module ActiveModel
|
6
6
|
module Type
|
7
|
-
|
7
|
+
# = Active Model \Decimal \Type
|
8
|
+
#
|
9
|
+
# Attribute type for decimal, high-precision floating point numeric
|
10
|
+
# representation. It is registered under the +:decimal+ key.
|
11
|
+
#
|
12
|
+
# class BagOfCoffee
|
13
|
+
# include ActiveModel::Attributes
|
14
|
+
#
|
15
|
+
# attribute :weight, :decimal
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Numeric instances are converted to BigDecimal instances. Any other objects
|
19
|
+
# are cast using their +to_d+ method, except for blank strings, which are
|
20
|
+
# cast to +nil+. If a +to_d+ method is not defined, the object is converted
|
21
|
+
# to a string using +to_s+, which is then cast using +to_d+.
|
22
|
+
#
|
23
|
+
# bag = BagOfCoffee.new
|
24
|
+
#
|
25
|
+
# bag.weight = 0.01
|
26
|
+
# bag.weight # => 0.1e-1
|
27
|
+
#
|
28
|
+
# bag.weight = "0.01"
|
29
|
+
# bag.weight # => 0.1e-1
|
30
|
+
#
|
31
|
+
# bag.weight = ""
|
32
|
+
# bag.weight # => nil
|
33
|
+
#
|
34
|
+
# bag.weight = :arbitrary
|
35
|
+
# bag.weight # => nil (the result of `.to_s.to_d`)
|
36
|
+
#
|
37
|
+
# Decimal precision defaults to 18, and can be customized when declaring an
|
38
|
+
# attribute:
|
39
|
+
#
|
40
|
+
# class BagOfCoffee
|
41
|
+
# include ActiveModel::Attributes
|
42
|
+
#
|
43
|
+
# attribute :weight, :decimal, precision: 24
|
44
|
+
# end
|
45
|
+
class Decimal < Value
|
8
46
|
include Helpers::Numeric
|
9
47
|
BIGDECIMAL_PRECISION = 18
|
10
48
|
|
@@ -4,7 +4,36 @@ require "active_support/core_ext/object/try"
|
|
4
4
|
|
5
5
|
module ActiveModel
|
6
6
|
module Type
|
7
|
-
|
7
|
+
# = Active Model \Float \Type
|
8
|
+
#
|
9
|
+
# Attribute type for floating point numeric values. It is registered under
|
10
|
+
# the +:float+ key.
|
11
|
+
#
|
12
|
+
# class BagOfCoffee
|
13
|
+
# include ActiveModel::Attributes
|
14
|
+
#
|
15
|
+
# attribute :weight, :float
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Values are cast using their +to_f+ method, except for the following
|
19
|
+
# strings:
|
20
|
+
#
|
21
|
+
# - Blank strings are cast to +nil+.
|
22
|
+
# - <tt>"Infinity"</tt> is cast to +Float::INFINITY+.
|
23
|
+
# - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
|
24
|
+
# - <tt>"NaN"</tt> is cast to +Float::NAN+.
|
25
|
+
#
|
26
|
+
# bag = BagOfCoffee.new
|
27
|
+
#
|
28
|
+
# bag.weight = "0.25"
|
29
|
+
# bag.weight # => 0.25
|
30
|
+
#
|
31
|
+
# bag.weight = ""
|
32
|
+
# bag.weight # => nil
|
33
|
+
#
|
34
|
+
# bag.weight = "NaN"
|
35
|
+
# bag.weight # => Float::NAN
|
36
|
+
class Float < Value
|
8
37
|
include Helpers::Numeric
|
9
38
|
|
10
39
|
def type
|
@@ -8,6 +8,10 @@ module ActiveModel
|
|
8
8
|
cast(value)
|
9
9
|
end
|
10
10
|
|
11
|
+
def serialize_cast_value(value)
|
12
|
+
value
|
13
|
+
end
|
14
|
+
|
11
15
|
def cast(value)
|
12
16
|
# Checks whether the value is numeric. Spaceship operator
|
13
17
|
# will return nil if value is not numeric.
|
@@ -38,7 +42,8 @@ module ActiveModel
|
|
38
42
|
end
|
39
43
|
|
40
44
|
def number_to_non_number?(old_value, new_value_before_type_cast)
|
41
|
-
old_value != nil &&
|
45
|
+
old_value != nil && !new_value_before_type_cast.is_a?(::Numeric) &&
|
46
|
+
non_numeric_string?(new_value_before_type_cast.to_s)
|
42
47
|
end
|
43
48
|
|
44
49
|
def non_numeric_string?(value)
|
@@ -7,7 +7,7 @@ module ActiveModel
|
|
7
7
|
module Type
|
8
8
|
module Helpers # :nodoc: all
|
9
9
|
module TimeValue
|
10
|
-
def
|
10
|
+
def serialize_cast_value(value)
|
11
11
|
value = apply_seconds_precision(value)
|
12
12
|
|
13
13
|
if value.acts_like?(:time)
|
@@ -69,20 +69,36 @@ module ActiveModel
|
|
69
69
|
\z
|
70
70
|
/x
|
71
71
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
72
|
+
if RUBY_VERSION >= "3.2"
|
73
|
+
def fast_string_to_time(string)
|
74
|
+
return unless ISO_DATETIME.match?(string)
|
75
|
+
|
76
|
+
if is_utc?
|
77
|
+
# XXX: Wrapping the Time object with Time.at because Time.new with `in:` in Ruby 3.2.0 used to return an invalid Time object
|
78
|
+
# see: https://bugs.ruby-lang.org/issues/19292
|
79
|
+
::Time.at(::Time.new(string, in: "UTC"))
|
80
|
+
else
|
81
|
+
::Time.new(string)
|
82
|
+
end
|
83
|
+
rescue ArgumentError
|
84
|
+
nil
|
79
85
|
end
|
86
|
+
else
|
87
|
+
def fast_string_to_time(string)
|
88
|
+
return unless ISO_DATETIME =~ string
|
80
89
|
|
81
|
-
|
82
|
-
|
83
|
-
|
90
|
+
usec = $7.to_i
|
91
|
+
usec_len = $7&.length
|
92
|
+
if usec_len&.< 6
|
93
|
+
usec *= 10**(6 - usec_len)
|
94
|
+
end
|
84
95
|
|
85
|
-
|
96
|
+
if $8
|
97
|
+
offset = $8 == "Z" ? 0 : $8.to_i * 3600 + $9.to_i * 60
|
98
|
+
end
|
99
|
+
|
100
|
+
new_time($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
|
101
|
+
end
|
86
102
|
end
|
87
103
|
end
|
88
104
|
end
|
@@ -2,7 +2,39 @@
|
|
2
2
|
|
3
3
|
module ActiveModel
|
4
4
|
module Type
|
5
|
-
|
5
|
+
# = Active Model \ImmutableString \Type
|
6
|
+
#
|
7
|
+
# Attribute type to represent immutable strings. It casts incoming values to
|
8
|
+
# frozen strings.
|
9
|
+
#
|
10
|
+
# class Person
|
11
|
+
# include ActiveModel::Attributes
|
12
|
+
#
|
13
|
+
# attribute :name, :immutable_string
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# person = Person.new
|
17
|
+
# person.name = 1
|
18
|
+
#
|
19
|
+
# person.name # => "1"
|
20
|
+
# person.name.frozen? # => true
|
21
|
+
#
|
22
|
+
# Values are coerced to strings using their +to_s+ method. Boolean values
|
23
|
+
# are treated differently, however: +true+ will be cast to <tt>"t"</tt> and
|
24
|
+
# +false+ will be cast to <tt>"f"</tt>. These strings can be customized when
|
25
|
+
# declaring an attribute:
|
26
|
+
#
|
27
|
+
# class Person
|
28
|
+
# include ActiveModel::Attributes
|
29
|
+
#
|
30
|
+
# attribute :active, :immutable_string, true: "aye", false: "nay"
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# person = Person.new
|
34
|
+
# person.active = true
|
35
|
+
#
|
36
|
+
# person.active # => "aye"
|
37
|
+
class ImmutableString < Value
|
6
38
|
def initialize(**args)
|
7
39
|
@true = -(args.delete(:true)&.to_s || "t")
|
8
40
|
@false = -(args.delete(:false)&.to_s || "f")
|
@@ -22,6 +54,10 @@ module ActiveModel
|
|
22
54
|
end
|
23
55
|
end
|
24
56
|
|
57
|
+
def serialize_cast_value(value) # :nodoc:
|
58
|
+
value
|
59
|
+
end
|
60
|
+
|
25
61
|
private
|
26
62
|
def cast_value(value)
|
27
63
|
case value
|