Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[back_assignments] create fixture models and spec titles #190

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9ee697c
[back_assignments] create fixture models and spec titles
Jan 20, 2015
9fdeae5
[back_assignments] finish spec cases enumeration
Jan 20, 2015
8dc8f44
[back_assignments] simplier name for fixture models
Jan 20, 2015
0bdf3f1
[back_assignments] make examples 'not implemented yet'
Jan 20, 2015
e48dd9f
[back_assignments] partial try to remember associations
Jan 20, 2015
6c8d3ae
[back_assignments] belongs_to (1-1) back asignment works
Jan 20, 2015
1df987d
[back_assignments] from belongs_to done
Jan 22, 2015
83ab208
[back_assignments]
Jan 24, 2015
8f06355
[back_assignments] remove belongs_to other side when setting 1-1 assoc
Jan 25, 2015
98f7fed
[back_assignments] put callbacks before association when including in
Jan 25, 2015
4cd377d
[back_assignments] propagate save for 1-1 associations
Jan 25, 2015
330c1b8
[back_assignments]
Jan 26, 2015
b97403b
add option to support specifying reverse association field
Mar 11, 2015
1fc6cf7
[back_assignments] add option to support specifying reverse associati…
Mar 11, 2015
9d54af6
[back_assignments] add reverse_association on collections
Mar 13, 2015
a1d431c
[back_assignments] move CollectionOfProxy class in its own file
Mar 13, 2015
12b54be
[back_assignments] add pet fixctures model
Mar 13, 2015
19d988e
Merge branch 'back_assignments' of github.com:yarmand/couchrest_model…
Mar 15, 2015
3dbabb2
add documentation for revers_associations
Mar 15, 2015
8274b9d
[back_assignments] activate back assignement only if the
Apr 10, 2015
bd6c4da
[back_assignments] update README
Apr 10, 2015
de2bfc3
[back_assignments] update documentation.
Apr 10, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ $ rails generate couchrest_model:config
$ rails generate model person --orm=couchrest_model
```

## General Usage
## General Usage

```ruby
require 'couchrest_model'
Expand Down Expand Up @@ -140,6 +140,20 @@ end
@cat.update_attributes(:name => 'Felix', :random_text => 'feline')
@cat.new? # false
@cat.random_text # Raises error!

### reverse associations
class Parent < CouchRest::Model::Base
collection_of :children
end

class Child < CouchRest::Model::Base
belongs_to :dad, class: Parent, :reverse_association => :children
end

