From 324b2a8893735925738b92f9c5b773688c9ef6c2 Mon Sep 17 00:00:00 2001 From: Yaroslav Markin Date: Wed, 31 Dec 2008 18:19:20 +0300 Subject: [PATCH] Introduce automatic pluralized models generation for ActiveRecord. Now, if you have Post < ActiveRecord::Base, you can do things like Posts.find(:all), Posts.first, Posts.find_by_title or Posts.scope_by_author. Syntax sugar! --- activerecord/lib/active_record.rb | 1 + activerecord/lib/active_record/base.rb | 4 + .../lib/active_record/pluralized_models.rb | 80 ++++++++++++++++++++ activerecord/test/cases/pluralized_models_test.rb | 40 ++++++++++ activesupport/lib/active_support/dependencies.rb | 3 +- .../test/autoloading_fixtures/class_folder.rb | 4 + activesupport/test/dependencies_test.rb | 9 ++- 7 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 activerecord/lib/active_record/pluralized_models.rb create mode 100644 activerecord/test/cases/pluralized_models_test.rb diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 390c005..a31ee36 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -57,6 +57,7 @@ module ActiveRecord autoload :NamedScope, 'active_record/named_scope' autoload :Observing, 'active_record/observer' autoload :QueryCache, 'active_record/query_cache' + autoload :PluralizedModels, 'active_record/pluralized_models' autoload :Reflection, 'active_record/reflection' autoload :Schema, 'active_record/schema' autoload :SchemaDumper, 'active_record/schema_dumper' diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cca012e..057a6f1 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -396,8 +396,11 @@ module ActiveRecord #:nodoc: cattr_accessor :logger, :instance_writer => false def self.inherited(child) #:nodoc: + create_pluralized_model_for(child) if can_create_pluralized_model_for?(child) + @@subclasses[self] ||= [] @@subclasses[self] << child + super end @@ -3092,6 +3095,7 @@ module ActiveRecord #:nodoc: include Callbacks, Observing, Timestamp include Associations, AssociationPreload, NamedScope include Aggregations, Transactions, Reflection, Calculations, Serialization + include PluralizedModels end end diff --git a/activerecord/lib/active_record/pluralized_models.rb b/activerecord/lib/active_record/pluralized_models.rb new file mode 100644 index 0000000..2128272 --- /dev/null +++ b/activerecord/lib/active_record/pluralized_models.rb @@ -0,0 +1,80 @@ +module ActiveRecord + # Pluralized models generation for ActiveRecord::Base descendants. + # + # Example: + # + # class Post < ActiveRecord::Base + # end + # + # will create a Posts class that will proxy all classmethods to Post, so that + # + # Posts.all, Posts.first, Posts.find(...), Posts.scoped_by_date(...) + # + # methods will work. + # Namespaced models are supported as well. + # + # Pluralized models are not destructive to classes or models that already exist, so, + # for example, if you do already have a Posts class in your application, it will not be + # touched at all. + module PluralizedModels + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + # Can we create a pluralized model for class +klass+? + # + # This method is here to avoid "creating" anything for anonymous classes. We identify + # anonymous classes by comparing first character of their to_s representation with "#" + def can_create_pluralized_model_for?(klass) + !klass.to_s.match(/^\#/) + end + + # Create pluralized model for class +klass+. + # + # Does not do anything for classes or modules that already exist. + # + # Pluralized model initializer points to singular model initializer, + # respond_to? and methods methods take singular model's methods + # into account. Every other class method is proxied to singular model + # using method_missing. + def create_pluralized_model_for(klass) + pluralized_class_name = klass.to_s.pluralize + + if pluralized_class = Object.module_eval(%( + defined?(#{pluralized_class_name}) ? false : #{pluralized_class_name} = Class.new + ), __FILE__, __LINE__) + pluralized_class.class_eval %( + class << self # class << self + def new(*args) # def new(*args) + #{klass}.new(*args) # Posts.new(*args) + end # end + # + def respond_to?(method_id) # def respond_to?(method_id) + #{klass}.respond_to?(method_id) # Posts.respond_to?(method_id) + end # end + # + def methods # def methods + #{klass}.methods # Posts.methods + end # end + # + def method_missing(method_id, *arguments, &block) # def method_missing(method_id, *arguments, &block) + if #{klass}.respond_to?(method_id) # if Posts.respond_to?(method_id) + #{klass}.send(method_id, *arguments, &block) # Posts.send(method_id, *arguments, &block) + else # else + super # super + end # end + end # end + end # end + ) + end + end + end + end +end diff --git a/activerecord/test/cases/pluralized_models_test.rb b/activerecord/test/cases/pluralized_models_test.rb new file mode 100644 index 0000000..12d0c65 --- /dev/null +++ b/activerecord/test/cases/pluralized_models_test.rb @@ -0,0 +1,40 @@ +require "cases/helper" + +class PluralizedModelsTest < ActiveRecord::TestCase + class Developers + def self.do_the_monkey + (%w(DEVELOPERS) * 4).join(" ") + end + end + + def test_creating_pluralized_models + assert defined?(Posts) + assert defined?(StiPosts) + assert defined?(MixedCaseMonkeys) + assert defined?(MyApplication::Business::Firms) + assert defined?(MyApplication::Business::Projects) + end + + def test_not_creating_pluralized_models_if_already_exist + assert Developers.respond_to?(:do_the_monkey) + assert !Developers.respond_to?(:find) + end + + def test_pluralized_model_initializes_singular_class + assert_equal Posts.new.class, Post + end + + def test_pluralized_model_responds_to_singular_class_methods + assert Posts.respond_to?(:what_are_you) + assert Posts.respond_to?(:find) + end + + def test_pluralized_model_returns_methods_list_of_a_singular_class + assert_equal Posts.methods, Post.methods + end + + def test_pluralized_model_calls_singular_class_methods + assert_equal Posts.find(:all), Post.find(:all) + assert_equal Posts.first, Post.first + end +end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 7ce9ade..b7365d8 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -415,9 +415,10 @@ module ActiveSupport #:nodoc: qualified_name = qualified_name_for from_mod, const_name path_suffix = qualified_name.underscore + path_suffix_singular = qualified_name.singularize.underscore name_error = NameError.new("uninitialized constant #{qualified_name}") - file_path = search_for_file(path_suffix) + file_path = search_for_file(path_suffix) || search_for_file(path_suffix_singular) if file_path && ! loaded.include?(File.expand_path(file_path)) # We found a matching file to load require_or_load file_path raise LoadError, "Expected #{file_path} to define #{qualified_name}" unless uninherited_const_defined?(from_mod, const_name) diff --git a/activesupport/test/autoloading_fixtures/class_folder.rb b/activesupport/test/autoloading_fixtures/class_folder.rb index ad2b27b..d1665d6 100644 --- a/activesupport/test/autoloading_fixtures/class_folder.rb +++ b/activesupport/test/autoloading_fixtures/class_folder.rb @@ -1,3 +1,7 @@ class ClassFolder ConstantInClassFolder = 'indeed' end + +class ClassFolders + ConstantInClassFolder = 'indeed' +end \ No newline at end of file diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index fe04b91..e6a391b 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -177,7 +177,7 @@ class DependenciesTest < Test::Unit::TestCase Object.__send__ :remove_const, :ClassFolder end end - + def test_class_with_nested_inline_class with_loading 'autoloading_fixtures' do assert_kind_of Class, ClassFolder::InlineClass @@ -193,6 +193,13 @@ class DependenciesTest < Test::Unit::TestCase Object.__send__ :remove_const, :ClassFolder end end + + def test_class_with_pluralized_name + with_loading 'autoloading_fixtures' do + assert_kind_of Class, ClassFolders + Object.__send__ :remove_const, :ClassFolders + end + end def test_nested_class_can_access_sibling with_loading 'autoloading_fixtures' do -- 1.6.0.4