langalex-couch_potato 0.1 → 0.1.1

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 (38) hide show
  1. data/README.textile +340 -0
  2. data/VERSION.yml +4 -0
  3. data/lib/couch_potato/ordering.rb +5 -9
  4. data/lib/couch_potato/persistence.rb +32 -7
  5. data/lib/couch_potato/persistence/belongs_to_property.rb +17 -3
  6. data/lib/couch_potato/persistence/callbacks.rb +9 -0
  7. data/lib/couch_potato/persistence/custom_view.rb +41 -0
  8. data/lib/couch_potato/persistence/dirty_attributes.rb +19 -0
  9. data/lib/couch_potato/persistence/external_collection.rb +31 -2
  10. data/lib/couch_potato/persistence/external_has_many_property.rb +4 -0
  11. data/lib/couch_potato/persistence/finder.rb +21 -65
  12. data/lib/couch_potato/persistence/inline_has_many_property.rb +4 -0
  13. data/lib/couch_potato/persistence/json.rb +1 -1
  14. data/lib/couch_potato/persistence/simple_property.rb +29 -1
  15. data/lib/couch_potato/persistence/view_query.rb +81 -0
  16. data/lib/couch_potato/versioning.rb +1 -1
  17. data/spec/attributes_spec.rb +35 -15
  18. data/spec/belongs_to_spec.rb +18 -0
  19. data/spec/callbacks_spec.rb +31 -12
  20. data/spec/create_spec.rb +5 -0
  21. data/spec/custom_view_spec.rb +44 -0
  22. data/spec/destroy_spec.rb +4 -0
  23. data/spec/dirty_attributes_spec.rb +82 -0
  24. data/spec/find_spec.rb +11 -3
  25. data/spec/finder_spec.rb +10 -0
  26. data/spec/has_many_spec.rb +64 -1
  27. data/spec/ordering_spec.rb +1 -0
  28. data/spec/property_spec.rb +4 -0
  29. data/spec/reload_spec.rb +4 -0
  30. data/spec/spec_helper.rb +2 -1
  31. data/spec/unit/external_collection_spec.rb +84 -0
  32. data/spec/unit/finder_spec.rb +10 -0
  33. data/spec/unit/view_query_spec.rb +10 -0
  34. data/spec/update_spec.rb +6 -0
  35. data/spec/versioning_spec.rb +1 -0
  36. metadata +21 -48
  37. data/CREDITS +0 -3
  38. data/init.rb +0 -5
