couchobject 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +30 -6
  3. data/README.txt +580 -42
  4. data/TODO +2 -2
  5. data/config/hoe.rb +1 -1
  6. data/lib/couch_object.rb +7 -2
  7. data/lib/couch_object/database.rb +19 -34
  8. data/lib/couch_object/document.rb +13 -6
  9. data/lib/couch_object/error_classes.rb +110 -0
  10. data/lib/couch_object/persistable.rb +954 -36
  11. data/lib/couch_object/persistable/has_many_relations_array.rb +91 -0
  12. data/lib/couch_object/persistable/meta_classes.rb +568 -0
  13. data/lib/couch_object/persistable/overloaded_methods.rb +209 -0
  14. data/lib/couch_object/server.rb +1 -1
  15. data/lib/couch_object/utils.rb +44 -0
  16. data/lib/couch_object/version.rb +1 -1
  17. data/lib/couch_object/view.rb +129 -6
  18. data/script/console +0 -0
  19. data/script/destroy +0 -0
  20. data/script/generate +0 -0
  21. data/script/txt2html +0 -0
  22. data/spec/database_spec.rb +23 -31
  23. data/spec/database_spec.rb.orig +173 -0
  24. data/spec/document_spec.rb +21 -3
  25. data/spec/integration/database_integration_spec.rb +46 -15
  26. data/spec/integration/integration_helper.rb +3 -3
  27. data/spec/persistable/callback.rb +44 -0
  28. data/spec/persistable/callback_spec.rb +44 -0
  29. data/spec/persistable/cloning.rb +77 -0
  30. data/spec/persistable/cloning_spec.rb +77 -0
  31. data/spec/persistable/comparing_objects.rb +350 -0
  32. data/spec/persistable/comparing_objects_spec.rb +350 -0
  33. data/spec/persistable/deleting.rb +113 -0
  34. data/spec/persistable/deleting_spec.rb +113 -0
  35. data/spec/persistable/error_messages.rb +32 -0
  36. data/spec/persistable/error_messages_spec.rb +32 -0
  37. data/spec/persistable/loading.rb +339 -0
  38. data/spec/persistable/loading_spec.rb +339 -0
  39. data/spec/persistable/new_methods.rb +70 -0
  40. data/spec/persistable/new_methods_spec.rb +70 -0
  41. data/spec/persistable/persistable_helper.rb +194 -0
  42. data/spec/persistable/relations.rb +470 -0
  43. data/spec/persistable/relations_spec.rb +470 -0
  44. data/spec/persistable/saving.rb +137 -0
  45. data/spec/persistable/saving_spec.rb +137 -0
  46. data/spec/persistable/setting_storage_location.rb +65 -0
  47. data/spec/persistable/setting_storage_location_spec.rb +65 -0
  48. data/spec/persistable/timestamps.rb +76 -0
  49. data/spec/persistable/timestamps_spec.rb +76 -0
  50. data/spec/persistable/unsaved_changes.rb +211 -0
  51. data/spec/persistable/unsaved_changes_spec.rb +211 -0
  52. data/spec/server_spec.rb +5 -5
  53. data/spec/utils_spec.rb +60 -0
  54. data/spec/view_spec.rb +40 -7
  55. data/website/index.html +22 -7
  56. data/website/index.txt +13 -5
  57. metadata +93 -61
  58. data/bin/couch_ruby_view_requestor +0 -81
  59. data/lib/couch_object/model.rb +0 -5
  60. data/lib/couch_object/proc_condition.rb +0 -14
  61. data/spec/model_spec.rb +0 -5
  62. data/spec/persistable_spec.rb +0 -91
  63. data/spec/proc_condition_spec.rb +0 -26
data/TODO CHANGED
@@ -1,4 +1,4 @@
1
1
  - Expand on View class
2
- - Do dot notation for document attributes in Database#filter
3
2
 
4
- - CouchObject::Model for more domain specific/clearer way to model Couch docs in ruby
3
+ - CouchObject::Model for more domain specific/clearer way to model Couch docs in ruby
4
+
@@ -1,6 +1,6 @@
1
1
  require 'couch_object/version'
2
2
 
3
- AUTHOR = 'Johan Sørensen' # can also be an array of Authors
3
+ AUTHOR = ['Johan Sørensen','Sebastian Probst Eide'] # can also be an array of Authors
4
4
  EMAIL = "johan@johansorensen.com"
5
5
  DESCRIPTION = "CouchObject is a library that maps ruby objects to CouchDb documents"
6
6
  GEM_NAME = 'couchobject' # what ppl will type to install your gem
@@ -11,12 +11,16 @@ rescue LoadError
11
11
  end
12
12
 
13
13
  require 'json/add/core'
14
- require "ruby2ruby"
14
+ require 'thread'
15
+
16
+ # require "htmlentities" # if you want the UTILS::decode_strings to
17
+ # replace characters like ø with ø
18
+ # then this has to be included...
19
+ require "kconv" # To convert all input to UTF-8 before it is made to_json
15
20
 
16
21
  $:.unshift File.dirname(__FILE__)
17
22
 
18
23
  require "couch_object/utils"
19
- require "couch_object/proc_condition"
20
24
  require "couch_object/document"
21
25
  require "couch_object/response"
22
26
  require "couch_object/server"
@@ -24,6 +28,7 @@ require "couch_object/database"
24
28
  require "couch_object/view"
25
29
  require "couch_object/persistable"
26
30
  require "couch_object/model"
31
+ require "couch_object/error_classes"
27
32
 
28
33
  module CouchObject
29
34
 
@@ -2,7 +2,7 @@ module CouchObject
2
2
  # A CouchDb database object
3
3
  class Database
4
4
  # Create a new database at +uri+ with the name if +dbname+
5
- def self.create!(uri, dbname)
5
+ def self.create(uri, dbname)
6
6
  server = Server.new(uri)
7
7
  response = Response.new(server.put("/#{dbname}", "")).parse
8
8
  response.parsed_body
@@ -25,7 +25,7 @@ module CouchObject
25
25
  end
26
26
 
27
27
  # Open a connection to the database at +uri+, where +uri+ is a full uri
28
- # like: http://localhost:8888/foo
28
+ # like: http://localhost:5984/foo
29
29
  def self.open(uri)
30
30
  uri = URI.parse(uri)
31
31
  server_uri = "#{uri.scheme}://#{uri.host}:#{uri.port}"
@@ -39,7 +39,7 @@ module CouchObject
39
39
  end
