activemodel 7.0.8.1 → 7.2.2.1

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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -233
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +18 -18
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +5 -5
  7. data/lib/active_model/attribute/user_provided_default.rb +4 -0
  8. data/lib/active_model/attribute.rb +27 -2
  9. data/lib/active_model/attribute_assignment.rb +4 -2
  10. data/lib/active_model/attribute_methods.rb +145 -85
  11. data/lib/active_model/attribute_registration.rb +117 -0
  12. data/lib/active_model/attribute_set.rb +10 -1
  13. data/lib/active_model/attributes.rb +78 -48
  14. data/lib/active_model/callbacks.rb +6 -6
  15. data/lib/active_model/conversion.rb +14 -4
  16. data/lib/active_model/deprecator.rb +7 -0
  17. data/lib/active_model/dirty.rb +134 -13
  18. data/lib/active_model/error.rb +4 -3
  19. data/lib/active_model/errors.rb +37 -6
  20. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  21. data/lib/active_model/gem_version.rb +3 -3
  22. data/lib/active_model/lint.rb +1 -1
  23. data/lib/active_model/locale/en.yml +1 -0
  24. data/lib/active_model/model.rb +34 -2
  25. data/lib/active_model/naming.rb +29 -10
  26. data/lib/active_model/railtie.rb +4 -0
  27. data/lib/active_model/secure_password.rb +62 -24
  28. data/lib/active_model/serialization.rb +3 -3
  29. data/lib/active_model/serializers/json.rb +1 -1
  30. data/lib/active_model/translation.rb +18 -16
  31. data/lib/active_model/type/big_integer.rb +23 -1
  32. data/lib/active_model/type/binary.rb +7 -1
  33. data/lib/active_model/type/boolean.rb +11 -9
  34. data/lib/active_model/type/date.rb +28 -2
  35. data/lib/active_model/type/date_time.rb +45 -3
  36. data/lib/active_model/type/decimal.rb +39 -1
  37. data/lib/active_model/type/float.rb +30 -1
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
  39. data/lib/active_model/type/helpers/numeric.rb +6 -1
  40. data/lib/active_model/type/helpers/time_value.rb +50 -13
  41. data/lib/active_model/type/helpers/timezone.rb +5 -1
  42. data/lib/active_model/type/immutable_string.rb +37 -1
  43. data/lib/active_model/type/integer.rb +44 -1
  44. data/lib/active_model/type/registry.rb +2 -3
  45. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  46. data/lib/active_model/type/string.rb +9 -1
  47. data/lib/active_model/type/time.rb +48 -7
  48. data/lib/active_model/type/value.rb +17 -1
  49. data/lib/active_model/type.rb +1 -0
  50. data/lib/active_model/validations/absence.rb +1 -1
  51. data/lib/active_model/validations/acceptance.rb +1 -1
  52. data/lib/active_model/validations/callbacks.rb +5 -5
  53. data/lib/active_model/validations/clusivity.rb +5 -8
  54. data/lib/active_model/validations/comparability.rb +0 -11
  55. data/lib/active_model/validations/comparison.rb +16 -8
  56. data/lib/active_model/validations/format.rb +6 -7
  57. data/lib/active_model/validations/length.rb +10 -8
  58. data/lib/active_model/validations/numericality.rb +35 -23
  59. data/lib/active_model/validations/presence.rb +1 -1
  60. data/lib/active_model/validations/resolve_value.rb +26 -0
  61. data/lib/active_model/validations/validates.rb +4 -4
  62. data/lib/active_model/validations/with.rb +9 -2
  63. data/lib/active_model/validations.rb +44 -9
  64. data/lib/active_model/validator.rb +7 -5
  65. data/lib/active_model/version.rb +1 -1
  66. data/lib/active_model.rb +5 -1
  67. metadata +17 -12
@@ -18,6 +18,7 @@ en:
18
18
  too_long:
19
19
  one: "is too long (maximum is 1 character)"
20
20
  other: "is too long (maximum is %{count} characters)"
21
+ password_too_long: "is too long"
21
22
  too_short:
22
23
  one: "is too short (minimum is 1 character)"
23
24
  other: "is too short (minimum is %{count} characters)"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModel
4
- # == Active \Model \Basic \Model
4
+ # = Active \Model \Basic \Model
5
5
  #