@bob = Parent.new
@kevin = Child.new
@kevin.dad = @bob
@bob.children.include?(@kevin) # true
```

## Development
Expand Down
132 changes: 65 additions & 67 deletions lib/couchrest/model/associations.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
module CouchRest
module Model
module Associations
extend ActiveSupport::Concern

# Basic support for relationships between CouchRest::Model::Base
def self.included(base)
base.extend(ClassMethods)

included do
after_save :save_dirty_association if respond_to?(:after_save)
end

Association = Struct.new(:type, :attribute, :options, :target)

module ClassMethods

# Define an association that this object belongs to.
#
# An attribute will be created matching the name of the attribute
# with '_id' on the end, or the foreign key (:foreign_key) provided.
#
# Searching for the assocated object is performed using a string
# (:proxy) to be evaulated in the context of the owner. Typically
# Searching for the associated object is performed using a string
# (:proxy) to be evaluated in the context of the owner. Typically
# this will be set to the class name (:class_name), or determined
# automatically if the owner belongs to a proxy object.
#
# If the association owner is proxied by another model, than an attempt will
# be made to automatically determine the correct place to request
# the documents. Typically, this is a method with the pluralized name of the
# the documents. Typically, this is a method with the pluralized name of the
# association inside owner's owner, or proxy.
#
# For example, imagine a company acts as a proxy for invoices and clients.
Expand All @@ -32,14 +35,23 @@ module ClassMethods
#
# self.company.clients
#
# If the name of the collection proxy is not the pluralized assocation name,
# If the name of the collection proxy is not the pluralized association name,
# it can be set with the :proxy_name option.
#
# If the owner model define an association back to the belonged model, setting
# the owner will also set the (:reverse_association) attribute of the owner.
# After such affectation, saving the object model will also trigger the save of
# the owner object.
# (:reverse_association) is optional. When used, saving the belonged object will
# trigger the save of the owner object.
#
def belongs_to(attrib, *options)
opts = merge_belongs_to_association_options(attrib, options.first)

property(opts[:foreign_key], String, opts)

associations.push(Association.new(:belongs_to, attrib, opts, nil))

create_association_property_setter(attrib, opts)
create_belongs_to_getter(attrib, opts)
create_belongs_to_setter(attrib, opts)
Expand Down Expand Up @@ -84,25 +96,41 @@ def belongs_to(attrib, *options)
# NOTE: This method is *not* recommended for large collections or collections that change
# frequently! Use with prudence.
#
# If the associated model define an association back to the collection owner model, adding
# or removing from the collection will also populate the (:reverse_association) attribute
# of associated model.
# After such affectation, saving the object model will also trigger the save of
# the associated object.
# (:reverse_association) is optional. When used, saving the object with a collection will
# trigger save of the new members of the collection.
#
def collection_of(attrib, *options)
opts = merge_belongs_to_association_options(attrib, options.first)
opts[:foreign_key] = opts[:foreign_key].pluralize
opts[:readonly] = true

property(opts[:foreign_key], [String], opts)

associations.push(Association.new(:collection_of, attrib, opts, nil))

create_association_property_setter(attrib, opts)
create_collection_of_getter(attrib, opts)
create_collection_of_setter(attrib, opts)
end


def associations
@_associations ||= []
end

private

def merge_belongs_to_association_options(attrib, options = nil)
class_name = options.delete(:class_name) if options.is_a?(Hash)
class_name ||= attrib
opts = {
:foreign_key => attrib.to_s.singularize + '_id',
:class_name => attrib.to_s.singularize.camelcase,
:class_name => class_name.to_s.singularize.camelcase,
:proxy_name => attrib.to_s.pluralize,
:allow_blank => false
}
Expand Down Expand Up @@ -146,7 +174,15 @@ def #{attrib}
def create_belongs_to_setter(attrib, options)
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{attrib}=(value)
self.#{options[:foreign_key]} = value.nil? ? nil : value.id
binding = @#{attrib}
self.#{options[:foreign_key]} = value.nil? ? nil : value.id
unless value.nil?
binding = value
binding.set_back_association(self, self.class.name, '#{options[:reverse_association]}')
else
binding.set_back_association(nil, self.class.name, '#{options[:reverse_association]}')
end
register_dirty_association(binding)
@#{attrib} = value
end
EOS
Expand All @@ -159,84 +195,46 @@ def create_collection_of_getter(attrib, options)
def #{attrib}(reload = false)
return @#{attrib} unless @#{attrib}.nil? or reload
ary = self.#{options[:foreign_key]}.collect{|i| #{options[:proxy]}.get(i)}
@#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(ary, find_property('#{options[:foreign_key]}'), self)
@#{attrib} = ::CouchRest::Model::Associations::CollectionOfProxy.new(ary, find_property('#{options[:foreign_key]}'), self)
end
EOS
end

def create_collection_of_setter(attrib, options)
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{attrib}=(value)
@#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(value, find_property('#{options[:foreign_key]}'), self)
@#{attrib} = ::CouchRest::Model::Associations::CollectionOfProxy.new(value, find_property('#{options[:foreign_key]}'), self)
end
EOS
end

end

end

# Special proxy for a collection of items so that adding and removing
# to the list automatically updates the associated property.
class CollectionOfProxy < CastedArray

def initialize(array, property, parent)
(array ||= []).compact!
super(array, property, parent)
casted_by[casted_by_property.to_s] = [] # replace the original array!
array.compact.each do |obj|
check_obj(obj)
casted_by[casted_by_property.to_s] << obj.id
def set_back_association(value, class_name, reverse_association = nil)
if reverse_association && !reverse_association.empty?
prop = self.class.properties.detect { |prop| prop.name =~ %r{#{reverse_association.to_s.singularize}_ids?} }
raise "Cannot find reverse association: #{reverse_association}" unless prop
if attributes[prop.name].class.ancestors.include?(Enumerable)
instance_eval("#{prop.name}.push('#{value.nil? ? nil : value.id}')")
else
send("#{prop.name}=", (value.nil? ? nil : value.id))
end
end
end

def << obj
check_obj(obj)
casted_by[casted_by_property.to_s] << obj.id
super(obj)
def dirty_associations
@_dirty_associations ||= []
end

def push(obj)
check_obj(obj)
casted_by[casted_by_property.to_s].push obj.id
super(obj)
def register_dirty_association(obj)
dirty_associations << obj unless @_dirty_associations.include?(obj)
end

def unshift(obj)
check_obj(obj)
casted_by[casted_by_property.to_s].unshift obj.id
super(obj)
end

def []= index, obj
check_obj(obj)
casted_by[casted_by_property.to_s][index] = obj.id
super(index, obj)
end

def pop
casted_by[casted_by_property.to_s].pop
super
end

def shift
casted_by[casted_by_property.to_s].shift
super
end

protected

def check_obj(obj)
raise "Object cannot be added to #{casted_by.class.to_s}##{casted_by_property.to_s} collection unless saved" if obj.new?
end

# Override CastedArray instantiation_and_cast method for a simpler
# version that will not try to cast the model.
def instantiate_and_cast(obj, change = true)
couchrest_parent_will_change! if change && use_dirty?
obj.casted_by = casted_by if obj.respond_to?(:casted_by)
obj.casted_by_property = casted_by_property if obj.respond_to?(:casted_by_property)
obj
def save_dirty_association
while !dirty_associations.empty? do
obj = dirty_associations.pop
obj.save
end
end

end
Expand Down
81 changes: 81 additions & 0 deletions lib/couchrest/model/associations/collection_of_proxy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
module CouchRest
module Model
module Associations
# Special proxy for a collection of items so that adding and removing
# to the list automatically updates the associated property.
class CollectionOfProxy < CastedArray

def initialize(array, property, parent)
(array ||= []).compact!
super(array, property, parent)
casted_by[casted_by_property.to_s] = [] # replace the original array!
array.compact.each do |obj|
check_obj(obj)
casted_by[casted_by_property.to_s] << obj.id
end
end

def << obj
add_to_collection_with(:<<, obj)
super(obj)
end

def push(obj)
add_to_collection_with(:push, obj)
super(obj)
end

def unshift(obj)
add_to_collection_with(:unshift, obj)
super(obj)
end

def []= index, obj
add_to_collection_with(:[]=, obj, index)
super(index, obj)
end

def pop
obj = casted_by.send(casted_by_property.options[:proxy_name]).last
casted_by[casted_by_property.to_s].pop
obj.set_back_association(nil, casted_by.class.name, casted_by_property.options[:reverse_association])
casted_by.register_dirty_association(obj)
super
end

def shift
obj = casted_by.send(casted_by_property.options[:proxy_name]).first
casted_by[casted_by_property.to_s].shift
obj.set_back_association(nil, casted_by.class.name, casted_by_property.options[:reverse_association])
casted_by.register_dirty_association(obj)
super
end

protected

def check_obj(obj)
raise "Object cannot be added to #{casted_by.class.to_s}##{casted_by_property.to_s} collection unless saved" if obj.new?
end

def add_to_collection_with(method, obj, index=nil)
check_obj(obj)
args = [ obj.id ]
args = args.insert(0, index) if index
casted_by[casted_by_property.to_s].send(method, *args)
obj.set_back_association(casted_by, casted_by.class.name, casted_by_property.options[:reverse_association])
casted_by.register_dirty_association(obj)
end

# Override CastedArray instantiation_and_cast method for a simpler
# version that will not try to cast the model.
def instantiate_and_cast(obj, change = true)
couchrest_parent_will_change! if change && use_dirty?
obj.casted_by = casted_by if obj.respond_to?(:casted_by)
obj.casted_by_property = casted_by_property if obj.respond_to?(:casted_by_property)
obj
end

end
end
end
end
10 changes: 5 additions & 5 deletions lib/couchrest/model/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ class Base < CouchRest::Document
include ExtendedAttachments
include Proxyable
include PropertyProtection
include Associations
include Validations
include Callbacks
include Associations
include Designs
include CastedBy
include Dirty


def self.subclasses
@subclasses ||= []
Expand Down Expand Up @@ -68,13 +68,13 @@ def initialize(attributes = {}, options = {})
alias :new_record? :new?
alias :new_document? :new?

# Compare this model with another by confirming to see
# Compare this model with another by confirming to see
# if the IDs and their databases match!
#
# Camparison of the database is required in case the
# Camparison of the database is required in case the
# model has been proxied or loaded elsewhere.
#
# A Basic CouchRest document will only ever compare using
# A Basic CouchRest document will only ever compare using
# a Hash comparison on the attributes.
def == other
return false unless other.is_a?(Base)
Expand Down
1 change: 1 addition & 0 deletions lib/couchrest_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
require "couchrest/model/extended_attachments"
require "couchrest/model/proxyable"
require "couchrest/model/associations"
require "couchrest/model/associations/collection_of_proxy"
require "couchrest/model/configuration"
require "couchrest/model/connection"
require "couchrest/model/design"
Expand Down
7 changes: 7 additions & 0 deletions spec/fixtures/models/kid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Kid < CouchRest::Model::Base
property :name, String

belongs_to :dad, :class_name => 'Parent', :reverse_association => :children
belongs_to :mum, :class_name => 'Parent', :reverse_association => :children

end
Loading