40
40
  attr_accessor :server
41
41
 
42
- # The full url of this database, eg http://localhost:8888/foo
42
+ # The full url of this database, eg http://localhost:5984/foo
43
43
  def url
44
44
  Utils.join_url(@uri, @dbname).to_s
45
45
  end
@@ -50,35 +50,37 @@ module CouchObject
50
50
  end
51
51
 
52
52
  # Send a GET request to the +path+ which is relative to the database path
53
- # so calling with with "bar" as the path in the "foo_db" database will call
54
- # http://host:port/foo_db/bar.
53
+ # so calling with with "bar" as the path in the "foo_db" database will
54
+ # call http://host:port/foo_db/bar.
55
55
  # Returns a Response object
56
56
  def get(path)
57
57
  Response.new(@server.get("/#{Utils.join_url(@dbname, path)}")).parse
58
58
  end
59
59
 
60
60
  # Send a POST request to the +path+ which is relative to the database path
61
- # so calling with with "bar" as the path in the "foo_db" database will call
62
- # http://host:port/foo_db/bar. The post body is the +payload+
61
+ # so calling with with "bar" as the path in the "foo_db" database will
62
+ # call http://host:port/foo_db/bar. The post body is the +payload+
63
63
  # Returns a Response object
64
- def post(path, payload)
65
- Response.new(@server.post("/#{Utils.join_url(@dbname, path)}", payload)).parse
64
+ def post(path, payload, content_type="application/json")
65
+ Response.new(@server.post("/#{Utils.join_url(@dbname, path)}", payload, content_type)).parse
66
66
  end
67
67
 
68
68
  # Send a PUT request to the +path+ which is relative to the database path
69
- # so calling with with "bar" as the path in the "foo_db" database will call
70
- # http://host:port/foo_db/bar. The put body is the +payload+
69
+ # so calling with with "bar" as the path in the "foo_db" database will
70
+ # call http://host:port/foo_db/bar. The put body is the +payload+
71
71
  # Returns a Response object
72
72
  def put(path, payload="")
73
- Response.new(@server.put("/#{Utils.join_url(@dbname, path)}", payload)).parse
73
+ Response.new(@server.
74
+ put("/#{Utils.join_url(@dbname, path)}", payload)).parse
74
75
  end
75
76
 
76
- # Send a DELETE request to the +path+ which is relative to the database path
77
- # so calling with with "bar" as the path in the "foo_db" database will call
78
- # http://host:port/foo_db/bar.
77
+ # Send a DELETE request to the +path+ which is relative to the
78
+ # database path so calling with with "bar" as the path in the "foo_db"
79
+ # database will call http://host:port/foo_db/bar.
79
80
  # Returns a Response object
80
- def delete(path)
81
- Response.new(@server.delete("/#{Utils.join_url(@dbname, path)}")).parse
81
+ def delete(path, revision)
82
+ Response.new(@server.
83
+ delete("/#{Utils.join_url(@dbname, path)}?rev=#{revision}")).parse
82
84
  end
83
85
 
84
86
  # Get a document by id
@@ -105,23 +107,6 @@ module CouchObject
105
107
  document.save(self)
106
108
  end
107
109
 
108
- # Queries the database with the block (using a temp. view)
109
- # Requires a block argument that's the doc thats evaluted in
110
- # CouchDb
111
- #
112
- # >> pp db.filter do |doc|
113
- # if doc["foo"] == "baz"
114
- # return doc["foo"]
115
- # end
116
- # end
117
- # [{"_rev"=>928806717,
118
- # "_id"=>"28D568C5992CBD2B4711F57225A19517",
119
- # "value"=>"baz"}]
120
- def filter(&block)
121
- resp = Response.new(post("_temp_view", ProcCondition.new(&block).to_ruby)).parse
122
- resp.to_document.rows
123
- end
124
-
125
110
  def views(view_name)
126
111
  view = View.new(self, view_name)
127
112
  view.query
@@ -7,8 +7,8 @@ module CouchObject
7
7
  # the document values
8
8
  def initialize(attributes={})
9
9
  @attributes = attributes.dup
10
- @id = @attributes.delete("_id")
11
- @revision = @attributes.delete("_rev")
10
+ @id = @attributes.delete("_id") || @attributes.delete("id")
11
+ @revision = @attributes.delete("_rev") || @attributes.delete("rev")
12
12
  end
13
13
  attr_accessor :attributes, :id, :revision
14
14
 
@@ -79,14 +79,14 @@ module CouchObject
79
79
  def create(database)
80
80
  response = database.post("", self.to_json)
81
81
  # TODO error handling
82
- @id = response.to_document.id
83
- @revision = response.to_document.revision
84
- response
82
+ apply_response(response)
85
83
  end
86
84
 
87
85
  def update(database)
88
- response = database.put(id, self.to_json("_rev" => revision))
86
+ revision ? json = self.to_json("_rev" => revision) : json = self.to_json
87
+ response = database.put(id, json)
89
88
  # TODO error handling
89
+ apply_response(response)
90
90
  end
91
91
 
92
92
  private
@@ -102,5 +102,12 @@ module CouchObject
102
102
  has_key?(method_name) ? self[method_name] : super
103
103
  end
104
104
  end
105
+
106
+ def apply_response(response)
107
+ doc = response.to_document
108
+ @id = doc.id
109
+ @revision = doc.revision
110
+ response
111
+ end
105
112
  end
106
113
  end