6
6
  # Allows implementing models similar to ActiveRecord::Base.
7
7
  # Includes ActiveModel::API for the required interface for an
@@ -37,10 +37,42 @@ module ActiveModel
37
37
  # person.omg # => true
38
38
  #
39
39
  # For more detailed information on other functionalities available, please
40
- # refer to the specific modules included in <tt>ActiveModel::Model</tt>
40
+ # refer to the specific modules included in +ActiveModel::Model+
41
41
  # (see below).
42
42
  module Model
43
43
  extend ActiveSupport::Concern
44
44
  include ActiveModel::API
45
+ include ActiveModel::Access
46
+
47
+ ##
48
+ # :method: slice
49
+ #
50
+ # :call-seq: slice(*methods)
51
+ #
52
+ # Returns a hash of the given methods with their names as keys and returned
53
+ # values as values.
54
+ #
55
+ # person = Person.new(id: 1, name: "bob")
56
+ # person.slice(:id, :name)
57
+ # => { "id" => 1, "name" => "bob" }
58
+ #
59
+ #--
60
+ # Implemented by ActiveModel::Access#slice.
61
+
62
+ ##
63
+ # :method: values_at
64
+ #
65
+ # :call-seq: values_at(*methods)
66
+ #
67
+ # Returns an array of the values returned by the given methods.
68
+ #
69
+ # person = Person.new(id: 1, name: "bob")
70
+ # person.values_at(:id, :name)
71
+ # => [1, "bob"]
72
+ #
73
+ #--
74
+ # Implemented by ActiveModel::Access#values_at.
45
75
  end
76
+
77
+ ActiveSupport.run_load_hooks(:active_model, Model)
46
78
  end
@@ -195,18 +195,15 @@ module ActiveModel
195
195
  #
196
196
  # Specify +options+ with additional translating options.
197
197
  def human(options = {})
198
- return @human unless @klass.respond_to?(:lookup_ancestors) &&
199
- @klass.respond_to?(:i18n_scope)
200
-
201
- defaults = @klass.lookup_ancestors.map do |klass|
202
- klass.model_name.i18n_key
203
- end
198
+ return @human if i18n_keys.empty? || i18n_scope.empty?
204
199
 
200
+ key, *defaults = i18n_keys
205
201
  defaults << options[:default] if options[:default]
206
- defaults << @human
202
+ defaults << MISSING_TRANSLATION
207
203
 
208
- options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
209
- I18n.translate(defaults.shift, **options)
204
+ translation = I18n.translate(key, scope: i18n_scope, count: 1, **options, default: defaults)
205
+ translation = @human if translation == MISSING_TRANSLATION
206
+ translation
210
207
  end
211
208
 
212
209
  def uncountable?
@@ -214,12 +211,26 @@ module ActiveModel
214
211
  end
215
212
 
216
213
  private
214
+ MISSING_TRANSLATION = -(2**60) # :nodoc:
215
+
217
216
  def _singularize(string)
218
217
  ActiveSupport::Inflector.underscore(string).tr("/", "_")
219
218
  end
219
+
220
+ def i18n_keys
221
+ @i18n_keys ||= if @klass.respond_to?(:lookup_ancestors)
222
+ @klass.lookup_ancestors.map { |klass| klass.model_name.i18n_key }
223
+ else
224
+ []
225
+ end
226
+ end
227
+
228
+ def i18n_scope
229
+ @i18n_scope ||= @klass.respond_to?(:i18n_scope) ? [@klass.i18n_scope, :models] : []
230
+ end
220
231
  end
221
232
 
222
- # == Active \Model \Naming
233
+ # = Active \Model \Naming
223
234
  #
224
235
  # Creates a +model_name+ method on your object.
225
236
  #
@@ -336,5 +347,13 @@ module ActiveModel
336
347
  end
337
348
  end
338
349
  private_class_method :model_name_from_record_or_class
350
+
351
+ private
352
+ def inherited(base)
353
+ super
354
+ base.class_eval do
355
+ @_model_name = nil
356
+ end
357
+ end
339
358
  end
340
359
  end
@@ -9,6 +9,10 @@ module ActiveModel
9
9
 
10
10
  config.active_model = ActiveSupport::OrderedOptions.new
11
11
 
