Permalink
Browse files

Split SexyScopes::ActiveRecord into 2 modules: ClassMethods and Dynam…

…icMethods, rescue from ActiveRecord::StatementInvalid in DynamicMethods
  • Loading branch information...
1 parent f7332b0 commit 724244783eda2cdd22888c84367cd93c935b2dfd @samleb committed Jan 9, 2013
@@ -1,106 +1,6 @@
-require 'delegate'
require 'active_record'
-require 'sexy_scopes/wrappers'
-require 'sexy_scopes/arel'
+require 'sexy_scopes/active_record/class_methods'
+require 'sexy_scopes/active_record/dynamic_methods'
-module SexyScopes
- module ActiveRecord
- include Wrappers
-
- # Creates and extends an Arel <tt>Attribute</tt> representing the table's column with
- # the given <tt>name</tt>.
- #
- # @param [String, Symbol] name The attribute name
- #
- # @note Please note that no exception is raised if no such column actually exists.
- #
- # @example
- # User.where(User.attribute(:score) > 1000)
- # # => SELECT "users".* FROM "users" WHERE ("users"."score" > 1000)
- #
- def attribute(name)
- attribute = arel_table[name]
- extend_expression(attribute)
- end
-
- # Creates and extends an Arel <tt>SqlLiteral</tt> instance for the given <tt>expression</tt>,
- # first converted to a string using <tt>to_s</tt>.
- #
- # @param [String, #to_s] expression Any SQL expression.
- #
- # @example
- # def Circle.with_perimeter_smaller_than(perimeter)
- # where sql(2 * Math::PI) * radius < perimeter
- # end
- #
- # Circle.with_perimeter_smaller_than(20)
- # # => SELECT "circles".* FROM "circles" WHERE (6.283185307179586 * "circles"."radius" < 20)
- #
- def sql_literal(expression)
- ::Arel.sql(expression.to_s).tap do |literal|
- extend_expression(literal)
- extend_predicate(literal)
- end
- end
- alias_method :sql, :sql_literal
-
- # @!visibility private
- def respond_to?(method_name, include_private = false) # :nodoc:
- super || respond_to_missing?(method_name, include_private)
- end
-
- # # @!visibility private
- def respond_to_missing?(method_name, include_private = false) # :nodoc:
- Object.respond_to?(:respond_to_missing?) && super || attribute_names.include?(method_name.to_s)
- end
-
- private
- # Equivalent to calling {#attribute} with the missing method's <tt>name</tt> if the table
- # has a column with that name.
- #
- # Delegates to superclass implementation otherwise, eventually raising <tt>NoMethodError</tt>.
- #
- # @see #attribute
- #
- # @note Due to the way this works, be careful not to use this syntactic sugar with existing
- # <tt>ActiveRecord::Base</tt> methods (see last example).
- #
- # @raise [NoMethodError] if the table has no corresponding column
- #
- # @example
- # # Suppose the "users" table has an "email" column, then these are equivalent:
- # User.email
- # User.attribute(:email)
- #
- # @example
- # # Here is the previous example (from `attribute`) rewritten:
- # User.where(User.score > 1000)
- # # => SELECT "users".* FROM "users" WHERE ("users"."score" > 1000)
- #
- # @example
- # # Don't use it with existing `ActiveRecord::Base` methods, i.e. `name`:
- # User.name # => "User"
- # # In these cases you'll have to use `attribute` explicitely
- # User.attribute(:name)
- #
- def method_missing(name, *args)
- if column_names.include?(name.to_s)
- define_sexy_scopes_attribute_method(name)
- attribute(name)
- else
- super
- end
- end
-
- def define_sexy_scopes_attribute_method(name)
- class_eval <<-EVAL, __FILE__, __LINE__ + 1
- def self.#{name} # def self.username
- attribute(:#{name}) # attribute(:username)
- end # end
- EVAL
- end
- end
-
- # Add these methods to Active Record
- ::ActiveRecord::AttributeMethods::ClassMethods.extend SexyScopes::ActiveRecord
-end
+ActiveRecord::Base.extend SexyScopes::ActiveRecord::ClassMethods
+ActiveRecord::Base.extend SexyScopes::ActiveRecord::DynamicMethods
@@ -0,0 +1,47 @@
+require 'sexy_scopes/arel'
+require 'sexy_scopes/wrappers'
+
+module SexyScopes
+ module ActiveRecord
+ module ClassMethods
+ include Wrappers
+
+ # Creates and extends an Arel <tt>Attribute</tt> representing the table's column with
+ # the given <tt>name</tt>.
+ #
+ # @param [String, Symbol] name The attribute name
+ #
+ # @note Please note that no exception is raised if no such column actually exists.
+ #
+ # @example
+ # User.where(User.attribute(:score) > 1000)
+ # # => SELECT "users".* FROM "users" WHERE ("users"."score" > 1000)
+ #
+ def attribute(name)
+ attribute = arel_table[name]
+ extend_expression(attribute)
+ end
+
+ # Creates and extends an Arel <tt>SqlLiteral</tt> instance for the given <tt>expression</tt>,
+ # first converted to a string using <tt>to_s</tt>.
+ #
+ # @param [String, #to_s] expression Any SQL expression.
+ #
+ # @example
+ # def Circle.with_perimeter_smaller_than(perimeter)
+ # where sql(2 * Math::PI) * radius < perimeter
+ # end
+ #
+ # Circle.with_perimeter_smaller_than(20)
+ # # => SELECT "circles".* FROM "circles" WHERE (6.283185307179586 * "circles"."radius" < 20)
+ #
+ def sql_literal(expression)
+ ::Arel.sql(expression.to_s).tap do |literal|
+ extend_expression(literal)
+ extend_predicate(literal)
+ end
+ end
+ alias_method :sql, :sql_literal
+ end
+ end
+end
@@ -0,0 +1,69 @@
+module SexyScopes
+ module ActiveRecord
+ module DynamicMethods
+ # @!visibility private
+ def respond_to?(method_name, include_private = false) # :nodoc:
+ super || respond_to_missing?(method_name, include_private)
+ end
+
+ # # @!visibility private
+ def respond_to_missing?(method_name, include_private = false) # :nodoc:
+ Object.respond_to?(:respond_to_missing?) && super || sexy_scopes_has_attribute?(method_name)
+ end
+
+ private
+ # Equivalent to calling {#attribute} with the missing method's <tt>name</tt> if the table
+ # has a column with that name.
+ #
+ # Delegates to superclass implementation otherwise, eventually raising <tt>NoMethodError</tt>.
+ #
+ # @see #attribute
+ #
+ # @note Due to the way this works, be careful not to use this syntactic sugar with existing
+ # <tt>ActiveRecord::Base</tt> methods (see last example).
+ #
+ # @raise [NoMethodError] if the table has no corresponding column
+ #
+ # @example
+ # # Suppose the "users" table has an "email" column, then these are equivalent:
+ # User.email
+ # User.attribute(:email)
+ #
+ # @example
+ # # Here is the previous example (from `attribute`) rewritten:
+ # User.where(User.score > 1000)
+ # # => SELECT "users".* FROM "users" WHERE ("users"."score" > 1000)
+ #
+ # @example
+ # # Don't use it with existing `ActiveRecord::Base` methods, i.e. `name`:
+ # User.name # => "User"
+ # # In these cases you'll have to use `attribute` explicitely
+ # User.attribute(:name)
+ #
+ def method_missing(name, *args)
+ if sexy_scopes_has_attribute?(name)
+ sexy_scopes_define_attribute_method(name)
+ attribute(name)
+ else
+ super
+ end
+ end
+
+ def sexy_scopes_define_attribute_method(name)
+ class_eval <<-EVAL, __FILE__, __LINE__ + 1
+ def self.#{name} # def self.username
+ attribute(:#{name}) # attribute(:username)
+ end # end
+ EVAL
+ end
+
+ def sexy_scopes_has_attribute?(attribute_name)
+ if self != ::ActiveRecord::Base && !abstract_class? && table_exists?
+ column_names.include?(attribute_name.to_s)
+ end
+ rescue ::ActiveRecord::StatementInvalid
+ false
+ end
+ end
+ end
+end
View
@@ -1,8 +1,8 @@
require 'spec_helper'
-describe SexyScopes::ActiveRecord do
+describe SexyScopes::ActiveRecord::ClassMethods do
it "should extend ActiveRecord::Base" do
- ActiveRecord::Base.should be_extended_by SexyScopes::ActiveRecord
+ ActiveRecord::Base.should be_extended_by SexyScopes::ActiveRecord::ClassMethods
end
describe ".attribute(name)" do
@@ -23,46 +23,53 @@
end
it "should be aliased as `sql`" do
- SexyScopes::ActiveRecord.instance_method(:sql).should ==
- SexyScopes::ActiveRecord.instance_method(:sql_literal)
+ SexyScopes::ActiveRecord::ClassMethods.instance_method(:sql).should ==
+ SexyScopes::ActiveRecord::ClassMethods.instance_method(:sql_literal)
end
it { should be_extended_by SexyScopes::ExpressionWrappers }
it { should be_extended_by SexyScopes::PredicateWrappers }
end
+end
+
+describe SexyScopes::ActiveRecord::DynamicMethods do
+ before do
+ ActiveRecord::Migration.create_table :temp_users
+ ActiveRecord::Migration.add_column :temp_users, :username, :string
+ class ::TempUser < ActiveRecord::Base; end
+ end
- context "dynamic method handling (method_missing/respond_to?)" do
- before do
- ActiveRecord::Migration.add_column :users, :temp_column, :string
- User.reset_column_information
- end
-
- after do
- ActiveRecord::Migration.remove_column :users, :temp_column
- end
-
- it "should delegate to `attribute` when the method name is the name of an existing column" do
- User.should respond_to(:temp_column)
- User.should_receive(:attribute).with(:temp_column).once.and_return(:ok)
- User.temp_column.should == :ok
- end
-
- it "should define an attribute method to avoid repeated `method_missing` calls" do
- User.temp_column
- User.should_not_receive(:method_missing)
- User.temp_column
- end
-
- ruby_19 do
- it "should return a Method object for an existing column" do
- lambda { User.method(:temp_column) }.should_not raise_error
- end
- end
-
- it "should raise NoMethodError otherwise" do
- User.should_not respond_to(:foobar)
- lambda { User.foobar }.should raise_error NoMethodError
+ after do
+ Object.send(:remove_const, :TempUser)
+ ActiveRecord::Migration.drop_table :temp_users
+ end
+
+ it "should delegate to `attribute` when the method name is the name of an existing column" do
+ TempUser.should respond_to(:username)
+ TempUser.should_receive(:attribute).with(:username).once.and_return(:ok)
+ TempUser.username.should == :ok
+ end
+
+ it "should define an attribute method to avoid repeated `method_missing` calls" do
+ TempUser.username
+ TempUser.should_not_receive(:method_missing)
+ TempUser.username
+ end
+
+ ruby_19 do
+ it "should return a Method object for an existing column" do
+ lambda { TempUser.method(:username) }.should_not raise_error
end
end
+
+ it "should raise NoMethodError for a non-existing column" do
+ TempUser.should_not respond_to(:foobar)
+ lambda { TempUser.foobar }.should raise_error NoMethodError
+ end
+
+ it "should not raise error when table doesn't exist" do
+ TempUser.should_receive(:column_names).any_number_of_times.and_raise ActiveRecord::StatementInvalid
+ lambda { TempUser.respond_to?(:username) }.should_not raise_error
+ end
end

0 comments on commit 7242447

Please sign in to comment.