aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
authorJon Leighton <j@jonathanleighton.com>2011-12-28 15:38:16 +0000
committerJon Leighton <j@jonathanleighton.com>2011-12-28 18:27:41 +0000
commit93c1f11c0a5097a5431819a1551a02a869a16a38 (patch)
treefd88de442e03a600df63387dc4b2389e9c861c7b /activemodel
parentafe6e059ea216f01d160e4603116356b78df12e5 (diff)
downloadrails-93c1f11c0a5097a5431819a1551a02a869a16a38.tar.gz
rails-93c1f11c0a5097a5431819a1551a02a869a16a38.tar.bz2
rails-93c1f11c0a5097a5431819a1551a02a869a16a38.zip
Support configuration on ActiveRecord::Model.
The problem: We need to be able to specify configuration in a way that can be inherited to models that include ActiveRecord::Model. So it is no longer sufficient to put 'top level' config on ActiveRecord::Base, but we do want configuration specified on ActiveRecord::Base and descendants to continue to work. So we need something like class_attribute that can be defined on a module but that is inherited when ActiveRecord::Model is included. The solution: added ActiveModel::Configuration module which provides a config_attribute macro. It's a bit specific hence I am not putting this in Active Support or making it a 'public API' at present.
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb3
-rw-r--r--activemodel/lib/active_model/configuration.rb134
-rw-r--r--activemodel/lib/active_model/mass_assignment_security.rb22
-rw-r--r--activemodel/lib/active_model/serializers/json.rb3
-rw-r--r--activemodel/lib/active_model/validations.rb5
-rw-r--r--activemodel/test/cases/configuration_test.rb154
7 files changed, 308 insertions, 14 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index d0e2a6f39c..9fc308375f 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -32,6 +32,7 @@ module ActiveModel
autoload :AttributeMethods
autoload :BlockValidator, 'active_model/validator'
autoload :Callbacks
+ autoload :Configuration
autoload :Conversion
autoload :Dirty
autoload :EachValidator, 'active_model/validator'
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index d7ec7e6ca5..71ab1501c8 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -61,7 +61,8 @@ module ActiveModel
CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
included do
- class_attribute :attribute_method_matchers, :instance_writer => false
+ extend ActiveModel::Configuration
+ config_attribute :attribute_method_matchers
self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new]
end
diff --git a/activemodel/lib/active_model/configuration.rb b/activemodel/lib/active_model/configuration.rb
new file mode 100644
index 0000000000..1757c12ebf
--- /dev/null
+++ b/activemodel/lib/active_model/configuration.rb
@@ -0,0 +1,134 @@
+require 'active_support/concern'
+require 'active_support/core_ext/class/attribute'
+require 'active_support/core_ext/class/attribute_accessors'
+
+module ActiveModel
+ # This API is for Rails' internal use and is not currently considered 'public', so
+ # it may change in the future without warning.
+ #
+ # It creates configuration attributes that can be inherited from a module down
+ # to a class that includes the module. E.g.
+ #
+ # module MyModel
+ # extend ActiveModel::Configuration
+ # config_attribute :awesome
+ # self.awesome = true
+ # end
+ #
+ # class Post
+ # include MyModel
+ # end
+ #
+ # Post.awesome # => true
+ #
+ # Post.awesome = false
+ # Post.awesome # => false
+ # MyModel.awesome # => true
+ #
+ # We assume that the module will have a ClassMethods submodule containing methods
+ # to be transferred to the including class' singleton class.
+ #
+ # Config options can also be defined directly on a class:
+ #
+ # class Post
+ # extend ActiveModel::Configuration
+ # config_attribute :awesome
+ # end
+ #
+ # So this allows us to define a module that doesn't care about whether it is being
+ # included in a class or a module:
+ #
+ # module Awesomeness
+ # extend ActiveSupport::Concern
+ #
+ # included do
+ # extend ActiveModel::Configuration
+ # config_attribute :awesome
+ # self.awesome = true
+ # end
+ # end
+ #
+ # class Post
+ # include Awesomeness
+ # end
+ #
+ # module AwesomeModel
+ # include Awesomeness
+ # end
+ module Configuration #:nodoc:
+ def config_attribute(name, options = {})
+ klass = self.is_a?(Class) ? ClassAttribute : ModuleAttribute
+ klass.new(self, name, options).define
+ end
+
+ class Attribute
+ attr_reader :host, :name, :options
+
+ def initialize(host, name, options)
+ @host, @name, @options = host, name, options
+ end
+
+ def instance_writer?
+ options.fetch(:instance_writer, false)
+ end
+ end
+
+ class ClassAttribute < Attribute
+ def define
+ if options[:global]
+ host.cattr_accessor name, :instance_writer => instance_writer?
+ else
+ host.class_attribute name, :instance_writer => instance_writer?
+ end
+ end
+ end
+
+ class ModuleAttribute < Attribute
+ def class_methods
+ @class_methods ||= begin
+ if host.const_defined?(:ClassMethods, false)
+ host.const_get(:ClassMethods)
+ else
+ host.const_set(:ClassMethods, Module.new)
+ end
+ end
+ end
+
+ def define
+ host.singleton_class.class_eval <<-CODE, __FILE__, __LINE__
+ attr_accessor :#{name}
+ def #{name}?; !!#{name}; end
+ CODE
+
+ name, host = self.name, self.host
+
+ class_methods.class_eval do
+ define_method(name) { host.send(name) }
+ define_method("#{name}?") { !!send(name) }
+ end
+
+ host.class_eval <<-CODE
+ def #{name}; defined?(@#{name}) ? @#{name} : self.class.#{name}; end
+ def #{name}?; !!#{name}; end
+ CODE
+
+ if options[:global]
+ class_methods.class_eval do
+ define_method("#{name}=") { |val| host.send("#{name}=", val) }
+ end
+ else
+ class_methods.class_eval <<-CODE, __FILE__, __LINE__
+ def #{name}=(val)
+ singleton_class.class_eval do
+ remove_possible_method(:#{name})
+ define_method(:#{name}) { val }
+ end
+ end
+ CODE
+ end
+
+ host.send(:attr_writer, name) if instance_writer?
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb
index c895968f77..9b12d9d281 100644
--- a/activemodel/lib/active_model/mass_assignment_security.rb
+++ b/activemodel/lib/active_model/mass_assignment_security.rb
@@ -10,11 +10,13 @@ module ActiveModel
extend ActiveSupport::Concern
included do
- class_attribute :_accessible_attributes
- class_attribute :_protected_attributes
- class_attribute :_active_authorizer
+ extend ActiveModel::Configuration
- class_attribute :_mass_assignment_sanitizer
+ config_attribute :_accessible_attributes
+ config_attribute :_protected_attributes
+ config_attribute :_active_authorizer
+
+ config_attribute :_mass_assignment_sanitizer
self.mass_assignment_sanitizer = :logger
end
@@ -56,7 +58,7 @@ module ActiveModel
# You can specify your own sanitizer object eg. MySanitizer.new.
# See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for example implementation.
#
- #
+ #
module ClassMethods
# Attributes named in this macro are protected from mass-assignment
# whenever attributes are sanitized before assignment. A role for the
@@ -70,13 +72,13 @@ module ActiveModel
#
# class Customer
# include ActiveModel::MassAssignmentSecurity
- #
+ #
# attr_accessor :name, :email, :logins_count
- #
+ #
# attr_protected :logins_count
# # Suppose that admin can not change email for customer
- # attr_protected :logins_count, :email, :as => :admin
- #
+ # attr_protected :logins_count, :email, :as => :admin
+ #
# def assign_attributes(values, options = {})
# sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
# send("#{k}=", v)
@@ -99,7 +101,7 @@ module ActiveModel
# customer.name # => "David"
# customer.email # => nil
# customer.logins_count # => nil
- #
+ #
# customer.email = "c@d.com"
# customer.email # => "c@d.com"
#
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index c845440120..63ab8e7edc 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -10,8 +10,9 @@ module ActiveModel
included do
extend ActiveModel::Naming
+ extend ActiveModel::Configuration
- class_attribute :include_root_in_json
+ config_attribute :include_root_in_json
self.include_root_in_json = true
end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 8ed392abca..1779efd526 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -42,9 +42,9 @@ module ActiveModel
#
module Validations
extend ActiveSupport::Concern
- include ActiveSupport::Callbacks
included do
+ extend ActiveModel::Callbacks
extend ActiveModel::Translation
extend HelperMethods
@@ -53,7 +53,8 @@ module ActiveModel
attr_accessor :validation_context
define_callbacks :validate, :scope => :name
- class_attribute :_validators
+ extend ActiveModel::Configuration
+ config_attribute :_validators
self._validators = Hash.new { |h,k| h[k] = [] }
end
diff --git a/activemodel/test/cases/configuration_test.rb b/activemodel/test/cases/configuration_test.rb
new file mode 100644
index 0000000000..a172fa26a3
--- /dev/null
+++ b/activemodel/test/cases/configuration_test.rb
@@ -0,0 +1,154 @@
+require 'cases/helper'
+
+class ConfigurationOnModuleTest < ActiveModel::TestCase
+ def setup
+ @mod = mod = Module.new do
+ extend ActiveSupport::Concern
+ extend ActiveModel::Configuration
+
+ config_attribute :omg
+ self.omg = "default"
+
+ config_attribute :wtf, global: true
+ self.wtf = "default"
+
+ config_attribute :boolean
+
+ config_attribute :lol, instance_writer: true
+ end
+
+ @klass = Class.new do
+ include mod
+ end
+
+ @subklass = Class.new(@klass)
+ end
+
+ test "default" do
+ assert_equal "default", @mod.omg
+ assert_equal "default", @klass.omg
+ assert_equal "default", @klass.new.omg
+ end
+
+ test "setting" do
+ @mod.omg = "lol"
+ assert_equal "lol", @mod.omg
+ end
+
+ test "setting on class including the module" do
+ @klass.omg = "lol"
+ assert_equal "lol", @klass.omg
+ assert_equal "lol", @klass.new.omg
+ assert_equal "default", @mod.omg
+ end
+
+ test "setting on subclass of class including the module" do
+ @subklass.omg = "lol"
+ assert_equal "lol", @subklass.omg
+ assert_equal "default", @klass.omg
+ assert_equal "default", @mod.omg
+ end
+
+ test "setting on instance" do
+ assert !@klass.new.respond_to?(:omg=)
+
+ @klass.lol = "lol"
+ obj = @klass.new
+ assert_equal "lol", obj.lol
+ obj.lol = "omg"
+ assert_equal "omg", obj.lol
+ assert_equal "lol", @klass.lol
+ assert_equal "lol", @klass.new.lol
+ obj.lol = false
+ assert !obj.lol?
+ end
+
+ test "global attribute" do
+ assert_equal "default", @mod.wtf
+ assert_equal "default", @klass.wtf
+
+ @mod.wtf = "wtf"
+
+ assert_equal "wtf", @mod.wtf
+ assert_equal "wtf", @klass.wtf
+
+ @klass.wtf = "lol"
+
+ assert_equal "lol", @mod.wtf
+ assert_equal "lol", @klass.wtf
+ end
+
+ test "boolean" do
+ assert_equal false, @mod.boolean?
+ assert_equal false, @klass.new.boolean?
+ @mod.boolean = true
+ assert_equal true, @mod.boolean?
+ assert_equal true, @klass.new.boolean?
+ end
+end
+
+class ConfigurationOnClassTest < ActiveModel::TestCase
+ def setup
+ @klass = Class.new do
+ extend ActiveModel::Configuration
+
+ config_attribute :omg
+ self.omg = "default"
+
+ config_attribute :wtf, global: true
+ self.wtf = "default"
+
+ config_attribute :omg2, instance_writer: true
+ config_attribute :wtf2, instance_writer: true, global: true
+ end
+
+ @subklass = Class.new(@klass)
+ end
+
+ test "defaults" do
+ assert_equal "default", @klass.omg
+ assert_equal "default", @klass.wtf
+ assert_equal "default", @subklass.omg
+ assert_equal "default", @subklass.wtf
+ end
+
+ test "changing" do
+ @klass.omg = "lol"
+ assert_equal "lol", @klass.omg
+ assert_equal "lol", @subklass.omg
+ end
+
+ test "changing in subclass" do
+ @subklass.omg = "lol"
+ assert_equal "lol", @subklass.omg
+ assert_equal "default", @klass.omg
+ end
+
+ test "changing global" do
+ @klass.wtf = "wtf"
+ assert_equal "wtf", @klass.wtf
+ assert_equal "wtf", @subklass.wtf
+
+ @subklass.wtf = "lol"
+ assert_equal "lol", @klass.wtf
+ assert_equal "lol", @subklass.wtf
+ end
+
+ test "instance_writer" do
+ obj = @klass.new
+
+ @klass.omg2 = "omg"
+ @klass.wtf2 = "wtf"
+
+ assert_equal "omg", obj.omg2
+ assert_equal "wtf", obj.wtf2
+
+ obj.omg2 = "lol"
+ obj.wtf2 = "lol"
+
+ assert_equal "lol", obj.omg2
+ assert_equal "lol", obj.wtf2
+ assert_equal "omg", @klass.omg2
+ assert_equal "lol", @klass.wtf2
+ end
+end