@@ -0,0 +1,110 @@
1
+ module CouchObject
2
+ module Errors
3
+ #
4
+ # Is raised when trying to load a view that doesn't exist on
5
+ # the couchDB server
6
+ #
7
+ class MissingView < StandardError
8
+ def message
9
+ "The view doesn't exist on the server."
10
+ end
11
+ alias_method :to_s, :message
12
+ end
13
+
14
+
15
+ #####
16
+ #
17
+ # ERRORS for the Persistable module
18
+ #
19
+ #####
20
+
21
+ #
22
+ # Is raised when a method that interacts with the
23
+ # document store is called and the location variable
24
+ # has not been set
25
+ #
26
+ class NoDatabaseLocationSet < StandardError
27
+ def message
28
+ "Unless the document has previously been saved you need to " + \
29
+ "supply the full URI to the CouchDB server where the document " + \
30
+ "is to be saved."
31
+ end
32
+ alias_method :to_s, :message
33
+ end
34
+
35
+ #
36
+ # Is raised when trying to compare two object of which
37
+ # none has been saved.
38
+ #
39
+ class CantCompareSize < StandardError
40
+ def message
41
+ "Can't compare the size of two objects unless at least one of " + \
42
+ "them has been saved and both have timestamps."
43
+ end
44
+ alias_method :to_s, :message
45
+ end
46
+
47
+ #
48
+ # Is raised if a has_many relationship is poorly defined
49
+ #
50
+ class HasManyAssociationError < StandardError
51
+ def message
52
+ "The has_many relationship hasn't been correctly defined. It should look something like: has_many :fruits. Make sure to also include a belongs_to relationship in the related classes that reference back to the has_many relationship. For example like this: belongs_to :fruit_basket, :as => :fruits"
53
+ end
54
+ alias_method :to_s, :message
55
+ end
56
+
57
+ #
58
+ # Is raised if a belongs_to relationship is poorly defined
59
+ #
60
+ class BelongsToAssociationError < StandardError
61
+ def message
62
+ "The belongs_to relationship hasn't been properly defined. There are two required parameters: the name of the relation and the name of the corresponding has_many relation in the related class. Example: belongs_to :fruit_basket, :as => :fruits would be correct if the related class has it's relation defined as has_many :fruits"
63
+ end
64
+ alias_method :to_s, :message
65
+ end
66
+
67
+ #
68
+ # Is raised if a has_one relationship is poorly defined
69
+ #
70
+ class HasOneAssociationError < StandardError
71
+ def message
72
+ "The has_one relationship hasn't been correctly defined. It should look something like: has_one :sword. Make sure to also include a belongs_to relationship in the related classes that reference back to the has_one relationship. For example like this: belongs_to :master, :as => :sword"
73
+ end
74
+ alias_method :to_s, :message
75
+ end
76
+
77
+ #
78
+ # This error is raised when trying to load a document that doesn't
79
+ # exist on the server
80
+ #
81
+ class DocumentNotFound < StandardError
82
+ end
83
+
84
+ #
85
+ # This error is raised when trying to save or updating
86
+ # a document fails
87
+ #
88
+ class DatabaseSaveFailed < StandardError
89
+ end
90
+
91
+ #
92
+ # This error is raised when trying to load a view
93
+ # but CouchDB screwes it up and returns a MapProcessError
94
+ #
95
+ class MapProcessError < StandardError
96
+ def message
97
+ "CouchDB screwed it up somehow. There might be something wrong with your view as well."
98
+ end
99
+ alias_method :to_s, :message
100
+ end
101
+
102
+ #
103
+ # Is raised when one the CouchDB returns an error which isn't covered
104
+ # by one of the functions above
105
+ #
106
+ class CouchDBError < StandardError
107
+ end
108
+
109
+ end
110
+ end
@@ -1,59 +1,977 @@
1
+ # = Persistable
2
+ #
3
+ # == Introduction
4
+ #
5
+ # The persistable mixin let's you save and load your classes
6
+ # to and from CoachDB using the instance method +save+ and
7
+ # the class level method +get_by_id+.
8
+ # Documentation and examples can be found in the README file.
9
+ # The specs also contain lots of examples.
10
+ #
11
+ #
12
+ # = TODOs and known issues:
13
+ #
14
+ # * TODO: create a way to solve saving conflicts. Should also be possible
15
+ # to chose if only conflicts for certain variables should be handled.
16
+ #
17
+ # * TODO: If real world usage shows that it is needed then create a reload method
18
+ # that reloads the content from the db. Might be useful if some other
19
+ # process has changed the content of the document in which case the local
20
+ # copy can't be stored to the DB. Should local changes be overwritten?
21
+ # How should differences between the version on the server and the local
22
+ # version be handeled?
23
+ #
24
+ # * ISSUE:
25
+ # When creating a new document the server does not return other values
26
+ # than the new documents ID and REVISION number. The local representation
27
+ # of the created_at and updated_at attributes are therefore set right before
28
+ # the call to the server is issued. Their values are therefore not
29
+ # guaranteed to be completely in sync with the values stored on the server!
30
+ #
31
+ # * ISSUE:
32
+ # When loading multiple objects from a view using the get_from_view
33
+ # mehtod that normally would have been in a belongs_to or has_many relation,
34
+ # their relations aren't set.
35
+ #
36
+ # * ISSUE:
37
+ # Given the following scenario: B is in a has_many/has_one relationship to A
38
+ # and both have been saved to the database. B is removed from the
39
+ # relationship and A is then saved. Saving A wont save the changed version
40
+ # of B where the relationship has been ended. Therefore: the next time A
41
+ # is loaded from the database B will be loaded as a relative!
42
+ #
43
+ # Possible sollutions:
44
+ # 1) keep a list of objects that need to be saved in A so that B gets
45
+ # saved when A is saved althouh the relationship has ended.
46
+ # 2) save B as it is removed from the relationship with A.
47
+ #
48
+ # I belive 1) to be the better solution as it doesn't prematurely save B
49
+ # in cases where you don't want the changes you do to be saved to the
50
+ # database.
51
+ # MAKE SURE: that the object that is added doesn't have the same id
52
+ # and revision number ass the object that is replaced. That happens
53
+ # when a class loads its belongs_to relations! In cases like that
54
+ # the old class should NOT be tracked and saved later!
55
+ #
56
+ $:.unshift File.dirname(__FILE__)
57
+ require 'persistable/has_many_relations_array'
58
+ require 'persistable/meta_classes'
59
+ require 'persistable/overloaded_methods'
60
+
1
61
  module CouchObject
2
- module Persistable
3
- def self.included(klazz)
4
- klazz.extend(ClassMethods)
5
- end
6
-
62
+ module Persistable
63
+
7
64
  module ClassMethods
8
- # Get a document from +db_uri+ with +id+ as the document id
9
- def get_by_id(db_uri, id)
10
- raise NoFromCouchMethodError unless respond_to?(:from_couch)
65
+ #
66
+ # Loads a document from the database
67
+ #
68
+ # Aliases:
69
+ # * +get+
70
+ #
71
+ # Takes:
72
+ # * +id+: the ID of the document that should be loaded
73
+ # * +db_uri+: the uri to the database. Is optional if the database has
74
+ # been defined on class level:
75
+ #
76
+ # class SomeClass
77
+ # include CouchObject::Persistable
78
+ # database 'http://localhost:5984'
79
+ # end
80
+ #
81
+ # Returns:
82
+ # * a fully initialized class of type self
83
+ #
84
+ # Raises:
85
+ # * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+
86
+ # is blank AND has not been set on class level
87
+ # * CouchObject::Errors::DocumentNotFound if the document doesn't
88
+ # exist or has been deleted
89
+ #
90
+ def get_by_id(id, db_uri = self.location)
91
+ # Raises an error if the location variable hasn't been set
92
+ raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri
93
+
11
94
  db = CouchObject::Database.open(db_uri)
