Skip to content

Commit

Permalink
Basic belongs_to association
Browse files Browse the repository at this point in the history
  • Loading branch information
spohlenz committed Sep 15, 2009
1 parent d11f735 commit 53c111e
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 193 deletions.
36 changes: 36 additions & 0 deletions features/associations/belongs_to.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Feature: belongs_to association


Background:
Given the default Recliner::Document database is set to "http://localhost:5984/recliner-features"
And the database "http://localhost:5984/recliner-features" exists
And the following document definitions:
"""
class User < Recliner::Document
end
class Article < Recliner::Document
belongs_to :author
end
"""

Scenario: assigning instance to association
Given I have a saved instance of "User" with id "user-1"
And I have a saved instance of "Article" with id "article-1"
When I set its author to the "User" with id "user-1"
Then its "author" should be the "User" with id "user-1"

Scenario: assigning id to association
Given I have a saved instance of "User" with id "user-1"
And I have a saved instance of "Article" with id "article-1"
When I set its author_id to "user-1"
Then its "author" should be the "User" with id "user-1"

Scenario: loading association
Given I have a saved instance of "User" with id "user-1"
And I have a saved instance of "Article" with attributes:
| id | article-1 |
| author_id | user-1 |
When I load the "Article" instance with id "article-1"
Then its "author" should be the "User" with id "user-1"

8 changes: 8 additions & 0 deletions features/step_definitions/association_steps.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
When /^I set its (\w+) to the "([^\"]*)" with id "([^\"]*)"$/ do |association, model, id|
@instance.send("#{association}=", model.constantize.load!(id))
end

Then /^its "([^\"]*)" should be the "([^\"]*)" with id "([^\"]*)"$/ do |association, model, id|
@instance.send(association).should be_an_instance_of(model.constantize)
@instance.send(association).id.should == id
end
10 changes: 9 additions & 1 deletion features/step_definitions/document_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
end
end

Given /^the following document definition:$/ do |code|
Given /^the following document definitions?:$/ do |code|
@defined_constants ||= []
@defined_constants += ActiveSupport::Dependencies.new_constants_in(Object) { eval(code) }
end
Expand All @@ -46,6 +46,14 @@
@instance.should_not be_a_new_record
end

Given /^I have a saved instance of "([^\"]*)" with attributes:$/ do |klass, table|
attributes = table.rows_hash
@instance = klass.constantize.new
attributes.each { |k, v| @instance.write_attribute(k, v) }
@instance.save!
@instance.should_not be_a_new_record
end

When /^I create an instance of "([^\"]*)"$/ do |klass|
@instance = klass.constantize.new
end
Expand Down
2 changes: 1 addition & 1 deletion lib/recliner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module Recliner
autoload :ViewFunction, 'recliner/views/function'
autoload :ViewGenerator, 'recliner/views/generator'

# autoload :Associations, 'recliner/associations'
autoload :Associations, 'recliner/associations'

autoload :Validations, 'recliner/validations'
autoload :Callbacks, 'recliner/callbacks'
Expand Down
23 changes: 2 additions & 21 deletions lib/recliner/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,11 @@ module Recliner
module Associations
extend ActiveSupport::Concern

autoload :Reference, 'recliner/associations/reference'
autoload :BelongsTo, 'recliner/associations/belongs_to'

included do
extend BelongsTo::ClassMethods
end

module ClassMethods
def associations
read_inheritable_attribute(:associations) || write_inheritable_attribute(:associations, {})
end
end

def associations
@associations ||= initialize_associations
end

private
def initialize_associations
self.class.associations.inject({}) { |result, name_and_association|
name, association = name_and_association

result[name] = association.create_proxy(self)
result
}
extend BelongsTo
end
end
end
177 changes: 10 additions & 167 deletions lib/recliner/associations/belongs_to.rb
Original file line number Diff line number Diff line change
@@ -1,175 +1,18 @@
module Recliner
module Associations
module BelongsTo
module ClassMethods
def belongs_to(name, options={})
associations[name] = Association.new(name, options)
class_eval associations[name].generate_code

associations[name]
end
end

class Proxy
alias_method :proxy_respond_to?, :respond_to?
alias_method :proxy_extend, :extend

delegate :to_param, :to => :proxy_target

instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }

