forked from rails/activeresource
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request rails#1 from SweeD/add_associations
[Feature] ActiveResource - Associations through reflections
- Loading branch information
Showing
13 changed files
with
438 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
module ActiveResource::Associations | ||
|
||
module Builder | ||
autoload :Association, 'active_resource/associations/builder/association' | ||
autoload :HasMany, 'active_resource/associations/builder/has_many' | ||
autoload :HasOne, 'active_resource/associations/builder/has_one' | ||
autoload :BelongsTo, 'active_resource/associations/builder/belongs_to' | ||
end | ||
|
||
|
||
|
||
# Specifies a one-to-many association. | ||
# | ||
# === Options | ||
# [:class_name] | ||
# Specify the class name of the association. This class name would | ||
# be used for resolving the association class. | ||
# | ||
# ==== Example for [:class_name] - option | ||
# GET /posts/123.xml delivers following response body: | ||
# <post> | ||
# <title>ActiveResource now have associations</title> | ||
# <content> ... </content> | ||
# <comments> | ||
# <comment> ... </comment> | ||
# <comment> ... </comment> | ||
# </comments> | ||
# </post> | ||
# ==== | ||
# | ||
# <tt>has_many :comments, :class_name => 'myblog/comment'</tt> | ||
# Would resolve those comments into the <tt>Myblog::Comment</tt> class. | ||
def has_many(name, options = {}) | ||
Builder::HasMany.build(self, name, options) | ||
end | ||
|
||
# Specifies a one-to-one association. | ||
# | ||
# === Options | ||
# [:class_name] | ||
# Specify the class name of the association. This class name would | ||
# be used for resolving the association class. | ||
# | ||
# ==== Example for [:class_name] - option | ||
# GET /posts/123.xml delivers following response body: | ||
# <post> | ||
# <title>ActiveResource now have associations</title> | ||
# <content> ... </content> | ||
# <author> | ||
# <name>caffeinatedBoys</name> | ||
# </author> | ||
# </post> | ||
# ==== | ||
# | ||
# <tt>has_one :author, :class_name => 'myblog/author'</tt> | ||
# Would resolve this author into the <tt>Myblog::Author</tt> class. | ||
def has_one(name, options = {}) | ||
Builder::HasOne.build(self, name, options) | ||
end | ||
|
||
# Specifies a one-to-one association with another class. This class should only be used | ||
# if this class contains the foreign key. | ||
# | ||
# Methods will be added for retrieval and query for a single associated object, for which | ||
# this object holds an id: | ||
# | ||
# [association(force_reload = false)] | ||
# Returns the associated object. +nil+ is returned if the foreign key is +nil+. | ||
# Throws a ActiveResource::ResourceNotFound exception if the foreign key is not +nil+ | ||
# and the resource is not found. | ||
# | ||
# (+association+ is replaced with the symbol passed as the first argument, so | ||
# <tt>belongs_to :post</tt> would add among others <tt>post.nil?</tt>. | ||
# | ||
# === Example | ||
# | ||
# A Comment class declaress <tt>belongs_to :post</tt>, which will add: | ||
# * <tt>Comment#post</tt> (similar to <tt>Post.find(post_id)</tt>) | ||
# The declaration can also include an options hash to specialize the behavior of the association. | ||
# | ||
# === Options | ||
# [:class_name] | ||
# Specify the class name for the association. Use it only if that name canÄt be inferred from association name. | ||
# So <tt>belongs_to :post</tt> will by default be linked to the Post class, but if the real class name is Article, | ||
# you'll have to specify it with whis option. | ||
# [:foreign_key] | ||
# Specify the foreign key used for the association. By default this is guessed to be the name | ||
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :post</tt> | ||
# association will use "post_id" as the default <tt>:foreign_key</tt>. Similarly, | ||
# <tt>belongs_to :article, :class_name => "Post"</tt> will use a foreign key | ||
# of "article_id". | ||
# | ||
# Option examples: | ||
# <tt>belongs_to :customer, :class_name => 'User'</tt> | ||
# Creates a belongs_to association called customer which is represented through the <tt>User</tt> class. | ||
# | ||
# <tt>belongs_to :customer, :foreign_key => 'user_id'</tt> | ||
# Creates a belongs_to association called customer which would be resolved by the foreign_key <tt>user_id</tt> instead of <tt>customer_id</tt> | ||
# | ||
def belongs_to(name, options={}) | ||
Builder::BelongsTo.build(self, name, options) | ||
end | ||
|
||
# Defines the belongs_to association finder method | ||
def defines_belongs_to_finder_method(method_name, association_model, finder_key) | ||
ivar_name = :"@#{method_name}" | ||
|
||
if method_defined?(method_name) | ||
instance_variable_set(ivar_name, nil) | ||
remove_method(method_name) | ||
end | ||
|
||
define_method(method_name) do | ||
instance_variable_defined?(ivar_name) ? instance_variable_get(ivar_name) : instance_variable_set(ivar_name, association_model.find(send(finder_key))) | ||
end | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
module ActiveResource::Associations::Builder | ||
class Association #:nodoc: | ||
|
||
# providing a Class-Variable, which will have a different store of subclasses | ||
class_attribute :valid_options | ||
self.valid_options = [:class_name] | ||
|
||
# would identify subclasses of association | ||
class_attribute :macro | ||
|
||
attr_reader :model, :name, :options, :klass | ||
|
||
def self.build(model, name, options) | ||
new(model, name, options).build | ||
end | ||
|
||
def initialize(model, name, options) | ||
@model, @name, @options = model, name, options | ||
end | ||
|
||
def build | ||
validate_options | ||
model.create_reflection(self.class.macro, name, options) | ||
end | ||
|
||
private | ||
|
||
def validate_options | ||
options.assert_valid_keys(self.class.valid_options) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
module ActiveResource::Associations::Builder | ||
class BelongsTo < Association | ||
self.valid_options += [:foreign_key] | ||
|
||
self.macro = :belongs_to | ||
|
||
def build | ||
validate_options | ||
reflection = model.create_reflection(self.class.macro, name, options) | ||
model.defines_belongs_to_finder_method(reflection.name, reflection.klass, reflection.foreign_key) | ||
return reflection | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module ActiveResource::Associations::Builder | ||
class HasMany < Association | ||
self.macro = :has_many | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module ActiveResource::Associations::Builder | ||
class HasOne < Association | ||
self.macro = :has_one | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
require 'active_support/core_ext/class/attribute' | ||
require 'active_support/core_ext/module/deprecation' | ||
|
||
module ActiveResource | ||
# = Active Resource reflection | ||
# | ||
# Associations in ActiveResource would be used to resolve nested attributes | ||
# in a response with correct classes. | ||
# Now they could be specified over Associations with the options :class_name | ||
module Reflection # :nodoc: | ||
extend ActiveSupport::Concern | ||
|
||
included do | ||
class_attribute :reflections | ||
self.reflections = {} | ||
end | ||
|
||
module ClassMethods | ||
def create_reflection(macro, name, options) | ||
reflection = AssociationReflection.new(macro, name, options) | ||
self.reflections = self.reflections.merge(name => reflection) | ||
reflection | ||
end | ||
end | ||
|
||
|
||
class AssociationReflection | ||
|
||
def initialize(macro, name, options) | ||
@macro, @name, @options = macro, name, options | ||
end | ||
|
||
# Returns the name of the macro. | ||
# | ||
# <tt>has_many :clients</tt> returns <tt>:clients</tt> | ||
attr_reader :name | ||
|
||
# Returns the macro type. | ||
# | ||
# <tt>has_many :clients</tt> returns <tt>:has_many</tt> | ||
attr_reader :macro | ||
|
||
# Returns the hash of options used for the macro. | ||
# | ||
# <tt>has_many :clients</tt> returns +{}+ | ||
attr_reader :options | ||
|
||
# Returns the class for the macro. | ||
# | ||
# <tt>has_many :clients</tt> returns the Client class | ||
def klass | ||
@klass ||= class_name.constantize | ||
end | ||
|
||
# Returns the class name for the macro. | ||
# | ||
# <tt>has_many :clients</tt> returns <tt>'Client'</tt> | ||
def class_name | ||
@class_name ||= derive_class_name | ||
end | ||
|
||
# Returns the foreign_key for the macro. | ||
def foreign_key | ||
@foreign_key ||= self.options[:foreign_key] || "#{self.name.to_s.downcase}_id" | ||
end | ||
|
||
private | ||
def derive_class_name | ||
return (options[:class_name] ? options[:class_name].to_s : name.to_s).classify | ||
end | ||
|
||
def derive_foreign_key | ||
return options[:foreign_key] ? options[:foreign_key].to_s : "#{name.to_s.downcase}_id" | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
require 'abstract_unit' | ||
|
||
require 'fixtures/person' | ||
require 'fixtures/beast' | ||
require 'fixtures/customer' | ||
|
||
|
||
class AssociationTest < ActiveSupport::TestCase | ||
def setup | ||
@klass = ActiveResource::Associations::Builder::Association | ||
end | ||
|
||
|
||
def test_validations_for_instance | ||
object = @klass.new(Person, :customers, {}) | ||
assert_equal({}, object.send(:validate_options)) | ||
end | ||
|
||
def test_instance_build | ||
object = @klass.new(Person, :customers, {}) | ||
assert_kind_of ActiveResource::Reflection::AssociationReflection, object.build | ||
end | ||
|
||
def test_valid_options | ||
assert @klass.build(Person, :customers, {:class_name => 'Client'}) | ||
|
||
assert_raise ArgumentError do | ||
@klass.build(Person, :customers, {:soo_invalid => true}) | ||
end | ||
end | ||
|
||
def test_association_class_build | ||
assert_kind_of ActiveResource::Reflection::AssociationReflection, @klass.build(Person, :customers, {}) | ||
end | ||
|
||
def test_has_many | ||
External::Person.send(:has_many, :people) | ||
assert_equal 1, External::Person.reflections.select{|name, reflection| reflection.macro.eql?(:has_many)}.count | ||
end | ||
|
||
def test_has_one | ||
External::Person.send(:has_one, :customer) | ||
assert_equal 1, External::Person.reflections.select{|name, reflection| reflection.macro.eql?(:has_one)}.count | ||
end | ||
|
||
def test_belongs_to | ||
External::Person.belongs_to(:Customer) | ||
assert_equal 1, External::Person.reflections.select{|name, reflection| reflection.macro.eql?(:belongs_to)}.count | ||
end | ||
|
||
def test_defines_belongs_to_finder_method_with_instance_variable_cache | ||
Person.defines_belongs_to_finder_method(:customer, Customer, 'customer_id') | ||
|
||
person = Person.new | ||
assert !person.instance_variable_defined?(:@customer) | ||
person.stubs(:customer_id).returns(2) | ||
Customer.expects(:find).with(2).once() | ||
2.times{person.customer} | ||
assert person.instance_variable_defined?(:@customer) | ||
end | ||
end |
Oops, something went wrong.