couchobject 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -0,0 +1,209 @@
1
+ module CouchObject
2
+ module Persistable
3
+ #####
4
+ #
5
+ # Overloaded methods
6
+ #
7
+ #####
8
+
9
+ #
10
+ # When saved, clones and dupes are stored as separate documents from
11
+ # the object it originated from
12
+ #
13
+ # Returns:
14
+ # * A new instance of itself where the ID and revision number
15
+ # is set to nil
16
+ # * the new instance shares the same storage location as the object
17
+ # it originates from.
18
+ #
19
+ alias couch_object_origianl_clone clone
20
+ def clone
21
+ new_clone = self.couch_object_origianl_clone
22
+ new_clone.instance_variable_set("@id", nil)
23
+ new_clone.instance_variable_set("@revision", nil)
24
+
25
+ # return the new clone
26
+ new_clone
27
+ end
28
+ def dup
29
+ clone
30
+ end
31
+
32
+ #
33
+ # Equality is checked for using the following rules:
34
+ # * if the object types do not match the result is false
35
+ # * if the object_id of the class sent as an argument is the same
36
+ # as the object_id of self, equal returns true.
37
+ # * if the two objects have a different class type it fails
38
+ # as a result of the object_id test above which can't be true
39
+ # * fails if the object_id or revision numbers are different
40
+ # * it fails if one or both of the objects are new and they don't share
41
+ # the object_id, because in that case they will be stored as
42
+ # separate documents in the database
43
+ # * is true if all instance_variables are the same
44
+ #
45
+ # Takes:
46
+ # * +other_object+ which is any object one wishes to compare self to
47
+ #
48
+ # Returns:
49
+ # * true or false
50
+ #
51
+ def ==(other_object)
52
+ return false unless self.class == other_object.class
53
+ return true if self.object_id == other_object.object_id
54
+ return false if self.id != other_object.id || \
55
+ self.revision != other_object.revision
56
+ return false if self.new? || other_object.new?
57
+
58
+ # All relations should be loaded so that the comparison can be realistic
59
+ has.each do |what_is_has|
60
+ self.send(what_is_has)
61
+ other_object.send(what_is_has) if other_object.
62
+ respond_to?(what_is_has)
63
+ end
64
+ belongs_to.each do |what_it_belongs_to|
65
+ self.send(what_it_belongs_to)
66
+ other_object.send(what_it_belongs_to) if other_object.
67
+ respond_to?(what_it_belongs_to)
68
+ end
69
+
70
+ self.instance_variables.each do |var|
71
+ return false if eval(var).to_s != \
72
+ other_object.instance_variable_get(var).to_s
73
+ end
74
+
75
+ # has to be true because else it would already have failed :)
76
+ true
77
+ end
78
+
79
+ #
80
+ # Goal:
81
+ # The goal is to return true for the object which has the newest
82
+ # representation in the database.
83
+ #
84
+ # Requires:
85
+ # * that both objects have timestamps
86
+ #
87
+ # Returns:
88
+ # * false if they are equal objects.
89
+ # * true if self has been saved while the other_object has not.
90
+ # * false if self hasn't been saved but the other_object has
91
+ # * true if both have been saved and self has been saved more recently.
92
+ # * false if both have been saved but the other object more recently.
93
+ #
94
+ # Takes:
95
+ # * +other_object+ to compare size with
96
+ #
97
+ # Raises:
98
+ # * CantCompareSize when trying to compare two object that have not
99
+ # been saved or saved but do not have timestamps
100
+ #
101
+ # Returns:
102
+ # * true or false
103
+ #
104
+ def >(other_object)
105
+ return false if self == other_object
106
+ unless self.new? && other_object.new?
107
+ if self.class.couch_object_timestamp_on_update? and \
108
+ other_object.class.couch_object_timestamp_on_update?
109
+ return ((self.updated_at || 0).to_i \
110
+ > (other_object.updated_at || 0).to_i) \
111
+ ? true : false
112
+ elsif self.class.couch_object_timestamp_on_create? and \
113
+ other_object.class.couch_object_timestamp_on_create?
114
+ return ((self.created_at || 0).to_i \
115
+ > (other_object.created_at || 0).to_i) \
116
+ ? true : false
117
+ else
118
+ # Both or one of them does not have a timestamp,
119
+ # so we can't really compare them
120
+ raise CouchObject::Errors::CantCompareSize
121
+ end
122
+ else
123
+ raise CouchObject::Errors::CantCompareSize
124
+ end
125
+
126
+ end
127
+ def <(other_object)
128
+ other_object > self
129
+ end
130
+
131
+ #
132
+ # <= and >= Uses the results from the methods ==, < and >
133
+ # to determine the result
134
+ #
135
+ # Takes:
136
+ # * +other_object+ to compare size with
137
+ #
138
+ # Raises:
139
+ # * CantCompareSize when trying to compare two object that have not
140
+ # been saved or saved but do not have timestamps and that are not equal
141
+ #
142
+ # Returns:
143
+ # * true or false
144
+ #
145
+ def <=(other_object)
146
+ self == other_object || self < other_object ? true : false
147
+ end
148
+ def >=(other_object)
149
+ other_object <= self
150
+ end
151
+
152
+ #
153
+ # Returns a prettyfied string version of the object
154
+ #
155
+ # def inspect
156
+ # has_many_relations = []
157
+ #
158
+ #
159
+ # has_many.each do |what_it_has|
160
+ # related_objects = []
161
+ # objects_it_has = self.send(what_it_has)
162
+ # unless objects_it_has.size == 0
163
+ # objects_it_has.each do |obj|
164
+ # if obj.new?
165
+ # related_objects << "<#{obj.class}[unsaved]>"
166
+ # else
167
+ # related_objects << "<#{obj.class}[#{obj.id}/#{obj.revision}]>"
168
+ # end
169
+ # end
170
+ # end
171
+ #
172
+ # what_it_has_string = " #{what_it_has.to_s.capitalize}"
173
+ # what_it_has_string += ": (#{related_objects.join(", ")})" \
174
+ # unless related_objects.size == 0
175
+ #
176
+ # has_many_relations << what_it_has_string
177
+ #
178
+ # end
179
+ #
180
+ # has_many_string = ""
181
+ # belongs_to_string = ""
182
+ #
183
+ # has_many_string = " Has many: #{has_many_relations.join(",")}" unless has_many_relations.size == 0
184
+ #
185
+ # if belongs_to
186
+ # object_it_belongs_to = self.send(belongs_to)
187
+ # if object_it_belongs_to
188
+ # if object_it_belongs_to.new?
189
+ # belongs_to_string = " Belongs to: " + \
190
+ # "#{belongs_to.to_s.capitalize}" + \
191
+ # "[unsaved]"
192
+ # else
193
+ # belongs_to_string = " Belongs to:" + \
194
+ # " #{belongs_to.to_s.capitalize}" + \
195
+ # "[#{object_it_belongs_to.id}/#{object_it_belongs_to.revision}]"
196
+ # end
197
+ # end
198
+ # end
199
+ #
200
+ # if new?
201
+ # "<#{self.class}[unsaved]#{belongs_to_string}#{has_many_string}>"
202
+ # else
203
+ # "<#{self.class}[#{self.id}/#{self.revision}]" + \
204
+ # "#{belongs_to_string}#{has_many_string}>"
205
+ # end
206
+ #
207
+ # end
208
+ end
209
+ end
@@ -3,7 +3,7 @@ require "net/http"
3
3
  module CouchObject