data/README.textile ADDED
@@ -0,0 +1,340 @@
1
+ h2. Couch Potato
2
+
3
+ ... is a persistence layer written in ruby for CouchDB.
4
+
5
+ h3. Mission
6
+
7
+ The goal of Couch Potato is to create a migration path for users of ActiveRecord and other object relational mappers to port their applications to CouchDB. It therefore offers a basic set of the functionality provided by most ORMs and adds functionality unique to CouchDB on top.
8
+
9
+ h3. Core Features
10
+
11
+ Couch Potato is a work in progress so this list will hopefully grow over time.
12
+
13
+ * persisting objects by including the CouchPotato::Persistence module
14
+ * has_many/belongs_to relationships between persistant objects
15
+ * atomic write operations spanning multiple objects using bulk save queues
16
+ * extensive spec suite
17
+ * included versioning support via the CouchPotato::Versioning module
18
+ * included ordered lists support via the CouchPotato::Ordering module
19
+
20
+ h3. Installation
21
+
22
+ Couch Potato is hosted as a gem on github which you can install like this:
23
+
24
+ sudo gem source --add http://gems.github.com # if you haven't alread
25
+ sudo gem install langalex-couch_potato
26
+
27
+ h4. Using with your ruby application:
28
+
29
+ require 'rubygems'
30
+ gem 'couch_potato'
31
+ require 'couch_potato'
32
+ CouchPotato::Config.database_name = 'name of the db'
33
+
34
+ Alternatively you can download or clone the source repository and then require lib/couhc_potato.rb.
35
+
36
+ h4. Using with Rails
37
+
38
+ Add to your config/environment.rb:
39
+
40
+ config.gem 'langalex-couch_potato', :lib => 'couch_potato', :source => 'http://gems.github.com'
41
+
42
+ Then create a config/couchdb.yml:
43
+
44
+ development: development_db_name
45
+ test: test_db_name
46
+ production: production_db_name
47
+
48
+ Alternatively you can also install Couch Potato directly as a plugin.
49
+
50
+ h3. Introduction
51
+
52
+ This is a basic tutorial on how to use Couch Potato. If you want to know all the details feel free to read the specs.
53
+
54
+ h4. Save, load objects
55
+
56
+ First you need a class.
57
+
58
+ class User
59
+ end
60
+
61
+ To make instances of this class persistent include the persistence module:
62
+
63
+ class User
64
+ include CouchPotato::Persistence
65
+ end
66
+
67
+ If you want to store any properties you have to declare them:
68
+
69
+ class User
70
+ include CouchPotato::Persistence
71
+
72
+ property :name
73
+ end
74
+
75
+ Now you can save your objects:
76
+
77
+ user = User.new :name => 'joe'
78
+ user.save # or save!
79
+
80
+ Properties:
81
+
82
+ user.name # => 'joe'
83
+ user.name = {:first => ['joe', 'joey'], :last => 'doe', :middle => 'J'} # you can set any ruby object that responds_to :to_json (includes all core objects)
84
+ user._id # => "02097f33a0046123f1ebc0ebb6937269"
85
+ user._rev # => "2769180384"
86
+ user.created_at # => Fri Oct 24 19:05:54 +0200 2008
87
+ user.updated_at # => Fri Oct 24 19:05:54 +0200 2008
88
+ user.new_document? # => false, for compatibility new_record? will work as well
89
+
90
+ You can of course also retrieve your instance:
91
+
92
+ User.get "02097f33a0046123f1ebc0ebb6937269" # => <#User 0x3075>
93
+
94
+ h4. Object validations
95
+
96
+ Couch Potato uses the validatable library for vaidation (http://validatable.rubyforge.org/)\
97
+
98
+ class User
99
+ property :name
100
+ validates_presence_of :name
101
+ end
102
+
103
+ user = User.new
104
+ user.valid? # => false
105
+ user.errors.on(:name) # => [:name, 'can't be blank']
106
+
107
+ h4. Finding stuff
108
+
109
+ For running basic finds (e.g. creating/querying views) you can either use the CouchPotato::Persistence::Finder class:
110
+
111
+ joe = User.create! :first_name => 'joe', :position => 1
112
+ jane = User.create! :first_name => 'jane', :position => 2
113
+ Finder.new.find User, :first_name => 'joe' # => [joe]
114
+ Finder.new.find User, :first_name => ['joe', 'jane'] # => [joe, jane]
115
+ Finder.new.find User, :position => 1..2 # => [joe, jane]
116
+
117
+ You can also count:
118
+
119
+ user = User.create! :first_name => 'joe'
120
+ Finder.new.count User, :first_name => 'joe' # => 1
121
+
122
+ Or you can call first/all/count on a class persistent class which will call the Finder.find, e.g.:
123
+
124
+ User.first :login => 'alex'
125
+ User.all
126
+ User.count :activated => true
127
+
128
+ Be warned though that executing these finder methods will generate a new view for every new combination of class and attribute names it gets called with, so you really don't want to use this in a console on a production system.
129
+
130
+ Support for more sophisticated views will be added later.
131
+
132
+ h4. Associations
133
+
134
+ As of now has_many and belongs_to are supported. By default the associated objects are stored in separate documents linked via foreign keys just like in relational databases.
135
+
136
+ class User
137
+ has_many :addresses, :dependent => :destroy
138
+ end
139
+
140
+ class Address
141
+ belongs_to :user
142
+ property :street
143
+ end
144
+
145
+ user = User.new
146
+ user.addresses.build :street => 'potato way'
147
+ user.addresses.first # => <#Address 0x987>
148
+ user.addresses.create! # raises an exception as street is blank
149
+ user.addresses.first.user == user # => true
150
+
151
+ As CouchDB can not only store flat structures you also store associations inline:
152
+
153
+ class User
154
+ has_many :addresses, :stored => :inline
155
+ end
156
+
157
+ This will store the addresses of the user as an array within your CouchDB document.
158
+
159
+ You can also find stuff on associations (not the :stored => :inline):
160
+
161
+ user.addresses.all :street => 'potato way'
162
+
163
+ ... returns only adresses belonging to this user instance and also matching the given conditions. You can call #count a well, #first and #last are not implemented here since those collide with the methods in Enumerable.
164
+
165
+ h4. callbacks
166
+
167
+ Couch Potato supports the usual lifecycle callbacks known from ActiveRecord:
168
+
169
+ class User
170
+ include CouchPotato::Persistence
171
+
172
+ before_create :do_something_before_create
173
+ after_update :do_something_else
174
+ end
175
+
176
+ This will call the method do_something_before_create before creating an object and do_something_else after updating one. Supported callbacks are: :before_validation_on_create, :before_validation_on_update, :before_validation_on_save, :before_create, :after_create, :before_update, :after_update, :before_save, :after_save, :before_destroy, :after_destroy
177
+
178
+ If you want to do any CouchDB update/create/delete operation in your callback methods...
179
+
180
+ class User
181
+ include CouchPotato::Persistence
182
+ has_many :comments
183
+
184
+ before_update :create_a_comment
185
+
186
+ private
187
+ def create_a_comment
188
+ comments.create :body => 'i was updated'
189
+ end
190
+ end
191
+
192
+ ... and you want the entire operation including its hooks to be atomic you can do this:
193
+
194
+ class User
195
+ include CouchPotato::Persistence
196
+ has_many :comments
197
+
198
+ before_update :create_a_comment
199
+
200
+ private
201
+ def create_a_comment
202
+ bulk_save_queue << Comment.new(:body => 'i was updated')
203
+ end
204
+ end
205
+
206
+ h4. Custom Views
207
+
208
+ *This is still in very early in development and of limited use*
209
+
210
+ This is useful if you have a set of documents that has not been created with Couch Potato, for example doesn't have a ruby_class attribute but you still want to be able to load those documents as instances of a Couch Potato class.
211
+
212
+ Example: Assuming you have a document like this:
213
+
214
+ {name: "joe"}
215
+
216
+ And a class:
217
+
218
+ class User
219
+ include CouchPotato::Persistence
220
+ property :name
221
+ end
222
+
223
+ To be able to load that user all you have to do is this:
224
+
225
+ class User
226
+ ...
227
+
228
+ view :everyone
229
+ end
230
+
231
+ User.everyone
232
+
233
+ This will load all documents in your database as instances of User and assign their _id and name attributes.
234
+
235
+ If you have larger structures and you only want to load some attributes you can customize the view:
236
+
237
+ {name: "joe", bio: "52 pages of text ...."}
238
+
239
+ class User
240
+ property :name
241
+ property :bio
242
+
243
+ view :everyone, :properties => [:name]
244
+ end
245
+
246
+ User.everyone.first.name # => "joe"
247
+ User.everyone.first.bio # => nil
248
+
249
+
250
+ h4. Versioning
251
+
252
+ Couch Potato supports versioning your objects, very similar to the popular acts_as_versioned plugin for ActiveRecord. To use it include the module:
253
+
254
+ class Document
255
+ include CouchPotato::Persistence
256
+ include CouchPotato::Versioning
257
+ end
258
+
259
+ After that your object will have a version that gets incremented on each save.
260
+
261
+ doc = Document.create
262
+ doc.version # => 1
263
+ doc.save
264
+ doc.version # => 2
265
+
266
+ You can access the older versions via the versions method.
267
+
268
+ doc.versions.first.version # => 1
269
+
270
+ When passing a version number the version method will only return that version:
271
+
272
+ doc.versions(1).version # => 1
273
+
274
+ You can set a condition for when to create a new version:
275
+
276
+ class Document
277
+ attr_accessor :update_version
278
+ include CouchPotato::Persistence
279
+ include CouchPotato::Versioning
280
+
281
+ set_version_condition lambda {|doc| doc.update_version}
282
+ end
283
+
284
+ doc = Document.create
285
+ doc.update_version = false
286
+ doc.version # => 1
287
+ doc.save
288
+ doc.version # => 1
289
+ doc.update_version = true
290
+ doc.save
291
+ doc.version # => 2
292
+
293
+ h4. Ordered Lists
294
+
295
+ Couch Potato supports ordered lists for has_many relationships (with the :stored => :separately option only), very similar to the popular acts_as_list plugin for ActiveRecord. To use it include the module:
296
+
297
+ class PersistenArray
298
+ include CouchPotato::Persistence
299
+ has_many :items
300
+ end
301
+
302
+ class Item
303
+ include CouchPotato::Ordering
304
+ belongs_to :persistent_array
305
+ set_ordering_scope :persistent_array_id
306
+ end
307
+
308
+ array = PersistenArray.new
309
+ item1 = array.items.create!
310
+ item1.position # => 1
311
+ item2 = array.items.create!
312
+ item2.position # => 2
313
+
314
+ You can move items up and down simply by changing the position:
315
+
316
+ item2.position = 1
317
+ item2.save!
318
+ item1.position # => 2
319
+
320
+ And you can insert new items at any position you want:
321
+
322
+ item3 = array.items.create! :position => 2
323
+ item1.position # => 3
324
+
325
+ And remove:
326
+
327
+ item3.destroy
328
+ item1.position # => 2
329
+
330
+ h3. Helping out
331
+
332
+ Please fix bugs, add more specs, implement new features by forking the github repo at http://github.com/langalex/couch_potato.
333
+
334
+ You can run all the specs by calling 'rake spec_unit' and 'rake spec_functional' in the root folder of Couch Potato. The specs require a running CouchDB instance at http://localhost:5984
335
+
336
+ I will only accept patches that are covered by specs - sorry.
337
+
338
+ h3. Contact
339
+
340
+ If you have any questions/suggestions etc. please contact me at alex at upstream-berlin.com or @langalex on twitter.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 0
4
+ :minor: 1
@@ -71,15 +71,11 @@ module CouchPotato
71
71
  end
72
72
 
73
73
  module ExternalCollectionOrderedFindExtension
74
- def self.included(base)
75
- base.class_eval do
76
- def items
77
- if @item_class.property_names.include?(:position)
78
- @items ||= CouchPotato::Persistence::Finder.new.find @item_class, @owner_id_attribute_name => owner_id, :position => 1..CouchPotato::Ordering::MAX
79
- else
80
- @items ||= CouchPotato::Persistence::Finder.new.find @item_class, @owner_id_attribute_name => owner_id
81
- end
82
- end
74
+ def items
75
+ if @item_class.property_names.include?(:position)
76
+ super @item_class, @owner_id_attribute_name => owner_id, :position => 1..CouchPotato::Ordering::MAX
77
+ else
78
+ super
83
79
  end
84
80
  end
85
81
  end
@@ -8,6 +8,9 @@ require File.dirname(__FILE__) + '/persistence/callbacks'
8
8
  require File.dirname(__FILE__) + '/persistence/json'
9
9
  require File.dirname(__FILE__) + '/persistence/bulk_save_queue'
10
10
  require File.dirname(__FILE__) + '/persistence/find'
11
+ require File.dirname(__FILE__) + '/persistence/dirty_attributes'
12
+ require File.dirname(__FILE__) + '/persistence/custom_view'
13
+ require File.dirname(__FILE__) + '/persistence/view_query'
11
14
 
12
15
  module CouchPotato
13
16
  module Persistence
@@ -17,7 +20,7 @@ module CouchPotato
17
20
 
18
21
  def self.included(base)
19
22
  base.send :extend, ClassMethods, Find
20
- base.send :include, Callbacks, Properties, Validatable, Json
23
+ base.send :include, Callbacks, Properties, Validatable, Json, DirtyAttributes, CustomView
21
24
  base.class_eval do
22
25
  attr_accessor :_id, :_rev, :_attachments, :_deleted, :created_at, :updated_at
23
26
  attr_reader :bulk_save_queue
@@ -38,6 +41,11 @@ module CouchPotato
38
41
  end
39
42
  end
40
43
 
44
+ def update_attributes(hash)
45
+ self.attributes = hash
46
+ save
47
+ end
48
+
41
49
  def attributes
42
50
  self.class.properties.inject({}) do |res, property|
43
51
  property.serialize(res, self)
@@ -90,7 +98,7 @@ module CouchPotato
90
98
  end
91
99
 
92
100
  def ==(other)
93
- other.class == self.class && self.class.property_names.map{|name| self.send(name)} == self.class.property_names.map{|name| other.send(name)}
101
+ other.class == self.class && self.to_json == other.to_json
94
102
  end
95
103
 
96
104
  private
@@ -103,7 +111,7 @@ module CouchPotato
103
111
  run_callbacks :before_create
104
112
  self.created_at = Time.now
105
113
  self.updated_at = Time.now
106
- self._id = self.class.db.server.next_uuid rescue Digest::MD5.hexdigest(rand(1000000000000).to_s) # only works with couchdb 0.9
114
+ self._id = generate_uuid
107
115
  bulk_save_queue << self
108
116
  save_dependent_objects
109
117
  bulk_save_queue.save do |res|
@@ -114,6 +122,10 @@ module CouchPotato
114
122
  true
115
123
  end
116
124
 
125
+ def generate_uuid
126
+ self.class.server.next_uuid rescue Digest::MD5.hexdigest(rand(1000000000000).to_s) # only works with couchdb 0.9
127
+ end
128
+
117
129
  def extract_rev(res)
118
130
  res['new_revs'].select{|hash| hash['id'] == self.id}.first['rev']
119
131
  end
@@ -172,15 +184,28 @@ module CouchPotato
172
184
  def db(name = nil)
173
185
  ::CouchPotato::Persistence.Db(name)
174
186
  end
187
+
175
188
  end
176
189
 
177
190
  def self.Db(database_name = nil)
191
+ @@__database ||= CouchRest.database(full_url_to_database(database_name))
192
+ end
193
+
194
+ def self.Server(database_name = nil)
195
+ @@_server ||= Db(database_name).server
196
+ end
197
+
198
+ def self.Db!(database_name = nil)
199
+ CouchRest.database!(full_url_to_database(database_name))
200
+ end
201
+
202
+ def self.full_url_to_database(database_name)
178
203
  database_name ||= CouchPotato::Config.database_name || raise('No Database configured. Set CouchPotato::Config.database_name')
179
- full_url_to_database = database_name
180
- if full_url_to_database !~ /^http:\/\//
181
- full_url_to_database = "http://localhost:5984/#{database_name}"
204
+ url = database_name
205
+ if url !~ /^http:\/\//
206
+ url = "http://localhost:5984/#{database_name}"
182
207
  end
183
- CouchRest.database!(full_url_to_database)
208
+ url
184
209
  end
185
210
  end
186
211
  end