12
+ initializer "active_model.deprecator", before: :load_environment_config do |app|
13
+ app.deprecators[:active_model] = ActiveModel.deprecator
14
+ end
15
+
12
16
  initializer "active_model.secure_password" do
13
17
  ActiveModel::SecurePassword.min_cost = Rails.env.test?
14
18
  end
@@ -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
- # Where +XXX+ is the attribute name of your desired password.
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,12 +29,19 @@ module ActiveModel
29
29
  # it). When this attribute has a +nil+ value, the validation will not be
30
30
  # triggered.
31
31
  #
32
- # For further customizability, it is possible to suppress the default
33
- # validations by passing <tt>validations: false</tt> as an argument.
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
- # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
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.
36
41
  #
37
- # gem 'bcrypt', '~> 3.1.7'
42
+ # To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile:
43
+ #
44
+ # gem "bcrypt", "~> 3.1.7"
38
45
  #
39
46
  # ==== Examples
40
47
  #
@@ -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: 'david', password: '', password_confirmation: 'nomatch')
50
- # user.save # => false, password required
51
- # user.password = 'mUc3m00RsqyRe'
52
- # user.save # => false, confirmation doesn't match
53
- # user.password_confirmation = 'mUc3m00RsqyRe'
54
- # user.save # => true
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 # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
57
- # user.save # => true
58
- # user.authenticate('notright') # => false
59
- # user.authenticate('mUc3m00RsqyRe') # => user
60
- # user.authenticate_recovery_password('42password') # => user
61
- # User.find_by(name: 'david')&.authenticate('notright') # => false
62
- # User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
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
- $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
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
- validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
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
- define_method("#{attribute}_confirmation=") do |unencrypted_password|
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
- # == Active \Model \Serialization
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 <tt>ActiveModel::Serialization</tt> module, so there is no need to
37
- # explicitly include <tt>ActiveModel::Serialization</tt>.
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
  #
@@ -4,7 +4,7 @@ require "active_support/json"
4
4
 
5
5
  module ActiveModel
6
6
  module Serializers
7
- # == Active \Model \JSON \Serializer
7
+ # = Active \Model \JSON \Serializer
8
8
  module JSON
9
9
  extend ActiveSupport::Concern
10
10
  include ActiveModel::Serialization
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModel
4
- # == Active \Model \Translation
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
- options = { count: 1 }.merge!(options)
46
- parts = attribute.to_s.split(".")
47
- attribute = parts.pop
48
- namespace = parts.join("/") unless parts.empty?
49
- attributes_scope = "#{i18n_scope}.attributes"
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
- :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
54
+ :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
54
55
  end
55
- defaults << :"#{attributes_scope}.#{namespace}.#{attribute}"
56
+ defaults << :"#{i18n_scope}.attributes.#{namespace}.#{attribute}"
56
57
  else
57
58
  defaults = lookup_ancestors.map do |klass|
58
- :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}"
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.delete(:default) if options[:default]
64
- defaults << attribute.humanize
64
+ defaults << options[:default] if options[:default]
65
+ defaults << MISSING_TRANSLATION
65
66
 
66
- options[:default] = defaults
67
- I18n.translate(defaults.shift, **options)
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
- class BigInteger < Integer # :nodoc:
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
- class Binary < Value # :nodoc:
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
- # == Active \Model \Type \Boolean
5
+ # = Active Model \Boolean \Type
6
6
  #
7
- # A class that behaves like a boolean type, including rules for coercion of user input.
7
+ # A class that behaves like a boolean type, including rules for coercion of
8
+ # user input.
8
9
  #
9
- # === Coercion
10
- # Values set from user input will first be coerced into the appropriate ruby type.
11
- # Coercion behavior is roughly mapped to Ruby's boolean semantics.
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
- class Date < Value # :nodoc:
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
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
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
- class DateTime < Value # :nodoc:
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 = ::Date._parse(string)
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
- class Decimal < Value # :nodoc:
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
- class Float < Value # :nodoc:
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
@@ -6,7 +6,11 @@ module ActiveModel
6
6
  class AcceptsMultiparameterTime < Module
7
7
  module InstanceMethods
8
8
  def serialize(value)
9
- super(cast(value))
9
+ serialize_cast_value(cast(value))
10
+ end
11
+
12
+ def serialize_cast_value(value)
13
+ value
10
14
  end
11
15
 
12
16
  def cast(value)