def initialize(owner, association)
@owner, @association = owner, association
end

# Returns the owner of the proxy.
def proxy_owner
@owner
end

# Returns the reflection object that represents the association handled
# by the proxy.
def proxy_target
@target
end

# Returns the \target of the proxy, same as +target+.
def proxy_association
@association
end

# Has the \target been already \loaded?
def loaded?
@loaded
end

# Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded
@loaded = true
end

# Returns the target of this proxy, same as +proxy_target+.
def target
@target
end

# Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
def target=(target)
@target = target
loaded
end

# Resets the \loaded flag to +false+ and sets the \target to +nil+.
def reset
@loaded = false
@target = nil
end

# Reloads the \target and returns +self+ on success.
def reload
reset
load_target
self unless @target.nil?
end

def inspect
load_target
@target.inspect
end

# Does the proxy or its \target respond to +symbol+?
def respond_to?(*args)
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
end

# Forwards <tt>===</tt> explicitly to the \target because the instance method
# removal above doesn't catch it. Loads the \target if needed.
def ===(other)
load_target
other === @target
end

def send(method, *args)
if proxy_respond_to?(method)
super
else
load_target
@target.send(method, *args)
end
end

private
# Forwards any missing method call to the \target.
def method_missing(method, *args)
if load_target
unless @target.respond_to?(method)
message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
raise NoMethodError, message
end

if block_given?
@target.send(method, *args) { |*block_args| yield(*block_args) }
else
@target.send(method, *args)
end
end
end

# Loads the \target if needed and returns it.
#
# This method is abstract in the sense that it relies on +find_target+,
# which is expected to be provided by descendants.
#
# If the \target is already \loaded it is just returned. Thus, you can call
# +load_target+ unconditionally to get the \target.
#
# ActiveRecord::RecordNotFound is rescued within the method, and it is
# not reraised. The proxy is \reset and +nil+ is the return value.
def load_target
unless loaded?
@target = find_target
end
def belongs_to(name, options={})
property "#{name}_id", Reference

@loaded = true
@target
rescue Recliner::DocumentNotFound
reset
end

def find_target
Recliner::Document.load(@owner.send(@association.property))
define_method(name) do
reference = send("#{name}_id")
Recliner::Document.with_database(database) { reference.target } if reference
end
end

class Association
attr_reader :name, :options

def initialize(name, options)
@name, @options = name, options
end

def generate_code
<<-END_RUBY
property :#{property}, String # property :book_id, String
#
def #{name}(force_reload=false) # def book(force_reload=false)
associations[:#{name}].reset if force_reload # associations[:book].reset if force_reload
associations[:#{name}] # associations[:book]
end # end
#
def #{name}=(obj) # def book=(obj)
self.#{property} = obj.id # self.book_id = obj.id
associations[:#{name}].target = obj # associations[:book].target = obj
end # end
END_RUBY
end

def property
"#{name}_id"
end

def create_proxy(owner)
Proxy.new(owner, self)

define_method("#{name}=") do |obj|
reference = send("#{name}_id")
reference = send("#{name}_id=", Reference.new) unless reference
reference.replace(obj)
end
end
end
Expand Down
40 changes: 40 additions & 0 deletions lib/recliner/associations/reference.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module Recliner
module Associations
class Reference
attr_reader :id

def initialize(id=nil)
@id = id
end

def ==(other)
other.is_a?(Reference) && id == other.id
end

def self.from_couch(id)
new(id)
end

def self.parse(id)
new(id)
end

def to_couch
id
end

def inspect
id.nil? ? 'nil' : id
end

def replace(object)
@id = object.id
@target = object
end

def target
@target ||= Recliner::Document.load!(id) if id
end
end
end
end
2 changes: 1 addition & 1 deletion lib/recliner/attribute_methods/dirty.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def write_attribute(attr, value)
old = clone_attribute_value(attr)
changed_attributes[attr] = old unless value == old
end

# Carry on.
super
end
Expand Down
5 changes: 3 additions & 2 deletions lib/recliner/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,9 @@ def load_multiple(ids, raise_exceptions=false)
include Callbacks

include Views
# include Associations


include Associations

include Timestamps
include PrettyInspect

Expand Down
Loading

0 comments on commit 53c111e

Please sign in to comment.