12
- response = db.get(id)
13
- self.send(:from_couch, response["attributes"])
95
+ response = JSON.parse(db.get(id).body)
96
+
97
+ if response["error"]
98
+ case response["reason"]
99
+ when "deleted"
100
+ raise CouchObject::Errors::DocumentNotFound, "The document has been deleted"
101
+ else
102
+ raise CouchObject::Errors::DocumentNotFound, "The document could not be found"
103
+ end
104
+ end
105
+
106
+ # creates a new object and initialize all its sub objects
107
+ new_object = couch_load_object(response)
108
+
109
+ # set the storage location it was loaded from so it can be saved
110
+ # back directly without having to supply the db_uri again
111
+ new_object.instance_variable_set("@location", db_uri)
112
+
113
+ # return the new couch object
114
+ new_object
115
+ end
116
+
117
+ #
118
+ # Alias for get_by_id
119
+ #
120
+ alias get get_by_id
121
+
122
+ #
123
+ # Takes, returns and raises the same things as +get_by_id+
124
+ #
125
+ # Creates a new object that is forced into smart save mode
126
+ # although the class it is stemming from might not have smart
127
+ # saving enabled.
128
+ #
129
+ def get_with_smart_save(id, db_uri = self.location)
130
+
131
+ new_object = self.get_by_id(id, db_uri)
132
+
133
+ # Force it into smart save mode
134
+ new_object.couch_force_smart_save
135
+
136
+ # Initialize the original state.
137
+ new_object.couch_set_initial_state
138
+
139
+ new_object
140
+ end
141
+
142
+ #
143
+ # Loads all document from a given view from the database
144
+ #
145
+ # Takes:
146
+ # * +view+ (string): the name of the view to call
147
+ # * [+params+] (hash): a hash of URL query arguments supported
148
+ # by couchDB. If omitted it defaults to not use a key
149
+ # and not update the view.
150
+ # Additionally the +db_uri+ can be set as a parameter if
151
+ # it hasn't been defined at class level.
152
+ #
153
+ # Example:
154
+ #
155
+ # AppleTree.get_from_view("foo_view",
156
+ # { :db_uri => "http://localhost:5984/mydb",
157
+ # :update => false, :key => "bar"}) => Array
158
+ #
159
+ # Returns:
160
+ # * a array of initialized classes
161
+ # (if the view includes the documents full content)
162
+ #
163
+ # Raises:
164
+ # * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+
165
+ # is blank AND has not been set on class level
166
+ # * CouchObject::Errors::MissingView if the view doesn't exist
167
+ #
168
+ def get_from_view( view, params = {:key => nil,
169
+ :update => true,
170
+ :db_uri => self.location})
171
+
172
+ # Raise an error if the location variable hasn't been set
173
+ db_uri = params[:db_uri] || self.location
174
+ raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri
175
+
176
+ db = CouchObject::Database.open(db_uri)
177
+
178
+ params.delete(:db_uri)
179
+
180
+ #Create a querystring with the parameters passed inn
181
+ querystring = "?"
182
+ params.each_pair do |key, value|
183
+ querystring += \
184
+ "#{key}=#{Utils.encode_querystring_parameter(value)}&"
185
+ end
186
+ querystring = querystring[0...-1]
187
+
188
+ view_with_parameters = view + querystring
189
+
190
+ objects_to_return = []
191
+
192
+ response = JSON.parse(db.get(view_with_parameters).body)
193
+
194
+ raise CouchObject::Errors::MissingView, \
195
+ "The view '#{view}' doesn't exist on the server" \
196
+ if response["error"] == "not_found"
197
+
198
+ raise CouchObject::Errors::CouchDBError, \
199
+ "CouchDB returned and error and described the problem as #{response['reason']}. \n" + \
200
+ "There might be something wrong with one of your views, or it might be missing!" \
201
+ if response["error"]
202
+
203
+ response["rows"].each do |params_for_object|
204
+ objects_to_return << couch_load_object(params_for_object["value"])
205
+ end
206
+
207
+ objects_to_return
208
+
209
+ end
210
+
211
+ protected
212
+ #
213
+ # This recursive method initializes new instances of self and
214
+ # makes sure all sub classes are also initialized.
215
+ #
216
+ # Takes:
217
+ # * A hash of parameters loaded from CoachDB
218
+ #
219
+ # Returns:
220
+ # * An fully initialized object
221
+ #
222
+ # Raises:
223
+ # * Does currently not raise any error
224
+ #
225
+ def couch_load_object(parameters)
226
+
227
+ # Getting the values that shouldn't be passed on to the initializer
228
+ id = parameters["_id"]
229
+ revision = parameters["_rev"]
230
+ created_at = parameters["created_at"]
231
+ updated_at = parameters["updated_at"]
232
+ class_type = parameters["class"]
233
+ belongs_to = parameters["belongs_to"]
234
+
235
+ new_object_from_couch = nil
236
+
237
+ # Two possible routes:
238
+ # * the class has implemented a from_couch class method, in which
239
+ # case we use it, or
240
+ # * there is no from_couch method, so we just add the attributes
241
+ # as instance variables if they aren't part of a class...
242
+ if eval("#{class_type}.respond_to?(:from_couch)")
243
+ new_object_from_couch = eval("#{class_type}.send(:from_couch," \
244
+ " parameters[\"attributes\"])")
245
+ else
246
+ new_object_from_couch = eval("#{class_type}.new")
247
+ unless parameters["attributes"] == nil
248
+ parameters["attributes"].each_key do |key|
249
+ # Check if the key value pair is a class
250
+ if parameters["attributes"][key].class == Hash && \
251
+ parameters["attributes"][key]["class"] != nil
252
+
253
+ new_object_from_couch.instance_variable_set("@#{key}", \
254
+ self.couch_load_object(parameters["attributes"][key]))
255
+ else
256
+ # Add the value to the class
257
+ new_object_from_couch.
258
+ instance_variable_set("@#{key}", \
259
+ parameters["attributes"][key])
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ # Sets couch_object related values
266
+ new_object_from_couch.
267
+ instance_variable_set("@revision", revision) if revision
268
+
269
+ new_object_from_couch.instance_variable_set("@id", id) if id
270
+
271
+ new_object_from_couch.instance_variable_set("@created_at", \
272
+ created_at) if eval("#{class_type}." \
273
+ "couch_object_timestamp_on_create?")
274
+
275
+ new_object_from_couch.instance_variable_set("@updated_at", \
276
+ updated_at) if eval("#{class_type}." \
277
+ "couch_object_timestamp_on_update?")
278
+
279
+ new_object_from_couch.
280
+ instance_variable_set("@belongs_to", belongs_to) \
281
+ if belongs_to
282
+
283
+ new_object_from_couch.couch_set_initial_state
284
+
285
+ # Returns the new object
286
+ new_object_from_couch
14
287
  end