4
4
  class Server
5
5
  # Create a new Server object, +uri+ is the full URI of the server,
6
- # eg. "http://localhost:8888"
6
+ # eg. "http://localhost:5984"
7
7
  def initialize(uri)
8
8
  @uri = URI.parse(uri)
9
9
  @host = @uri.host
@@ -9,5 +9,49 @@ module CouchObject
9
9
  end.join("/")
10
10
  end
11
11
  end
12
+
13
+ def self.encode_querystring_parameter(parameter)
14
+ case parameter.class.to_s
15
+ when "Array"
16
+ # [12123,"ASD"] => [%2212123%22,%22ASD%22]
17
+ "[#{(parameter.map {|p| self.fix_parameter(p) }).join(",")}]"
18
+ else
19
+ self.fix_parameter(parameter)
20
+ end
21
+ end
22
+
23
+ def self.decode_strings(input)
24
+ case input.class.to_s
25
+ when "String"
26
+ # HTMLEntities.decode_entities(input).toutf8
27
+ input.toutf8
28
+ when "Array"
29
+ input.map! do |n|
30
+ decode_strings(n)
31
+ end
32
+ when "Hash"
33
+ input.each_pair do |key, value|
34
+ input[key] = decode_strings(value)
35
+ end
36
+ else
37
+ input
38
+ end
39
+ end
40
+
41
+ protected
42
+ def self.fix_parameter(p)
43
+ case p.class.to_s.downcase
44
+ when "fixnum", "bignum", "trueclass", "falseclass"
45
+ "#{p.to_s}"
46
+ when "nilclass"
47
+ nil
48
+ else
49
+ # Sanitize forward slashes
50
+ # # / => \/
51
+ val = p.gsub(/\//, "\\/")
52
+ "%22#{val}%22"
53
+ end
54
+ end
55
+
12
56
  end
13
57
  end
@@ -1,7 +1,7 @@
1
1
  module CouchObject #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 5
4
+ MINOR = 6
5
5
  TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
@@ -1,23 +1,146 @@
1
+ require "net/http"
1
2
  require "enumerator"
2
3
 
3
4
  module CouchObject
4
5
  class View
6
+
7
+ #
8
+ # Takes:
9
+ # * db as CouchObject::Database object or a string containing the database
10
+ # uri and database name (http://localhost:5984/mydb)
11
+ # * name (string). The name of the view
12
+ # * query (JSON string). The JSON document that should be
13
+ # added to the database
14
+ #
15
+ # Returns:
16
+ # CouchObject::Reponse object instance
17
+ #
18
+ # Raises:
19
+ # * an exeption if it can't figure out the database
20
+ #
5
21
  def self.create(db, name, query)
6
- db.post("/#{db.name}/_view_#{name}", query)
22
+ _db = self.get_db(db)
23
+ # %2F => /
24
+ _db.put("_design%2F#{name}", query)
7
25
  end
8
-
26
+
27
+ #
28
+ # Takes:
29
+ # * db as CouchObject::Database object or a string containing the database
30
+ # uri and database name (http://localhost:5984/mydb)
31
+ # * name (string). The name of the view
32
+ #
33
+ # Returns:
34
+ # new CouchObject::View instance
35
+ #
36
+ # Raises:
37
+ # * an exeption if it can't figure out the database
38
+ #
9
39
  def initialize(db, name)
10
- @db = db
40
+ @db = self.class.get_db(db)
11
41
  @name = name
12
42
  end
13
43
  attr_accessor :db
14
44
 
15
45
  def name
16
- "_view_#{@name.dup}"
46
+ "_view/#{@name.dup}"
47
+ end
48
+
49
+ #
50
+ # Returns an array of the rows returned by the view
51
+ #
52
+ # Takes:
53
+ # * [+params+] (hash): a hash of URL query arguments supported
54
+ # by couchDB. If omitted it defaults to not use a key
55
+ # and to update the view.
56
+ #
57
+ # Example:
58
+ #
59
+ # view.query({:update => false, :key => "bar"}) => Array
60
+ #
61
+ # Returns:
62
+ # * a array of rows from the view
63
+ #
64
+ # Raises:
65
+ # * CouchObject::Errors::MissingView if the view doesn't exist
66
+ #
67
+ def query(params = {})
68
+
69
+ response = raw_query(params)
70
+
71
+ rows_to_return = []
72
+ response["rows"].each do |params_for_object|
73
+ rows_to_return << params_for_object["value"]
74
+ end
75
+
76
+ rows_to_return
77
+
78
+ end
79
+
80
+ #
81
+ # Returns the response data from CouchDB for a view query without
82
+ #
83
+ # Takes:
84
+ # * [+params+] (hash): a hash of URL query arguments supported
85
+ # by couchDB. If omitted it defaults to not use a key
86
+ # and to update the view.
87
+ #
88
+ # Example:
89
+ #
90
+ # view.raw_query({:update => false, :key => "bar"}) => data
91
+ #
92
+ # Returns:
93
+ # * the response data for the view
94
+ #
95
+ # Raises:
96
+ # * CouchObject::Errors::MissingView if the view doesn't exist
97
+ #
98
+ def raw_query(params = {})
99
+
100
+ #Create a querystring with the parameters passed inn
101
+ querystring = "?"
102
+ params.each_pair do |key, value|
103
+ querystring += \
104
+ "#{key}=#{Utils.encode_querystring_parameter(value)}&"
105
+ end
106
+ querystring = querystring[0...-1]
107
+
108
+ view_with_parameters = name + querystring
109
+
110
+ response = JSON.parse(db.get(view_with_parameters).body)
111
+
112
+ raise CouchObject::Errors::MissingView, \
113
+ "The view '#{name}' doesn't exist on the server" \
114
+ if response["error"] == "not_found"
115
+
116
+ raise CouchObject::Errors::MapProcessError \
117
+ if response["error"] == "map_process_error"
118
+
119
+ raise CouchObject::Errors::CouchDBError, \
120
+ "CouchDB returned and error and described the problem as #{response['reason']}" \
121
+ if response["error"]
122
+
123
+ return response
124
+
17
125
  end
18
126
 
19
127
  def delete
20
128
  @db.delete("/#{db.name}/#{name}")
21
129
  end
22
- end
23
- end
130
+
131
+ protected
132
+ def self.get_db(db)
133
+ case db.class.to_s
134
+ when "CouchObject::Database"
135
+ db
136
+ when "String"
137
+ CouchObject::Database.open(db)
138
+ else
139
+ raise "You have to supply a database URI " + \
140
+ "or a CouchObject::Database object"
141
+ end
142
+ end
143
+
144
+ end
145
+ end
146
+
File without changes
File without changes
File without changes
File without changes
@@ -3,7 +3,7 @@ require File.dirname(__FILE__) + '/spec_helper.rb'
3
3
  describe CouchObject::Database do
4
4
  before(:each) do
5
5
  @server = mock("Couch server")
6
- @uri = "http://localhost:8888"
6
+ @uri = "http://localhost:5984"
7
7
  @response = mock("Net::HTTP::Response")
8
8
  CouchObject::Server.should_receive(:new).with(@uri).and_return(@server)
9
9
  @response.stub!(:code).and_return(200)
@@ -19,7 +19,7 @@ describe CouchObject::Database do
19
19
 
20
20
  it "should create a database" do
21
21
  @server.should_receive(:put).with("/foo", "").and_return(@response)
22
- CouchObject::Database.create!(@uri, "foo")
22
+ CouchObject::Database.create(@uri, "foo")
23
23
  end
24
24
 
25
25
  it "should delete a database" do
@@ -49,7 +49,7 @@ describe CouchObject::Database do
49
49
  end
50
50
 
51
51
  it "should lint the database name from slashes with ::open" do
52
- db = CouchObject::Database.open("http://localhost:8888/foo")
52
+ db = CouchObject::Database.open("http://localhost:5984/foo")
53
53
  class << db
54
54
  attr_accessor :dbname
55
55
  end
@@ -64,10 +64,16 @@ describe CouchObject::Database do
64
64
 
65
65
  it "should POST" do
66
66
  db = CouchObject::Database.new(@uri, "foo")
67
- @server.should_receive(:post).with("/foo/123", "postdata").and_return(@response)
67
+ @server.should_receive(:post).with("/foo/123", "postdata", "application/json").and_return(@response)
68
68
  db.post("123", "postdata")
69
69
  end
70
70
 
71
+ it "should POST with supplied ContentType header" do
72
+ db = CouchObject::Database.new(@uri, "foo")
73
+ @server.should_receive(:post).with("/foo/123", "postdata", "text/rspec").and_return(@response)
74
+ db.post("123", "postdata", "text/rspec")
75
+ end
76
+
71
77
  it "should PUT" do
72
78
  db = CouchObject::Database.new(@uri, "foo")
73
79
  @server.should_receive(:put).with("/foo/123", "postdata").and_return(@response)
@@ -76,12 +82,12 @@ describe CouchObject::Database do
76
82
 
77
83
  it "should DELETE" do
78
84
  db = CouchObject::Database.new(@uri, "foo")
79
- @server.should_receive(:delete).with("/foo/123").and_return(@response)
80
- db.delete("123")
85
+ @server.should_receive(:delete).with("/foo/123?rev=456").and_return(@response)
86
+ db.delete("123", "456")
81
87
  end
82
88
 
83
89
  it "should open a new connection from a full uri spec" do
84
- proc{ CouchObject::Database.open("http://localhost:8888/foo") }.should_not raise_error
90
+ proc{ CouchObject::Database.open("http://localhost:5984/foo") }.should_not raise_error
85
91
  end
86
92
 
87
93
  it "should know the database name" do
@@ -91,7 +97,7 @@ describe CouchObject::Database do
91
97
 
92
98
  it "should know the full uri" do
93
99
  db = CouchObject::Database.new(@uri, "foo")
94
- db.url.should == "http://localhost:8888/foo"
100
+ db.url.should == "http://localhost:5984/foo"
95
101
  end
96
102
 
97
103
  it "should load a document from id with #[]" do
@@ -142,31 +148,17 @@ describe CouchObject::Database do
142
148
  it "should store a document" do
143
149
  db = CouchObject::Database.new(@uri, "foo")
144
150
  doc = CouchObject::Document.new({"foo" => "bar"})
145
- db.should_receive(:post).with("", JSON.unparse("foo" => "bar"))
151
+
152
+ FakeRespons = Struct.new(:to_document)
153
+ FakeDocument = Struct.new(:id, :revision)
154
+ document = \
155
+ FakeDocument.new("2113826070","id"=>"594F126A80B45C8AC2298E0393E20192")
156
+ response = FakeRespons.new(document)
157
+
158
+ db.should_receive(:post).with("", JSON.unparse("foo" => "bar")).
159
+ and_return(response)
146
160
  db.store(doc)
147
161
  end
148
162
 
149
- it "should return the rows when filtering" do
150
- db = CouchObject::Database.new(@uri, "foo")
151
- rowdata = { "_rev"=>1,
152
- "_id"=>"1",
153
- "value"=> {
154
- "_id"=>"1",
155
- "_rev"=>1,
156
- "foo"=>"bar"
157
- }
158
- }
159
- resp = mock("response")
160
- resp.stub!(:body).and_return(JSON.unparse("rows" => [rowdata]))
161
- resp.stub!(:parse).and_return(resp)
162
- resp.stub!(:to_document).and_return(
163
- CouchObject::Document.new("rows" => [rowdata])
164
- )
165
- db.should_receive(:post).with("_temp_view", "proc { |doc|\n (doc[\"foo\"] == \"bar\")\n}").and_return(resp)
166
- rows = db.filter{|doc| doc["foo"] == "bar"}
167
- rows.size.should == 1
168
- rows.first["value"]["foo"].should == "bar"
169
- end
170
-
171
163
  #it "should url encode paths"
172
164
  end