diff options
Diffstat (limited to 'activemodel/lib')
30 files changed, 231 insertions, 149 deletions
diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb index 27f3e38e31..3f19cda07b 100644 --- a/activemodel/lib/active_model/attribute.rb +++ b/activemodel/lib/active_model/attribute.rb @@ -133,10 +133,6 @@ module ActiveModel end protected - - attr_reader :original_attribute - alias_method :assigned?, :original_attribute - def original_value_for_database if assigned? original_attribute.original_value_for_database @@ -146,6 +142,9 @@ module ActiveModel end private + attr_reader :original_attribute + alias :assigned? :original_attribute + def initialize_dup(other) if defined?(@value) && @value.duplicable? @value = @value.dup diff --git a/activemodel/lib/active_model/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb index f274b687d4..9dc16e882d 100644 --- a/activemodel/lib/active_model/attribute/user_provided_default.rb +++ b/activemodel/lib/active_model/attribute/user_provided_default.rb @@ -22,8 +22,29 @@ module ActiveModel self.class.new(name, user_provided_value, type, original_attribute) end - protected + def marshal_dump + result = [ + name, + value_before_type_cast, + type, + original_attribute, + ] + result << value if defined?(@value) + result + end + + def marshal_load(values) + name, user_provided_value, type, original_attribute, value = values + @name = name + @user_provided_value = user_provided_value + @type = type + @original_attribute = original_attribute + if values.length == 5 + @value = value + end + end + private attr_reader :user_provided_value end end diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index aa931119ff..217bf1ac01 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -35,6 +35,8 @@ module ActiveModel _assign_attributes(sanitize_for_mass_assignment(attributes)) end + alias attributes= assign_attributes + private def _assign_attributes(attributes) diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb index c67e1b809a..6abf37bd44 100644 --- a/activemodel/lib/active_model/attribute_mutation_tracker.rb +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -11,6 +11,10 @@ module ActiveModel @forced_changes = Set.new end + def changed_attribute_names + attr_names.select { |attr_name| changed?(attr_name) } + end + def changed_values attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if changed?(attr_name) @@ -23,7 +27,7 @@ module ActiveModel attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| change = change_to_attribute(attr_name) if change - result[attr_name] = change + result.merge!(attr_name => change) end end end @@ -65,13 +69,8 @@ module ActiveModel forced_changes << attr_name.to_s end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :attributes, :forced_changes - private + attr_reader :attributes, :forced_changes def attr_names attributes.keys @@ -81,6 +80,10 @@ module ActiveModel class NullMutationTracker # :nodoc: include Singleton + def changed_attribute_names(*) + [] + end + def changed_values(*) {} end diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb index 54a5dd4064..a890ee3932 100644 --- a/activemodel/lib/active_model/attribute_set.rb +++ b/activemodel/lib/active_model/attribute_set.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "active_support/core_ext/object/deep_dup" require "active_model/attribute_set/builder" require "active_model/attribute_set/yaml_encoder" diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb index 758eb830fc..2b1c2206ec 100644 --- a/activemodel/lib/active_model/attribute_set/builder.rb +++ b/activemodel/lib/active_model/attribute_set/builder.rb @@ -22,12 +22,12 @@ module ActiveModel class LazyAttributeHash # :nodoc: delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize - def initialize(types, values, additional_types, default_attributes) + def initialize(types, values, additional_types, default_attributes, delegate_hash = {}) @types = types @values = values @additional_types = additional_types @materialized = false - @delegate_hash = {} + @delegate_hash = delegate_hash @default_attributes = default_attributes end @@ -76,21 +76,20 @@ module ActiveModel end def marshal_dump - materialize + [@types, @values, @additional_types, @default_attributes, @delegate_hash] end - def marshal_load(delegate_hash) - @delegate_hash = delegate_hash - @types = {} - @values = {} - @additional_types = {} - @materialized = true + def marshal_load(values) + if values.is_a?(Hash) + empty_hash = {}.freeze + initialize(empty_hash, empty_hash, empty_hash, empty_hash, values) + @materialized = true + else + initialize(*values) + end end protected - - attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes - def materialize unless @materialized values.each_key { |key| self[key] } @@ -103,6 +102,7 @@ module ActiveModel end private + attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes def assign_default_value(name) type = additional_types.fetch(name, types[name]) diff --git a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb index 4ea945b956..ea1efc160e 100644 --- a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb +++ b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb @@ -33,8 +33,7 @@ module ActiveModel end end - protected - + private attr_reader :default_types end end diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index cac461b549..5bf213d593 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/object/deep_dup" require "active_model/attribute_set" require "active_model/attribute/user_provided_default" @@ -24,13 +23,13 @@ module ActiveModel end self.attribute_types = attribute_types.merge(name => type) define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type) - define_attribute_methods(name) + define_attribute_method(name) end private def define_method_attribute=(name) - safe_name = name.unpack("h*".freeze).first + safe_name = name.unpack1("h*".freeze) ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 @@ -67,6 +66,10 @@ module ActiveModel super end + def attributes + @attributes.to_hash + end + private def write_attribute(attr_name, value) @@ -100,7 +103,7 @@ module ActiveModel def self.set_name_cache(name, value) const_name = "ATTR_#{name}" unless const_defined? const_name - const_set const_name, value.dup.freeze + const_set const_name, -value end end } diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 8fa9680cb1..fde3381df2 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -127,26 +127,28 @@ module ActiveModel private def _define_before_model_callback(klass, callback) - klass.define_singleton_method("before_#{callback}") do |*args, &block| - set_callback(:"#{callback}", :before, *args, &block) + klass.define_singleton_method("before_#{callback}") do |*args, **options, &block| + options.assert_valid_keys(:if, :unless, :prepend) + set_callback(:"#{callback}", :before, *args, options, &block) end end def _define_around_model_callback(klass, callback) - klass.define_singleton_method("around_#{callback}") do |*args, &block| - set_callback(:"#{callback}", :around, *args, &block) + klass.define_singleton_method("around_#{callback}") do |*args, **options, &block| + options.assert_valid_keys(:if, :unless, :prepend) + set_callback(:"#{callback}", :around, *args, options, &block) end end def _define_after_model_callback(klass, callback) - klass.define_singleton_method("after_#{callback}") do |*args, &block| - options = args.extract_options! + klass.define_singleton_method("after_#{callback}") do |*args, **options, &block| + options.assert_valid_keys(:if, :unless, :prepend) options[:prepend] = true conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v| v != false } options[:if] = Array(options[:if]) << conditional - set_callback(:"#{callback}", :after, *(args << options), &block) + set_callback(:"#{callback}", :after, *args, options, &block) end end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index d2ebd18107..093052a70c 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -142,7 +142,9 @@ module ActiveModel end def changes_applied # :nodoc: - @previously_changed = changes + unless defined?(@attributes) + @previously_changed = changes + end @mutations_before_last_save = mutations_from_database @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @@ -302,7 +304,7 @@ module ActiveModel # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - previous_changes[attr] if attribute_previously_changed?(attr) + previous_changes[attr] end # Handles <tt>*_will_change!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 275e3f1313..edc30ee64d 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -62,6 +62,11 @@ module ActiveModel CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] MESSAGE_OPTIONS = [:message] + class << self + attr_accessor :i18n_full_message # :nodoc: + end + self.i18n_full_message = false + attr_reader :messages, :details # Pass in the instance of the object that is using the errors object. @@ -323,7 +328,7 @@ module ActiveModel # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) if message.is_a? Symbol - self.details[attribute].map { |e| e[:error] }.include? message + self.details[attribute.to_sym].map { |e| e[:error] }.include? message else message = message.call if message.respond_to?(:call) message = normalize_message(attribute, message, options) @@ -364,12 +369,51 @@ module ActiveModel # Returns a full message for a given attribute. # # person.errors.full_message(:name, 'is invalid') # => "Name is invalid" + # + # The `"%{attribute} %{message}"` error format can be overridden with either + # + # * <tt>activemodel.errors.models.person/contacts/addresses.attributes.street.format</tt> + # * <tt>activemodel.errors.models.person/contacts/addresses.format</tt> + # * <tt>activemodel.errors.models.person.attributes.name.format</tt> + # * <tt>activemodel.errors.models.person.format</tt> + # * <tt>errors.format</tt> def full_message(attribute, message) return message if attribute == :base + + if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope) + parts = attribute.to_s.split(".") + attribute_name = parts.pop + namespace = parts.join("/") unless parts.empty? + attributes_scope = "#{@base.class.i18n_scope}.errors.models" + + if namespace + defaults = @base.class.lookup_ancestors.map do |klass| + [ + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format", + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format", + ] + end + else + defaults = @base.class.lookup_ancestors.map do |klass| + [ + :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format", + :"#{attributes_scope}.#{klass.model_name.i18n_key}.format", + ] + end + end + + defaults.flatten! + else + defaults = [] + end + + defaults << :"errors.format" + defaults << "%{attribute} %{message}" + attr_name = attribute.to_s.tr(".", "_").humanize attr_name = @base.class.human_attribute_name(attribute, default: attr_name) - I18n.t(:"errors.format", - default: "%{attribute} %{message}", + I18n.t(defaults.shift, + default: defaults, attribute: attr_name, message: message) end diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 3c344fe854..cef5441e4a 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -7,10 +7,10 @@ module ActiveModel end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 - PRE = "beta2" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 34d9ac6c96..b7ceabb59a 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -29,7 +29,7 @@ module ActiveModel # <tt>to_key</tt> returns an Enumerable of all (primary) key attributes # of the model, and is used to a generate unique DOM id for the object. def test_to_key - assert model.respond_to?(:to_key), "The model should respond to to_key" + assert_respond_to model, :to_key def model.persisted?() false end assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false" end @@ -44,7 +44,7 @@ module ActiveModel # tests for this behavior in lint because it doesn't make sense to force # any of the possible implementation strategies on the implementer. def test_to_param - assert model.respond_to?(:to_param), "The model should respond to to_param" + assert_respond_to model, :to_param def model.to_key() [1] end def model.persisted?() false end assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" @@ -56,7 +56,7 @@ module ActiveModel # <tt>to_partial_path</tt> is used for looking up partials. For example, # a BlogPost model might return "blog_posts/blog_post". def test_to_partial_path - assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path" + assert_respond_to model, :to_partial_path assert_kind_of String, model.to_partial_path end @@ -68,7 +68,7 @@ module ActiveModel # will route to the create action. If it is persisted, a form for the # object will route to the update action. def test_persisted? - assert model.respond_to?(:persisted?), "The model should respond to persisted?" + assert_respond_to model, :persisted? assert_boolean model.persisted?, "persisted?" end @@ -79,14 +79,14 @@ module ActiveModel # # Check ActiveModel::Naming for more information. def test_model_naming - assert model.class.respond_to?(:model_name), "The model class should respond to model_name" + assert_respond_to model.class, :model_name model_name = model.class.model_name - assert model_name.respond_to?(:to_str) - assert model_name.human.respond_to?(:to_str) - assert model_name.singular.respond_to?(:to_str) - assert model_name.plural.respond_to?(:to_str) + assert_respond_to model_name, :to_str + assert_respond_to model_name.human, :to_str + assert_respond_to model_name.singular, :to_str + assert_respond_to model_name.plural, :to_str - assert model.respond_to?(:model_name), "The model instance should respond to model_name" + assert_respond_to model, :model_name assert_equal model.model_name, model.class.model_name end @@ -100,13 +100,13 @@ module ActiveModel # If localization is used, the strings should be localized for the current # locale. If no error is present, the method should return an empty array. def test_errors_aref - assert model.respond_to?(:errors), "The model should respond to errors" + assert_respond_to model, :errors assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array" end private def model - assert @model.respond_to?(:to_model), "The object should respond to to_model" + assert_respond_to @model, :to_model @model.to_model end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index dfccd03cd8..983401801f 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -111,6 +111,22 @@ module ActiveModel # BlogPost.model_name.eql?('Blog Post') # => false ## + # :method: match? + # + # :call-seq: + # match?(regexp) + # + # Equivalent to <tt>String#match?</tt>. Match the class name against the + # given regexp. Returns +true+ if there is a match, otherwise +false+. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name.match?(/Post/) # => true + # BlogPost.model_name.match?(/\d/) # => false + + ## # :method: to_s # # :call-seq: @@ -131,7 +147,7 @@ module ActiveModel # to_str() # # Equivalent to +to_s+. - delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, + delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s, :to_str, :as_json, to: :name # Returns a new ActiveModel::Name instance. By default, the +namespace+ diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb index a9cdabba00..0ed70bd473 100644 --- a/activemodel/lib/active_model/railtie.rb +++ b/activemodel/lib/active_model/railtie.rb @@ -7,8 +7,14 @@ module ActiveModel class Railtie < Rails::Railtie # :nodoc: config.eager_load_namespaces << ActiveModel + config.active_model = ActiveSupport::OrderedOptions.new + initializer "active_model.secure_password" do ActiveModel::SecurePassword.min_cost = Rails.env.test? end + + initializer "active_model.i18n_full_message" do + ActiveModel::Errors.i18n_full_message = config.active_model.delete(:i18n_full_message) || false + end end end diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 86f051f5ce..51d54f34f3 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -16,15 +16,16 @@ module ActiveModel module ClassMethods # Adds methods to set and authenticate against a BCrypt password. - # This mechanism requires you to have a +password_digest+ attribute. + # This mechanism requires you to have a +XXX_digest+ attribute. + # Where +XXX+ is the attribute name of your desired password. # # The following validations are added automatically: # * Password must be present on creation # * Password length should be less than or equal to 72 bytes - # * Confirmation of password (using a +password_confirmation+ attribute) + # * Confirmation of password (using a +XXX_confirmation+ attribute) # - # If password confirmation validation is not needed, simply leave out the - # value for +password_confirmation+ (i.e. don't provide a form field for + # If confirmation validation is not needed, simply leave out the + # value for +XXX_confirmation+ (i.e. don't provide a form field for # it). When this attribute has a +nil+ value, the validation will not be # triggered. # @@ -37,9 +38,10 @@ module ActiveModel # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # - # # Schema: User(name:string, password_digest:string) + # # Schema: User(name:string, password_digest:string, recovery_password_digest:string) # class User < ActiveRecord::Base # has_secure_password + # has_secure_password :recovery_password, validations: false # end # # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch') @@ -48,11 +50,15 @@ module ActiveModel # user.save # => false, confirmation doesn't match # user.password_confirmation = 'mUc3m00RsqyRe' # user.save # => true + # user.recovery_password = "42password" + # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC" + # user.save # => true # user.authenticate('notright') # => false # user.authenticate('mUc3m00RsqyRe') # => user + # user.authenticate_recovery_password('42password') # => user # User.find_by(name: 'david').try(:authenticate, 'notright') # => false # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user - def has_secure_password(options = {}) + def has_secure_password(attribute = :password, validations: true) # Load bcrypt gem only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. @@ -63,9 +69,40 @@ module ActiveModel raise end - include InstanceMethodsOnActivation + attr_reader attribute + + define_method("#{attribute}=") do |unencrypted_password| + if unencrypted_password.nil? + self.send("#{attribute}_digest=", nil) + elsif !unencrypted_password.empty? + instance_variable_set("@#{attribute}", unencrypted_password) + cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost + self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost)) + end + end + + define_method("#{attribute}_confirmation=") do |unencrypted_password| + instance_variable_set("@#{attribute}_confirmation", unencrypted_password) + end + + # Returns +self+ if the password is correct, otherwise +false+. + # + # class User < ActiveRecord::Base + # has_secure_password validations: false + # end + # + # user = User.new(name: 'david', password: 'mUc3m00RsqyRe') + # user.save + # user.authenticate_password('notright') # => false + # user.authenticate_password('mUc3m00RsqyRe') # => user + define_method("authenticate_#{attribute}") do |unencrypted_password| + attribute_digest = send("#{attribute}_digest") + BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self + end + + alias_method :authenticate, :authenticate_password if attribute == :password - if options.fetch(:validations, true) + if validations include ActiveModel::Validations # This ensures the model has a password by checking whether the password_digest @@ -73,57 +110,13 @@ module ActiveModel # when there is an error, the message is added to the password attribute instead # so that the error message will make sense to the end-user. validate do |record| - record.errors.add(:password, :blank) unless record.password_digest.present? + record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present? end - validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED - validates_confirmation_of :password, allow_blank: true + validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED + validates_confirmation_of attribute, allow_blank: true end end end - - module InstanceMethodsOnActivation - # Returns +self+ if the password is correct, otherwise +false+. - # - # class User < ActiveRecord::Base - # has_secure_password validations: false - # end - # - # user = User.new(name: 'david', password: 'mUc3m00RsqyRe') - # user.save - # user.authenticate('notright') # => false - # user.authenticate('mUc3m00RsqyRe') # => user - def authenticate(unencrypted_password) - BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self - end - - attr_reader :password - - # Encrypts the password into the +password_digest+ attribute, only if the - # new password is not empty. - # - # class User < ActiveRecord::Base - # has_secure_password validations: false - # end - # - # user = User.new - # user.password = nil - # user.password_digest # => nil - # user.password = 'mUc3m00RsqyRe' - # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4." - def password=(unencrypted_password) - if unencrypted_password.nil? - self.password_digest = nil - elsif !unencrypted_password.empty? - @password = unencrypted_password - cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost - self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost) - end - end - - def password_confirmation=(unencrypted_password) - @password_confirmation = unencrypted_password - end - end end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 47cb81bee5..c4b7b32291 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -179,7 +179,7 @@ module ActiveModel return unless includes = options[:include] unless includes.is_a?(Hash) - includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] + includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }] end includes.each do |association, opts| diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb index dc2eca18be..76203c5a88 100644 --- a/activemodel/lib/active_model/type/binary.rb +++ b/activemodel/lib/active_model/type/binary.rb @@ -40,7 +40,7 @@ module ActiveModel alias_method :to_str, :to_s def hex - @value.unpack("H*")[0] + @value.unpack1("H*") end def ==(other) diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb index bcdbab0343..f6c6efbc87 100644 --- a/activemodel/lib/active_model/type/boolean.rb +++ b/activemodel/lib/active_model/type/boolean.rb @@ -20,6 +20,10 @@ module ActiveModel :boolean end + def serialize(value) # :nodoc: + cast(value) + end + private def cast_value(value) diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb index 8cecc16d0f..8ec5deedc4 100644 --- a/activemodel/lib/active_model/type/date.rb +++ b/activemodel/lib/active_model/type/date.rb @@ -42,7 +42,7 @@ module ActiveModel end def new_date(year, mon, mday) - if year && year != 0 + unless year.nil? || (year == 0 && mon == 0 && mday == 0) ::Date.new(year, mon, mday) rescue nil end end diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index 250c4021c6..cb6aa67a9d 100644 --- a/activemodel/lib/active_model/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "active_support/core_ext/string/zones" require "active_support/core_ext/time/zones" module ActiveModel diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index fe396998a3..da74aaa3c5 100644 --- a/activemodel/lib/active_model/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -31,13 +31,8 @@ module ActiveModel result end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :range - private + attr_reader :range def cast_value(value) case value diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb index 7272d7b0c5..a19dc0f011 100644 --- a/activemodel/lib/active_model/type/registry.rb +++ b/activemodel/lib/active_model/type/registry.rb @@ -23,13 +23,8 @@ module ActiveModel end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :registrations - private + attr_reader :registrations def registration_klass Registration @@ -59,10 +54,7 @@ module ActiveModel type_name == name end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :name, :block end end diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb index ad7ba0351a..b3056b1333 100644 --- a/activemodel/lib/active_model/type/time.rb +++ b/activemodel/lib/active_model/type/time.rb @@ -18,6 +18,8 @@ module ActiveModel case value when ::String value = "2000-01-01 #{value}" + time_hash = ::Date._parse(value) + return if time_hash[:hour].nil? when ::Time value = value.change(year: 2000, day: 1, month: 1) end @@ -28,14 +30,10 @@ module ActiveModel private def cast_value(value) - return value unless value.is_a?(::String) + return apply_seconds_precision(value) unless value.is_a?(::String) return if value.empty? - if value.start_with?("2000-01-01") - dummy_time_value = value - else - dummy_time_value = "2000-01-01 #{value}" - end + dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ") fast_string_to_time(dummy_time_value) || begin time_hash = ::Date._parse(dummy_time_value) diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb index a8ea6a2c22..b6914dd63c 100644 --- a/activemodel/lib/active_model/type/value.rb +++ b/activemodel/lib/active_model/type/value.rb @@ -90,6 +90,10 @@ module ActiveModel false end + def force_equality?(_value) # :nodoc: + false + end + def map(value) # :nodoc: yield value end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index f35e4dec7f..ea3a6b52ab 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -58,13 +58,8 @@ module ActiveModel klass.send(:attr_writer, *attr_writers) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :attributes - private + attr_reader :attributes def convert_to_reader_name(method_name) method_name.to_s.chomp("=") diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb index 0b9b5ce6a1..bafb8e2106 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -32,7 +32,7 @@ module ActiveModel @delimiter ||= options[:in] || options[:within] end - # In Ruby 2.2 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all + # After Ruby 2.2, <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all # possible values in the range for equality, which is slower but more accurate. # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range # endpoints, which is fast but is only accurate on Numeric, Time, Date, diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 3104e7e329..9c12dc14c5 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -19,7 +19,7 @@ module ActiveModel # particular enumerable object. # # class Person < ActiveRecord::Base - # validates_inclusion_of :gender, in: %w( m f ) + # validates_inclusion_of :role, in: %w( admin contributor ) # validates_inclusion_of :age, in: 0..99 # validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list" # validates_inclusion_of :states, in: ->(person) { STATES[person.country] } diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 31750ba78e..0478915be7 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -19,9 +19,11 @@ module ActiveModel end def validate_each(record, attr_name, value) - before_type_cast = :"#{attr_name}_before_type_cast" + came_from_user = :"#{attr_name}_came_from_user?" - raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) && record.send(before_type_cast) != value + if record.respond_to?(came_from_user) && record.public_send(came_from_user) + raw_value = record.read_attribute_before_type_cast(attr_name) + end raw_value ||= value if record_attribute_changed_in_place?(record, attr_name) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index e28e7e9219..88cca318ef 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -63,7 +63,7 @@ module ActiveModel # and strings in shortcut form. # # validates :email, format: /@/ - # validates :gender, inclusion: %w(male female) + # validates :role, inclusion: %(admin contributor) # validates :password, length: 6..20 # # When using shortcut form, ranges and arrays are passed to your |