15
288
  end
16
289
 
17
- # Save the object to +db_uri+
18
- def save(db_uri)
19
- db = CouchObject::Database.open(db_uri)
20
- response = db.post("", self.to_json)
21
- unless response.empty?
22
- @id = response["_id"]
290
+ public
291
+
292
+ #
293
+ # Accessors for instance variables specific to the persistable
294
+ # CouchObject
295
+ #
296
+ attr_reader :id,
297
+ :revision,
298
+ :updated_at,
299
+ :created_at
300
+
301
+ #
302
+ # Returns:
303
+ # * +true+ if the object hasn't been saved
304
+ # * +false+ if the object has previously been stored or is loaded from the
305
+ # document store
306
+ #
307
+ def new?
308
+ id.nil? || revision.nil?
309
+ end
310
+
311
+ #
312
+ # Stores the initial value of the instance to a variable
313
+ # for later reference by the +unsaved_changes?+ method
314
+ #
315
+ def couch_set_initial_state
316
+ # For the unsaved_changes? instance method to work, we have to
317
+ # supply a snapshot of what the fresh object looked like.
318
+ # BUT ONLY if the user has activated the smart_save option
319
+ if use_smart_save
320
+
321
+ @couch_initial_load = true
322
+ @couch_object_original_state = to_json
323
+ @couch_initial_load = false
324
+
23
325
  end
24
- response
25
326
  end
327
+
328
+ #
329
+ # Forces the instance object into smart save mode
330
+ #
331
+ def couch_force_smart_save
332
+ def self.use_smart_save
333
+ true
334
+ end
335
+ end
26
336
 
27
- # Is this a new unsaved object?
28
- def new?
29
- id.nil?
337
+ #
338
+ # Saves the object to the db_uri supplied, or if not set, to the
339
+ # location the object has previously been saved to.
340
+ #
341
+ # Takes:
342
+ # * +db_uri+ as string which is the location of the database:
343
+ # Example: http://localhost:5984/mydb
344
+ #
345
+ # Raises:
346
+ # * CouchObject::Errors::NoDatabaseLocationSet error
347
+ # if the object doesn't have a previously set location and
348
+ # the +db_uri+ is nil
349
+ #
350
+ # Returns:
351
+ # * Hash with the id and revision:
352
+ # {:id => "1234", :revision => "ABC123"}
353
+ #
354
+ # Sub methods might raise:
355
+ # * CouchObject::Errors::DatabaseSaveFailed if the save fails
356
+ #
357
+ def save(db_uri = location)
358
+ # if the location hasn't been set, set it
359
+ @location ||= db_uri
360
+
361
+ # Raises an error if the location variable hasn't been set
362
+ raise CouchObject::Errors::NoDatabaseLocationSet unless location
363
+
364
+ # If it's belongs_to relationships haven't been saved
365
+ # then it has be done first.
366
+ # Saving the master will also automatically save the child
367
+ performed_save = false
368
+
369
+ # If the belongs_to relationships haven't already been loaded,
370
+ # there is reason to believe that:
371
+ # * The object is new and doesn't have any relation set
372
+ # * The object already knows about it's belongs_to relations
373
+ # and doesn't need aditional information about them for saving
374
+ # We therefore deactivate the loading of belongs_to relations
375
+ original_state_of_load_belongs_to_relations = \
376
+ @do_not_load_belongs_to_relations
377
+ @do_not_load_belongs_to_relations = true
378
+
379
+ belongs_to.each do |what_it_belongs_to|
380
+ master_class = self.send(what_it_belongs_to)
381
+ unless master_class == nil || !master_class.new?
382
+ master_class.save
383
+ performed_save = true
384
+ end
385
+ end
386
+
387
+ # Reset the do_not_load_belongs_to_relations variable to its
388
+ # original state
389
+ @do_not_load_belongs_to_relations = \
390
+ original_state_of_load_belongs_to_relations
391
+
392
+ # If none of the master classes were saved, meaning they weren't new
393
+ # or didn't exist, then we have to manually save this object.
394
+ couch_perform_save unless performed_save
395
+
396
+ {:id => @id, :revision => @revision}
397
+
398
+ end
399
+ protected
400
+ def couch_perform_save
401
+ perform_callback(:before_save)
402
+
403
+ # Go... action
404
+ the_return_value = new? ? couch_create : couch_update
405
+
406
+ # Save all the has_many relations
407
+ # But only the has_many relations that have already
408
+ # been loaded! No need to load the relations from
409
+ # the db to save them back again!
410
+ state_before_wants_to_load_relations = @do_not_load_has_many_relations
411
+ @do_not_load_has_many_relations = true
412
+
413
+ # We thread the save process in case the relations do
414
+ # some funky time consuming stuff in their call backs
415
+ threads = []
416
+ has_many.each do |thing_it_has_many_of|
417
+ self.send(thing_it_has_many_of).each do |related_object|
418
+ threads << Thread.new(related_object) do |object_to_save|
419
+ object_to_save.save(location)
420
+ end
421
+ end
422
+ end
423
+ threads.each {|thr| thr.join}
424
+
425
+ # Save all the has_one relations
426
+ has_one.each do |thing_it_has_one_of|
427
+ related_object = self.send(thing_it_has_one_of)
428
+
429
+ unless related_object == nil
430
+ related_object.save(location)
431
+ end
432
+ end
433
+
434
+ # Reset the do_not_load_has_many_relations variable
435
+ # to it's original state
436
+ @do_not_load_has_many_relations = state_before_wants_to_load_relations
437
+
438
+ perform_callback(:after_save)
439
+
440
+ return the_return_value
441
+ end
442
+
443
+ #
444
+ # Saves the class as a new document in the database
445
+ #
446
+ # Returns:
447
+ # * CouchObject::Response
448
+ #
449
+ # Raises:
450
+ # * CouchObject::Errors::DatabaseSaveFailed if the save fails
451
+ #
452
+ def couch_create
453
+ perform_callback(:before_create)
454
+
455
+ db = CouchObject::Database.open(location)
456
+
457
+ json_value = self.to_json
458
+
459
+ unless (response = db.post("", json_value)).to_document["error"]
460
+ response_document = response.to_document
461
+ @id = response_document.id
462
+ @revision = response_document.revision
463
+ @created_at = Time.now if self.
464
+ class::couch_object_timestamp_on_create?
465
+ @updated_at = Time.now if self.
466
+ class::couch_object_timestamp_on_update?
467
+
468
+ # If the user has activated smart_save, we should set the state
469
+ # to the new contents of this instance!
470
+ @couch_object_original_state = json_value if use_smart_save
471
+
472
+ perform_callback(:after_create)
473
+
474
+ # Returns a hash with the ID and Revision
475
+ {:id => @id, :revision => @revision}
476
+
477
+ else
478
+
479
+ raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
480
+ "couldn't be created.\n" + \
481
+ "CouchDB reported: #{response.to_document["error"]}"
482
+
483
+ end
484
+ end
485
+
486
+ #
487
+ # Updates a document in the database based on the id and revision
488
+ #
489
+ # Returns:
490
+ # * CouchObject::Response
491
+ #
492
+ # Raises:
493
+ # * CouchObject::Errors::DatabaseSaveFailed if the update fails
494
+ #
495
+ def couch_update
496
+
497
+ # Only save if it has unsaved changes!
498
+ if unsaved_changes?
499
+
500
+ perform_callback(:before_update)
501
+
502
+ # Please notice the following:
503
+ # The response from CouchDB only includes the revision number
504
+ # and the ID so the updated_at value in the document store
505
+ # differs from the updated_at value in the class
506
+ @updated_at = Time.now if self.
507
+ class::couch_object_timestamp_on_update?
508
+
509
+ db = CouchObject::Database.open(location)
510
+
511
+ json_value = self.to_json
512
+
513
+ unless (response = db.put(id, json_value)).to_document["error"]
514
+
515
+ @revision = response.to_document.revision
516
+
517
+ # If the user has activated smart_save, we should set the state
518
+ # to the new contents of this instance!
519
+ @couch_object_original_state = json_value if use_smart_save
520
+
521
+ perform_callback(:after_update)
522
+
523
+ # Returns a hash with the ID and Revision
524
+ {:id => @id, :revision => @revision}
525
+
526
+ else
527
+
528
+ raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
529
+ "couldn't be updated.\n" + \
530
+ "The reason might be a revision number conflict.\n" + \
531
+ "CouchDB reported: #{response.to_document["reason"]}"
532
+
533
+ end
534
+
535
+
536
+ end
537
+
30
538
  end
