Skip to content

Commit

Permalink
Split out most of the AR::Base code into separate modules 🍰
Browse files Browse the repository at this point in the history
  • Loading branch information
jonleighton committed Dec 15, 2011
1 parent 8854bf2 commit ceb33f8
Show file tree
Hide file tree
Showing 19 changed files with 1,811 additions and 1,688 deletions.
17 changes: 14 additions & 3 deletions activerecord/lib/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module ActiveRecord
autoload :Aggregations
autoload :Associations
autoload :AttributeMethods
autoload :AttributeAssignment
autoload :AutosaveAssociation

autoload :Relation
Expand All @@ -57,27 +58,37 @@ module ActiveRecord
autoload :Base
autoload :Callbacks
autoload :CounterCache
autoload :DefaultScope
autoload :DynamicMatchers
autoload :DynamicFinderMatch
autoload :DynamicScopeMatch
autoload :Explain
autoload :IdentityMap
autoload :Inheritance
autoload :Integration
autoload :Migration
autoload :Migrator, 'active_record/migration'
autoload :ModelSchema
autoload :NamedScope
autoload :NestedAttributes
autoload :Observer
autoload :Persistence
autoload :QueryCache
autoload :Querying
autoload :ReadonlyAttributes
autoload :Reflection
autoload :Result
autoload :Sanitization
autoload :Schema
autoload :SchemaDumper
autoload :Scoping
autoload :Serialization
autoload :Store
autoload :SessionStore
autoload :Store
autoload :Timestamp
autoload :Transactions
autoload :Translation
autoload :Validations
autoload :IdentityMap
autoload :Explain
end

module Coders
Expand Down
221 changes: 221 additions & 0 deletions activerecord/lib/active_record/attribute_assignment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
require 'active_support/concern'

module ActiveRecord
module AttributeAssignment
extend ActiveSupport::Concern
include ActiveModel::MassAssignmentSecurity

module ClassMethods
private

# The primary key and inheritance column can never be set by mass-assignment for security reasons.
def attributes_protected_by_default
default = [ primary_key, inheritance_column ]
default << 'id' unless primary_key.eql? 'id'
default
end
end

# Allows you to set all the attributes at once by passing in a hash with keys
# matching the attribute names (which again matches the column names).
#
# If any attributes are protected by either +attr_protected+ or
# +attr_accessible+ then only settable attributes will be assigned.
#
# class User < ActiveRecord::Base
# attr_protected :is_admin
# end
#
# user = User.new
# user.attributes = { :username => 'Phusion', :is_admin => true }
# user.username # => "Phusion"
# user.is_admin? # => false
def attributes=(new_attributes)
return unless new_attributes.is_a?(Hash)

assign_attributes(new_attributes)
end

# Allows you to set all the attributes for a particular mass-assignment
# security role by passing in a hash of attributes with keys matching
# the attribute names (which again matches the column names) and the role
# name using the :as option.
#
# To bypass mass-assignment security you can use the :without_protection => true
# option.
#
# class User < ActiveRecord::Base
# attr_accessible :name
# attr_accessible :name, :is_admin, :as => :admin
# end
#
# user = User.new
# user.assign_attributes({ :name => 'Josh', :is_admin => true })
# user.name # => "Josh"
# user.is_admin? # => false
#
# user = User.new
# user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
# user.name # => "Josh"
# user.is_admin? # => true
#
# user = User.new
# user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
# user.name # => "Josh"
# user.is_admin? # => true
def assign_attributes(new_attributes, options = {})
return unless new_attributes

attributes = new_attributes.stringify_keys
multi_parameter_attributes = []
nested_parameter_attributes = []
@mass_assignment_options = options

unless options[:without_protection]
attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
end

attributes.each do |k, v|
if k.include?("(")
multi_parameter_attributes << [ k, v ]
elsif respond_to?("#{k}=")
if v.is_a?(Hash)
nested_parameter_attributes << [ k, v ]
else
send("#{k}=", v)
end
else
raise(UnknownAttributeError, "unknown attribute: #{k}")
end
end

# assign any deferred nested attributes after the base attributes have been set
nested_parameter_attributes.each do |k,v|
send("#{k}=", v)
end

@mass_assignment_options = nil
assign_multiparameter_attributes(multi_parameter_attributes)
end

protected

def mass_assignment_options
@mass_assignment_options ||= {}
end

def mass_assignment_role
mass_assignment_options[:as] || :default
end

private

# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
# f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
# attribute will be set to nil.
def assign_multiparameter_attributes(pairs)
execute_callstack_for_multiparameter_attributes(
extract_callstack_for_multiparameter_attributes(pairs)
)
end

def instantiate_time_object(name, values)
if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
Time.zone.local(*values)
else
Time.time_with_datetime_fallback(self.class.default_timezone, *values)
end
end

def execute_callstack_for_multiparameter_attributes(callstack)
errors = []
callstack.each do |name, values_with_empty_parameters|
begin
send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
rescue => ex
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
end
end
unless errors.empty?
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
end
end

def read_value_from_parameter(name, values_hash_from_param)
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
if values_hash_from_param.values.all?{|v|v.nil?}
nil
elsif klass == Time
read_time_parameter_value(name, values_hash_from_param)
elsif klass == Date
read_date_parameter_value(name, values_hash_from_param)
else
read_other_parameter_value(klass, name, values_hash_from_param)
end
end

def read_time_parameter_value(name, values_hash_from_param)
# If Date bits were not provided, error
raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
# If Date bits were provided but blank, then return nil
return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}

set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
# If Time bits are not there, then default to 0
(3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
instantiate_time_object(name, set_values)
end

def read_date_parameter_value(name, values_hash_from_param)
return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
begin
Date.new(*set_values)
rescue ArgumentError # if Date.new raises an exception on an invalid date
instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
end
end

def read_other_parameter_value(klass, name, values_hash_from_param)
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
values = (1..max_position).collect do |position|
raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
values_hash_from_param[position]
end
klass.new(*values)
end

def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
[values_hash_from_param.keys.max,upper_cap].min
end

def extract_callstack_for_multiparameter_attributes(pairs)
attributes = { }

pairs.each do |pair|
multiparameter_name, value = pair
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] = {} unless attributes.include?(attribute_name)

parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
end

attributes
end

def type_cast_attribute_value(multiparameter_name, value)
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
end

def find_parameter_position(multiparameter_name)
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
end

end
end
Loading

5 comments on commit ceb33f8

@dmitry
Copy link
Contributor

@dmitry dmitry commented on ceb33f8 Dec 16, 2011

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. At last!

@sobrinho
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last file removes 2 tests.

This is right?

@jonleighton
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That test tests a method that is not called from anywhere, so I removed the method also.

@sobrinho
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! :)

@tiegz
Copy link
Contributor

@tiegz tiegz commented on ceb33f8 Apr 11, 2012

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if anyone else thinks to_param should be removed from AR now. I just realized today that to_param in Rails 3.2.3 resolves to ActiveModel::Conversion#to_param instead of ActiveRecord::Base#to_param. It doesn't looked to be tested in ActiveRecord currently either.

Please sign in to comment.