539
+
540
+
541
+ public
542
+ #
543
+ # Classes WITH smart_save activated:
544
+ # Any instance should be able to know if it has unsaved changes or not.
545
+ # When an instance is loaded from the DB it creates a snapshot of what
546
+ # its variables contain. Based on a comparison between the snapshot
547
+ # and the contents of the instance this method returns true or false.
548
+ #
549
+ # Classes WITHOUT smart_save activated:
550
+ # Will always return true regardless of what state it is in
551
+ #
552
+ # A new object will always return true
553
+ #
554
+ # Returns:
555
+ # * true: if it has changes that haven't been saved to the database
556
+ # * fase: if the nothing has changed since it was loaded from the
557
+ # database.
558
+ #
559
+ def unsaved_changes?
560
+ return true if new?
561
+ return true unless use_smart_save
562
+
563
+ @couch_object_original_state == self.to_json ? false : true
564
+
565
+ end
566
+
567
+ #
568
+ # Any instance should be able to delete itself
569
+ #
570
+ # Takes:
571
+ # * +db_uri+ if not set in the location variable
572
+ #
573
+ # Returns:
574
+ # * true on success
575
+ # * false on failure
576
+ #
577
+ # Note:
578
+ # * it also deletes all has_many relations from the database
579
+ # * it removes itself from object it belongs to
580
+ #
581
+ # Raises:
582
+ # * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+
583
+ # is blank AND has not been set on class level
584
+ #
585
+ def delete(db_uri = location)
586
+
587
+ perform_callback(:before_delete)
588
+
589
+ unless new?
590
+ # Raises an error if the location variable hasn't been set
591
+ raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri
592
+
593
+ db = CouchObject::Database.open(db_uri)
594
+
595
+ # Removes itself from the database
596
+ db.delete(id, revision)
597
+ end
598
+
599
+ # Remove all relations
600
+ has_many.each do |what_it_has|
601
+ self.send(what_it_has).dup.each do |related_object|
602
+ related_object.delete
603
+ end
604
+ end
605
+
606
+ # Remove the relationship with it's has many master object
607
+ belongs_to.each do |what_it_belongs_to|
608
+ self_belongs_to_classtype = what_it_belongs_to
609
+
610
+ self_belongs_to_classtype_as = self.
611
+ send("belongs_to_#{what_it_belongs_to}_as")
612
+
613
+ self_belongs_to_class = self.send(self_belongs_to_classtype) unless \
614
+ self_belongs_to_classtype.nil?
615
+
616
+ self_belongs_to_class.end_relationsship_with(self,
617
+ self_belongs_to_classtype_as) unless self_belongs_to_class.nil?
618
+
619
+ # Remove the relationship with it's belongs_to master from itself
620
+ remove_call = "#{self_belongs_to_classtype.to_s}" + \
621
+ "_without_call_back="
622
+ self.send(remove_call.to_sym, nil) if self_belongs_to_classtype
623
+
624
+ end
625
+
626
+ # Reset itself
627
+ @id = nil
628
+ @revision = nil
629
+ @location = nil
630
+
631
+ perform_callback(:after_delete)
632
+
633
+ true
31
634
 
32
- # the Couch document id of this object
33
- def id
34
- @id
35
635
  end
36
636
 
37
- # serializes this object, based on its #to_couch method, into JSON
637
+ #
638
+ # Breaks relations if existing
639
+ #
640
+ # Takes:
641
+ # * +undesired_object+ as a reference to the object the
642
+ # relationship should be broken with
643
+ # * +which_is_stored_as+ (string) which is what the relation
644
+ # is stored as.
645
+ #
646
+ def end_relationsship_with(undersired_object, which_is_stored_as)
647
+ self.send(which_is_stored_as.to_sym).perform_remove(undersired_object)
648
+ end
649
+
650
+ #
651
+ # Sets the location variable manually
652
+ #
653
+ # Takes:
654
+ # * +db_uri+ as string which is the location of the database:
655
+ # Example: http://localhost:5984/mydb
656
+ #
657
+ def set_location=(db_uri)
658
+ @location = db_uri == "" ? nil : db_uri
659
+ end
660
+ alias set_storage_location= set_location=
661
+
662
+ #
663
+ # serializes this object into JSON
664
+ #
665
+ # Returns:
666
+ # * The values of the class in json format
667
+ #
668
+ # Example
669
+ # {"class":"Bike","attributes":{"wheels":2}}
670
+ #
38
671
  def to_json
39
- raise NoToCouchMethodError unless respond_to?(:to_couch)
40
- {"class" => self.class, "attributes" => self.to_couch}.to_json
672
+ parameters = {}
673
+
674
+ parameters["class"] = self.class
675
+
676
+ if respond_to?(:to_couch)
677
+ parameters["attributes"] = self.to_couch
678
+ else
679
+ # Find all the instance variables and add them to the
680
+ # attributes parameter
681
+ p_attributes = {}
682
+ instance_variables = \
683
+ self.instance_variables - ["@location",
684
+ "@created_at",
685
+ "@updated_at",
686
+ "@id",
687
+ "@revision",
688
+ "@do_not_load_has_many_relations",
689
+ "@do_not_load_belongs_to_relations",
690
+ "@couch_object_original_state",
691
+ "@couch_initial_load",
692
+ "@belongs_to"]
693
+
694
+ # We also have to remove all the objects that are related
695
+ # through belongs_to and has_many relations. They have to be called
696
+ # saved separately
697
+ has.each do |thing_it_has|
698
+ instance_variables = instance_variables - \
699
+ ["@couch_object_#{thing_it_has.to_s}"]
700
+ end
701
+
702
+ belongs_to.each do |things_it_belongs_to|
703
+ instance_variables = instance_variables - \
704
+ ["@couch_object_#{things_it_belongs_to.to_s}"]
705
+ end
706
+
707
+ instance_variables.each do |var|
708
+ p_attributes[var[1..(var.length)]] = self.instance_variable_get(var)
709
+ end
710
+ parameters["attributes"] = p_attributes
711
+ end
712
+
713
+ parameters["updated_at"] = Time.now \
714
+ if self.class::couch_object_timestamp_on_update?
715
+
716
+ unless new?
717
+ parameters["_id"] = id
718
+ parameters["_rev"] = revision
719
+ parameters["created_at"] = created_at \
720
+ if self.class::couch_object_timestamp_on_create?
721
+ else
722
+ parameters["created_at"] = Time.now \
723
+ if self.class::couch_object_timestamp_on_create?
724
+ end
725
+
726
+ # If it is in belongs_to relationship(s), then that fact
727
+ # has to be stored in the database
728
+ if belongs_to != [] and !@couch_initial_load # the couch_initial_load
729
+ # is to get the initial
730
+ # state of the object
731
+ # without loading the
732
+ # belongs_to relations
733
+ # which would start an
734
+ # infinite loop.
735
+ # NOTE: this is only for
736
+ # cases where smart_save
737
+ # has been activated.
738
+
739
+ # No reason to load the belongs_to relations if they haven't
740
+ # already been loaded from the database!
741
+ original_state_of_load_belongs_to_relations = \
742
+ @do_not_load_belongs_to_relations
743
+ @do_not_load_belongs_to_relations = true
744
+
745
+ what_it_belongs_to = {}
746
+ self.send(:belongs_to).each do |relation|
747
+ as_what = self.send("belongs_to_#{relation}_as")
748
+ object_it_belongs_to = self.send(relation)
749
+
750
+ # Unless the relation is unset, in which case it will be of
751
+ # type NilClass, set it.
752
+ what_it_belongs_to[as_what.to_s] = object_it_belongs_to.id || "new" \
753
+ unless object_it_belongs_to.class == NilClass
754
+ end
755
+
756
+ # Reset the value so they are loaded the next time when needed
757
+ # if that is what the user wants.
758
+ @do_not_load_belongs_to_relations = \
759
+ original_state_of_load_belongs_to_relations
760
+
761
+
762
+ # We have to make sure the @belongs_to variable contains all changes
763
+ # and all the original values for the keys that haven't changed/
764
+ # relations that haven't been loaded
765
+ original_belongs_to = @belongs_to || {}
766
+ @belongs_to = what_it_belongs_to
767
+ times_through = 1
768
+
769
+ # LOOP 1... see below for problem description
770
+ original_belongs_to.each_pair do |key, value|
771
+ times_through += 1
772
+ @belongs_to[key.to_s] = value unless @belongs_to[key.to_s]
773
+ end
774
+
775
+ # FIXME:
776
+ # Now... this is a really hacky way to solve this problem
777
+ # and should be improved... Feel free to come up with sollutions
778
+ # Case:
779
+ # If it has a belongs_to relationship that is new and therefore
780
+ # doesn't have an ID it would normally be written in the @belongs_to
781
+ # variable as nil. The problem is that if self has previously
782
+ # been saved with another parent object this ID would still come
783
+ # through in the @belongs_to variable updater (see "LOOP 1" above).
784
+ # We therefore assign the ID "new" to all unsaved relations, which we
785
+ # now have to nilify. If we don't the smart save wont work for this
786
+ # type of cases.
787
+
788
+ end
789
+
790
+ parameters["belongs_to"] = @belongs_to
791
+ parameters.delete("belongs_to") if @belongs_to == {} or @belongs_to == nil
792
+
793
+ # if @couch_initial_load && @belongs_to
794
+
795
+ begin
796
+ parameters.to_json
797
+ rescue JSON::GeneratorError
798
+ # All strings aren't encoded properly, so we have to force them into
799
+ # UTF-8.
800
+ # FIXME: The kconv library has some weird artefacts though where
801
+ # a lot of Norwegian (Scandianavian?) letters get turned into
802
+ # asian characters of some sort!
803
+ CouchObject::Utils::decode_strings(parameters).to_json
804
+ end
805
+
806
+ end
807
+
808
+
809
+
810
+ #
811
+ # Sometimes you might want to add an object to
812
+ # a has_many relation without interacting with the other relations at all.
813
+ # In cases like that, when loading all the relations would just
814
+ # cause unnecessary traffic to the database, you can tell the object
815
+ # not to load it has_many relations using this method
816
+ #
817
+ def do_not_load_has_many_relations
818
+ @do_not_load_has_many_relations = true
819
+ end
820
+ def do_load_has_many_relations
821
+ @do_not_load_has_many_relations = false
822
+ end
823
+ alias do_not_load_has_one_relations do_not_load_has_many_relations
824
+ alias do_load_has_one_relations do_load_has_many_relations
825
+ alias do_not_load_has_one_relation do_not_load_has_many_relations
826
+ alias do_load_has_one_relation do_load_has_many_relations
827
+
828
+ #
829
+ # If you need to access the belongs_to variable without loading the
830
+ # relation if it hasn't already been loaded, you can call the instance
831
+ # method +do_not_load_belongs_to_relations+. To reactivate loading so
832
+ # the relation is loaded the next time it is needed, call the instance
833
+ # method +do_load_belongs_to_relations+.
834
+ #
835
+ def do_not_load_belongs_to_relations
836
+ @do_not_load_belongs_to_relations = true
41
837
  end
838
+ def do_load_belongs_to_relations
839
+ @do_not_load_belongs_to_relations = false
840
+ end
841
+ alias do_not_load_has_many_relation do_not_load_has_many_relations
842
+ alias do_load_belongs_to_relation do_load_belongs_to_relations
843
+
844
+
845
+ protected
846
+ #
847
+ # Loads has_many relations
848
+ #
849
+ def couch_load_has_many_relations(which_relation)
850
+ # If it is a new and unsaved object it wont have
851
+ # relations in the DB. Return a blank array
852
+ results = CouchObject::Persistable::HasManyRelation.new(self)
853
+
854
+ # If it is loading a has_one relation, the relation name has to
855
+ # be changed
856
+ which_relation = which_relation[8..-1] \
857
+ if which_relation[0..7] == "has_one_"
858
+
859
+ return results if new? || @do_not_load_has_many_relations
42
860
 
43
- class NoToCouchMethodError < StandardError
44
- def message
45
- "You need to define a #to_couch method that returns a hash of the " +
46
- "attributes you want to persist"
861
+ view_name = "couch_object_has_many_relations"
862
+
863
+ begin
864
+
865
+ new_objects = self.class.
866
+ get_from_view("_view/#{view_name}/related_documents", \
867
+ {:key => [self.id, which_relation],
868
+ :db_uri => location} )
869
+
870
+ # Disable the call back function of HasManyRelations so we don't
871
+ # get an infinite loop
872
+ results.disable_call_back_on_add
873
+
874
+ new_objects.each do |new_object|
875
+ results << new_object
876
+ end
877
+
878
+ # Reanable the call back function again so it works like normal
879
+ # from now on
880
+ results.enable_call_back_on_add
881
+
882
+ rescue CouchObject::Errors::MissingView, CouchObject::Errors::CouchDBError
883
+
884
+ # The view doesn't exist. It means this is the first
885
+ # time this script is used for a given database, or the user
886
+ # has deleted the view
887
+ view_code_query = JSON.unparse(
888
+ {
889
+ "_id" => "_design/#{view_name}",
890
+ "language" => "text/javascript",
891
+ "views" =>
892
+ {
893
+ "related_documents" => "function(doc){if (doc.belongs_to){" + \
894
+ "for (var i in doc.belongs_to) {map([doc.belongs_to[i],i], " + \
895
+ "doc);}}}"
896
+ }
897
+ }
898
+ )
899
+
900
+ db = CouchObject::Database.open(location)
901
+
902
+ if (response = db.put("_design%2F#{view_name}", \
903
+ view_code_query))
904
+ results = couch_load_has_many_relations(which_relation)
905
+ else
906
+ raise CouchObject::Errors::StandardError, "Couldn't create the view..."
907
+ end
47
908
  end
48
- alias_method :to_s, :message
909
+
910
+ return results
49
911
  end
50
912
 
51
- class NoFromCouchMethodError < StandardError
52
- def message
53
- "You need to define a from_couch(attrs) class method that maps attrs " +
54
- "to your class instance"
913
+ #
914
+ # Loads the belongs_to relation if the class has a previously
915
+ # save relation
916
+ #
917
+ def couch_load_belongs_to_relation(that_is_called)
918
+
919
+ return nil if new? || @do_not_load_belongs_to_relations
920
+
921
+ # If it doesn't have a belongs to ID then there is no
922
+ # related object in the database
923
+ if @belongs_to && @belongs_to[that_is_called]
924
+
925
+ # If it is a new and unsaved object it wont have
926
+ # relations in the DB. Return a blank array
927
+ return nil if new?
928
+
929
+ # Raises an error if the location variable hasn't been set
930
+ raise CouchObject::Errors::NoDatabaseLocationSet unless location
931
+
932
+ db = CouchObject::Database.open(location)
933
+
934
+ class_self_belongs_to = self.
935
+ class.get_by_id(@belongs_to[that_is_called])
936
+
937
+ # We have to add self to the other end of the relation
938
+ # Adds itself to the relationship
939
+ masters_relation = class_self_belongs_to.send(that_is_called)
940
+
941
+ if masters_relation.class == CouchObject::Persistable::HasManyRelation
942
+ # it is a has_many relations
943
+ # remove the object in the relation that is the freshly loaded
944
+ # copy of self, and then add self instead.
945
+ masters_relation.each do |has_one|
946
+ if has_one.id == self.id && has_one.revision == self.revision
947
+ masters_relation.perform_remove(has_one)
948
+ break
949
+ end
950
+ end
951
+ masters_relation.disable_call_back_on_add
952
+ masters_relation << self
953
+ masters_relation.enable_call_back_on_add
954
+ else
955
+ # it is a has_one relation, just add itself
956
+ class_self_belongs_to.send("#{that_is_called}=".to_sym, self)
957
+ end
958
+
959
+ return class_self_belongs_to
960
+
961
+ else
962
+ nil
55
963
  end
56
- alias_method :to_s, :message
57
964
  end
965
+
966
+ #
967
+ # Performs callbacks before and after these events:
968
+ # * create
969
+ # * update
970
+ # * save
971
+ # * delete
972
+ #
973
+ def perform_callback(the_callback)
974
+ self.send(the_callback) if self.respond_to?(the_callback)
975
+ end
58
976
  end
59
977
  end