peleteiro-activecouch 0.2.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 (58) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README +28 -0
  3. data/Rakefile +52 -0
  4. data/VERSION +1 -0
  5. data/lib/active_couch.rb +12 -0
  6. data/lib/active_couch/base.rb +608 -0
  7. data/lib/active_couch/callbacks.rb +89 -0
  8. data/lib/active_couch/connection.rb +164 -0
  9. data/lib/active_couch/errors.rb +13 -0
  10. data/lib/active_couch/support.rb +3 -0
  11. data/lib/active_couch/support/exporter.rb +97 -0
  12. data/lib/active_couch/support/extensions.rb +86 -0
  13. data/lib/active_couch/support/inflections.rb +52 -0
  14. data/lib/active_couch/support/inflector.rb +279 -0
  15. data/lib/active_couch/views.rb +3 -0
  16. data/lib/active_couch/views/errors.rb +4 -0
  17. data/lib/active_couch/views/raw_view.rb +40 -0
  18. data/lib/active_couch/views/view.rb +85 -0
  19. data/lib/activecouch.rb +1 -0
  20. data/spec/base/after_delete_spec.rb +110 -0
  21. data/spec/base/after_save_spec.rb +102 -0
  22. data/spec/base/before_delete_spec.rb +109 -0
  23. data/spec/base/before_save_spec.rb +101 -0
  24. data/spec/base/count_all_spec.rb +29 -0
  25. data/spec/base/count_spec.rb +77 -0
  26. data/spec/base/create_spec.rb +28 -0
  27. data/spec/base/database_spec.rb +70 -0
  28. data/spec/base/delete_spec.rb +97 -0
  29. data/spec/base/find_from_url_spec.rb +55 -0
  30. data/spec/base/find_spec.rb +383 -0
  31. data/spec/base/from_json_spec.rb +54 -0
  32. data/spec/base/has_many_spec.rb +89 -0
  33. data/spec/base/has_spec.rb +88 -0
  34. data/spec/base/id_spec.rb +25 -0
  35. data/spec/base/initialize_spec.rb +91 -0
  36. data/spec/base/marshal_dump_spec.rb +64 -0
  37. data/spec/base/marshal_load_spec.rb +58 -0
  38. data/spec/base/module_spec.rb +18 -0
  39. data/spec/base/nested_class_spec.rb +19 -0
  40. data/spec/base/rev_spec.rb +20 -0
  41. data/spec/base/save_spec.rb +130 -0
  42. data/spec/base/site_spec.rb +62 -0
  43. data/spec/base/to_json_spec.rb +73 -0
  44. data/spec/connection/initialize_spec.rb +28 -0
  45. data/spec/exporter/all_databases_spec.rb +24 -0
  46. data/spec/exporter/create_database_spec.rb +47 -0
  47. data/spec/exporter/delete_database_spec.rb +45 -0
  48. data/spec/exporter/delete_spec.rb +36 -0
  49. data/spec/exporter/export_spec.rb +62 -0
  50. data/spec/exporter/export_with_raw_views_spec.rb +66 -0
  51. data/spec/spec_helper.rb +9 -0
  52. data/spec/views/define_spec.rb +34 -0
  53. data/spec/views/include_attributes_spec.rb +30 -0
  54. data/spec/views/raw_view_spec.rb +49 -0
  55. data/spec/views/to_json_spec.rb +58 -0
  56. data/spec/views/with_filter_spec.rb +13 -0
  57. data/spec/views/with_key_spec.rb +19 -0
  58. metadata +117 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Arun Thampi & Cheah Chu Yeow
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,28 @@
1
+ ActiveCouch wants to be a simple, convenient, idiomatic Object Relational Mapper for the hot new kid on the block - CouchDB. CouchDB (simplistically speaking) is a document store, which essentially means that objects can be stored in a schema-less environment.
2
+
3
+ What it is?
4
+ -----------
5
+ With ActiveCouch, you can easily save, query, delete documents to/from a CouchDB database in your favourite language - Ruby. ActiveCouch derives a lot of its principles (and some code) from both ActiveRecord and ActiveResource, two libraries made popular by the other hot pubescent on the block - Ruby on Rails (http://www.rubyonrails.org).
6
+
7
+ Why?
8
+ ----
9
+ As they say, necessity is the mother of invention. And as they also say, death before inconvenience. Our company, Wego (http://www.wego.com) has been using CouchDB for the past six months now, as we have a need for a document-model to store vast amounts of information, and we needed a convenience mapper in our favourite language, in order to use CouchDB elegantly. Since, the Rubyists here at Wego are already very familiar with ActiveRecord semantics, care has been taken to ensure that ActiveCouch resembled it in many ways.
10
+
11
+ Contributors
12
+ ------------
13
+
14
+ - Cheah Chu Yeow (http://www.github.com/chuyeow)
15
+ - Carlos Villela (http://www.github.com/cv)
16
+
17
+ Requirements
18
+ ------------
19
+ - Ruby 1.8.5 or above (http://www.ruby-lang.org)
20
+ - rubygems 0.9.4 (http://rubygems.org)
21
+ - JSON gem (http://json.rubyforge.org) [Used for JSON encoding/decoding]
22
+ - RSpec gem (http://rspec.rubyforge.org) [Used to run specs]
23
+ - CouchDB 0.8.0 and upwards ( http://incubator.apache.org/couchdb/community/code.html ) [Some specs require running CouchDB at localhost:5984]
24
+
25
+ Important Notice
26
+ ----------------
27
+
28
+ ActiveCouch v0.2.0 does not support pre-0.8.0 versions of CouchDB
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |s|
7
+ s.platform = Gem::Platform::RUBY
8
+ s.name = 'activecouch'
9
+ s.summary = 'Ruby-based wrapper for CouchDB'
10
+ s.description = 'ActiveCouch wants to be a simple, convenient, idiomatic Object Relational Mapper for the
11
+ hot new kid on the block - CouchDB. CouchDB (simplistically speaking) is a document store,
12
+ which essentially means that objects can be stored in a schema-less environment.'
13
+ s.author = 'Arun Thampi & Cheah Chu Yeow'
14
+ s.email = "arun.thampi@gmail.com, chuyeow@gmail.com"
15
+ s.homepage = "http://github.com/arunthampi/activecouch"
16
+ s.rubyforge_project = 'activecouch'
17
+ s.files = FileList['[A-Z]*', 'lib/**/*.rb']
18
+ s.test_files = FileList['spec/**/*.rb']
19
+ s.has_rdoc = true
20
+ s.require_path = "lib"
21
+ s.extra_rdoc_files = ["README"]
22
+ s.add_dependency 'json', '>=1.1.2'
23
+ end
24
+ rescue LoadError
25
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
26
+ end
27
+
28
+ task :lines do
29
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
30
+
31
+ for file_name in FileList["lib/active_couch/**/*.rb"]
32
+ next if file_name =~ /vendor/
33
+ f = File.open(file_name)
34
+
35
+ while line = f.gets
36
+ lines += 1
37
+ next if line =~ /^\s*$/
38
+ next if line =~ /^\s*#/
39
+ codelines += 1
40
+ end
41
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
42
+
43
+ total_lines += lines
44
+ total_codelines += codelines
45
+
46
+ lines, codelines = 0, 0
47
+ end
48
+
49
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
50
+ end
51
+
52
+ task :default => [:gemspec, :build]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
@@ -0,0 +1,12 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'rubygems'
5
+ require 'json'
6
+
7
+ require 'active_couch/support'
8
+ require 'active_couch/errors'
9
+ require 'active_couch/base'
10
+ require 'active_couch/connection'
11
+ require 'active_couch/views'
12
+ require 'active_couch/callbacks'
@@ -0,0 +1,608 @@
1
+ module ActiveCouch
2
+ class Base
3
+ SPECIAL_MEMBERS = %w(attributes associations connection callbacks)
4
+ DEFAULT_ATTRIBUTES = %w(id rev)
5
+ TYPES = { :text => "", :number => 0, :decimal => 0.0, :boolean => true }
6
+ TYPES.default = ""
7
+
8
+ # Initializes an ActiveCouch::Base object. The constructor accepts both a hash, as well as
9
+ # a block to initialize attributes
10
+ #
11
+ # Examples:
12
+ # class Person < ActiveCouch::Base
13
+ # has :name
14
+ # end
15
+ #
16
+ # person1 = Person.new(:name => "McLovin")
17
+ # person1.name # => "McLovin"
18
+ #
19
+ # person2 = Person.new do |p|
20
+ # p.name = "Seth"
21
+ # end
22
+ # person2.name # => "Seth"
23
+ def initialize(params = {})
24
+ # Object instance variable
25
+ @attributes = {}; @associations = {}; @callbacks = Hash.new; @connection = self.class.connection
26
+ # Initialize local variables from class instance variables
27
+ klass_atts = self.class.attributes
28
+ klass_assocs = self.class.associations
29
+ klass_callbacks = self.class.callbacks
30
+ # ActiveCouch::Connection object will be readable in every
31
+ # object instantiated from a subclass of ActiveCouch::Base
32
+ SPECIAL_MEMBERS.each do |k|
33
+ self.instance_eval "def #{k}; @#{k}; end"
34
+ end
35
+ # First, initialize all the attributes
36
+ klass_atts.each_key do |property|
37
+ @attributes[property] = klass_atts[property]
38
+ self.instance_eval "def #{property}; attributes[:#{property}]; end"
39
+ self.instance_eval "def #{property}=(val); attributes[:#{property}] = val; end"
40
+ # These are special attributes which need aliases (for now, it's _id and _rev)
41
+ if property.to_s[0,1] == '_'
42
+ aliased_prop = property.to_s.slice(1, property.to_s.size)
43
+ self.instance_eval "def #{aliased_prop}; self.#{property}; end"
44
+ self.instance_eval "def #{aliased_prop}=(val); self.#{property}=(val); end"
45
+ end
46
+ end
47
+ # Then, initialize all the associations
48
+ klass_assocs.each_key do |k|
49
+ @associations[k] = klass_assocs[k]
50
+ self.instance_eval "def #{k}; @#{k} ||= []; end"
51
+ # If you have has_many :people, this will add a method called add_person
52
+ # to the object instantiated from the class
53
+ self.instance_eval "def add_#{k.singularize}(val); @#{k} = #{k} << val; end"
54
+ end
55
+ # Finally, all the calbacks
56
+ klass_callbacks.each_key do |k|
57
+ @callbacks[k] = klass_callbacks[k].dup
58
+ end
59
+ # Set any instance variables if any, which are present in the params hash
60
+ from_hash(params)
61
+ # Handle the block, which can also be used to initialize the object
62
+ yield self if block_given?
63
+ end
64
+
65
+ # Generates a JSON representation of an instance of a subclass of ActiveCouch::Base.
66
+ # Ignores attributes which have a nil value.
67
+ #
68
+ # Examples:
69
+ # class Person < ActiveCouch::Base
70
+ # has :name, :which_is => :text, :with_default_value => "McLovin"
71
+ # end
72
+ #
73
+ # person = Person.new
74
+ # person.to_json # {"name":"McLovin"}
75
+ #
76
+ # class AgedPerson < ActiveCouch::Base
77
+ # has :age, :which_is => :decimal, :with_default_value => 3.5
78
+ # end
79
+ #
80
+ # aged_person = AgedPerson.new
81
+ # aged_person.id = 'abc-def'
82
+ # aged_person.to_json # {"age":3.5, "_id":"abc-def"}
83
+ def to_json
84
+ hash = {}
85
+ # First merge the attributes...
86
+ hash.merge!(attributes.reject{ |k,v| v.nil? })
87
+ # ...and then the associations
88
+ associations.each_key { |name| hash.merge!({ name => self.__send__(name.to_s) }) }
89
+ # and by the Power of Grayskull, convert the hash to json
90
+ hash.to_json
91
+ end
92
+
93
+ # Saves a document into a CouchDB database. A document can be saved in two ways.
94
+ # One if it has been set an ID by the user, in which case the connection object
95
+ # needs to use an HTTP PUT request to the URL /database/user_generated_id.
96
+ # For the document needs a CouchDB-generated ID, the connection object needs
97
+ # to use an HTTP POST request to the URL /database.
98
+ #
99
+ # Examples:
100
+ # class Person < ActiveCouch::Base
101
+ # has :name, :which_is => :text
102
+ # end
103
+ #
104
+ # person = Person.new(:name => 'McLovin')
105
+ # person.id = 'abc'
106
+ # person.save # true
107
+ # person.new? # false
108
+ def save(options = {})
109
+ database = options[:to_database] || self.class.database_name
110
+ if id
111
+ response = connection.put("/#{database}/#{id}", to_json)
112
+ else
113
+ response = connection.post("/#{database}", to_json)
114
+ end
115
+ # Parse the JSON obtained from the body...
116
+ results = JSON.parse(response.body)
117
+ # ...and set the default id and rev attributes
118
+ DEFAULT_ATTRIBUTES.each { |a| self.__send__("#{a}=", results[a]) }
119
+ # Response sent will be 201, if the save was successful [201 corresponds to 'created']
120
+ return response.code == '201'
121
+ end
122
+
123
+ # Checks to see if a document has been persisted in a CouchDB database.
124
+ # If a document has been retrieved from CouchDB, or has been persisted in
125
+ # a CouchDB database, the attribute _rev would not be nil.
126
+ #
127
+ # Examples:
128
+ # class Person < ActiveCouch::Base
129
+ # has :name, :which_is => :text
130
+ # end
131
+ #
132
+ # person = Person.new(:name => 'McLovin')
133
+ # person.id = 'abc'
134
+ # person.save # true
135
+ # person.new? # false
136
+ def new?
137
+ rev.nil?
138
+ end
139
+
140
+ # Deletes a document from a CouchDB database. This is an instance-level delete method.
141
+ # Example:
142
+ # class Person < ActiveCouch::Base
143
+ # has :name
144
+ # end
145
+ #
146
+ # person = Person.create(:name => 'McLovin')
147
+ # person.delete # true
148
+ def delete(options = {})
149
+ database = options[:from_database] || self.class.database_name
150
+
151
+ if new?
152
+ raise ArgumentError, "You must specify a revision for the document to be deleted"
153
+ elsif id.nil?
154
+ raise ArgumentError, "You must specify an ID for the document to be deleted"
155
+ end
156
+ response = connection.delete("/#{database}/#{id}?rev=#{rev}")
157
+ # Set the id and rev to nil, since the object has been successfully deleted from CouchDB
158
+ if response.code =~ /20[0,2]/
159
+ self.id = nil; self.rev = nil
160
+ true
161
+ else
162
+ false
163
+ end
164
+ end
165
+
166
+ def marshal_dump # :nodoc:
167
+ # Deflate using Zlib
168
+ self.to_json
169
+ end
170
+
171
+ def marshal_load(str) # :nodoc:
172
+ self.instance_eval do
173
+ # Inflate first, and then parse the JSON
174
+ hash = JSON.parse(str)
175
+ initialize(hash)
176
+ end
177
+ self
178
+ end
179
+
180
+ class << self # Class methods
181
+ # Returns the CouchDB database name that's backing this model. The database name is guessed from the name of the
182
+ # class somewhat similar to ActiveRecord conventions.
183
+ #
184
+ # Examples:
185
+ # class Invoice < ActiveCouch::Base; end;
186
+ # file class database_name
187
+ # invoice.rb Invoice invoices
188
+ #
189
+ # class Invoice < ActiveCouch::Base; class Lineitem < ActiveCouch::Base; end; end;
190
+ # file class database_name
191
+ # invoice.rb Invoice::Lineitem invoice_lineitems
192
+ #
193
+ # module Invoice; class Lineitem < ActiveCouch::Base; end; end;
194
+ # file class database_name
195
+ # invoice/lineitem.rb Invoice::Lineitem lineitems
196
+ #
197
+ # You can override this method or use <tt>set_database_name</tt> to override this class method to allow for names
198
+ # that can't be inferred.
199
+ def database_name
200
+ base = base_class
201
+ name = (unless self == base
202
+ base.database_name
203
+ else
204
+ # Nested classes are prefixed with singular parent database name.
205
+ if parent < ActiveCouch::Base
206
+ contained = parent.database_name.singularize
207
+ contained << '_'
208
+ end
209
+ "#{contained}#{base.name.pluralize.demodulize.underscore}"
210
+ end)
211
+ set_database_name(name)
212
+ name
213
+ end
214
+
215
+ # Sets the database name to the given value, or (if the value is nil or false) to the value returned by the
216
+ # given block. Useful for setting database names that can't be automatically inferred from the class name.
217
+ #
218
+ # This method is aliased as <tt>database_name=</tt>.
219
+ #
220
+ # Example:
221
+ #
222
+ # class Post < ActiveCouch::Base
223
+ # set_database_name 'legacy_posts'
224
+ # end
225
+ def set_database_name(database = nil, &block)
226
+ define_attr_method(:database_name, database, &block)
227
+ end
228
+ alias :database_name= :set_database_name
229
+
230
+ # Sets the site which the ActiveCouch object has to connect to, which
231
+ # initializes an ActiveCouch::Connection object.
232
+ #
233
+ # Example:
234
+ # class Person < ActiveCouch::Base
235
+ # site 'localhost:5984'
236
+ # end
237
+ #
238
+ # Person.connection.nil? # false
239
+ def site(site)
240
+ @connection = Connection.new(site)
241
+ end
242
+
243
+ # Defines an attribute for a subclass of ActiveCouch::Base. The parameters
244
+ # for this method include name, which is the name of the attribute as well as
245
+ # an options hash.
246
+ #
247
+ # The options hash can contain the key 'which_is' which can
248
+ # have possible values :text, :decimal, :number. It can also contain the key
249
+ # 'with_default_value' which can set a default value for each attribute defined
250
+ # in the subclass of ActiveCouch::Base
251
+ #
252
+ # Examples:
253
+ # class Person < ActiveCouch::Base
254
+ # has :name
255
+ # end
256
+ #
257
+ # person = Person.new
258
+ # p.name.methods.include?(:name) # true
259
+ # p.name.methods.include?(:name=) # false
260
+ #
261
+ # class AgedPerson < ActiveCouch::Base
262
+ # has :age, :which_is => :number, :with_default_value = 18
263
+ # end
264
+ #
265
+ # person = AgedPerson.new
266
+ # person.age # 18
267
+ def has(name, options = {})
268
+ unless name.is_a?(String) || name.is_a?(Symbol)
269
+ raise ArgumentError, "#{name} is neither a String nor a Symbol"
270
+ end
271
+ # Set the attributes value to options[:with_default_value]
272
+ # In the constructor, this will be used to initialize the value of
273
+ # the 'name' instance variable to the value in the hash
274
+ @attributes[name] = options[:with_default_value] || TYPES[:which_is]
275
+ end
276
+
277
+ # Defines an array of objects which are 'children' of this class. The has_many
278
+ # function guesses the class of the child, based on the name of the association,
279
+ # but can be over-ridden by the :class key in the options hash.
280
+ #
281
+ # Examples:
282
+ #
283
+ # class Person < ActiveCouch::Base
284
+ # has :name
285
+ # end
286
+ #
287
+ # class GrandPerson < ActiveCouch::Base
288
+ # has_many :people # which will create an empty array which can contain
289
+ # # Person objects
290
+ # end
291
+ def has_many(name, options = {})
292
+ unless name.is_a?(String) || name.is_a?(Symbol)
293
+ raise ArgumentError, "#{name} is neither a String nor a Symbol"
294
+ end
295
+
296
+ @associations[name] = get_klass(name, options)
297
+ end
298
+
299
+ # Defines a single object which is a 'child' of this class. The has_one
300
+ # function guesses the class of the child, based on the name of the association,
301
+ # but can be over-ridden by the :class key in the options hash.
302
+ #
303
+ # Examples:
304
+ #
305
+ # class Child < ActiveCouch::Base
306
+ # has :name
307
+ # end
308
+ #
309
+ # class GrandParent < ActiveCouch::Base
310
+ # has_one :child
311
+ # end
312
+ def has_one(name, options = {})
313
+ unless name.is_a?(String) || name.is_a?(Symbol)
314
+ raise ArgumentError, "#{name} is neither a String nor a Symbol"
315
+ end
316
+
317
+ @associations[name] = get_klass(name, options)
318
+ end
319
+
320
+ # Initializes an object of a subclass of ActiveCouch::Base based on a JSON
321
+ # representation of the object.
322
+ #
323
+ # Example:
324
+ # class Person < ActiveCouch::Base
325
+ # has :name
326
+ # end
327
+ #
328
+ # person = Person.from_json('{"name":"McLovin"}')
329
+ # person.name # "McLovin"
330
+ def from_json(json)
331
+ hash = JSON.parse(json)
332
+ # Create new based on parsed
333
+ self.new(hash)
334
+ end
335
+
336
+ # Retrieves one or more object(s) from a CouchDB database, based on the search
337
+ # parameters given.
338
+ #
339
+ # Example:
340
+ # class Person < ActiveCouch::Base
341
+ # has :name
342
+ # end
343
+ #
344
+ # # This returns a single instance of an ActiveCouch::Base subclass
345
+ # people = Person.find(:first, :params => {:name => "McLovin"})
346
+ #
347
+ # # This returns an array of ActiveCouch::Base subclass instances
348
+ # person = Person.find(:all, :params => {:name => "McLovin"})
349
+ def find(*arguments)
350
+ scope = arguments.slice!(0)
351
+ search_params = arguments.slice!(0) || {}
352
+
353
+ case scope
354
+ when :all then find_every(search_params)
355
+ when :first then find_every(search_params, {:limit => 1}).first
356
+ else find_one(scope)
357
+ end
358
+ end
359
+
360
+ # Retrieves one or more object(s) from a CouchDB database, based on the search
361
+ # parameters given. This method is similar to the find_by_sql method in
362
+ # ActiveRecord, in a way that instead of using any conditions, we use a raw
363
+ # URL to query a CouchDB view.
364
+ #
365
+ # Example:
366
+ # class Person < ActiveCouch::Base
367
+ # has :name
368
+ # end
369
+ #
370
+ # # This returns a single instance of an ActiveCouch::Base subclass
371
+ # people = Person.find_from_url("/people/_view/by_name/by_name?key=%22Mclovin%22")
372
+ def find_from_url(url)
373
+ # If the url contains the word '_view' it means it will return objects as an array,
374
+ # how ever if it doesn't it means the user is getting an ID-based url like /properties/abcd
375
+ # which will only return a single object
376
+ if url =~ /_view/
377
+ instantiate_collection(connection.get(url))
378
+ else
379
+ begin
380
+ instantiate_object(connection.get(url))
381
+ rescue ResourceNotFound
382
+ nil
383
+ end
384
+ end
385
+ end
386
+
387
+ # Retrieves the count of the number of objects in the CouchDB database, based on the
388
+ # search parameters given.
389
+ #
390
+ # Example:
391
+ # class Person < ActiveCouch::Base
392
+ # has :name
393
+ # end
394
+ #
395
+ # # This returns the count of the number of objects
396
+ # people_count = Person.count(:params => {:name => "McLovin"})
397
+ def count(search_params = {})
398
+ path = "/#{database_name}/_view/#{query_string(search_params[:params], {:limit => 0})}"
399
+ result = connection.get(path)
400
+
401
+ JSON.parse(result)['total_rows'].to_i
402
+ end
403
+
404
+ # Retrieves the count of the number of objects in the CouchDB database, irrespective of
405
+ # any search criteria
406
+ #
407
+ # Example:
408
+ # class Person < ActiveCouch::Base
409
+ # has :name
410
+ # end
411
+ #
412
+ # # This returns the count of the number of objects
413
+ # people_count = Person.count_all
414
+ def count_all
415
+ result = connection.get("/#{database_name}")
416
+ JSON.parse(result)['doc_count'].to_i
417
+ end
418
+
419
+ # Initializes a new subclass of ActiveCouch::Base and saves in the CouchDB database
420
+ # as a new document
421
+ #
422
+ # Example:
423
+ # class Person < ActiveCouch::Base
424
+ # has :name
425
+ # end
426
+ #
427
+ # person = Person.create(:name => "McLovin")
428
+ # person.id.nil? # false
429
+ # person.new? # false
430
+ def create(arguments)
431
+ unless arguments.is_a?(Hash)
432
+ raise ArgumentError, "The arguments must be a Hash"
433
+ else
434
+ new_record = self.new(arguments)
435
+ new_record.save
436
+ new_record
437
+ end
438
+ end
439
+
440
+ # Deletes a document from the CouchDB database, based on the id and rev parameters passed to it.
441
+ # Returns true if the document has been deleted
442
+ #
443
+ # Example:
444
+ # class Person < ActiveCouch::Base
445
+ # has :name
446
+ # end
447
+ #
448
+ # Person.delete(:id => 'abc-def', :rev => '1235')
449
+ def delete(options = {})
450
+ if options.nil? || !options.has_key?(:id) || !options.has_key?(:rev)
451
+ raise ArgumentError, "You must specify both an id and a rev for the document to be deleted"
452
+ end
453
+ response = connection.delete("/#{self.database_name}/#{options[:id]}?rev=#{options[:rev]}")
454
+ # Returns true if the
455
+ !(response.code =~ /20[0,2]/).nil?
456
+ end
457
+
458
+ # Defines an "attribute" method. A new (class) method will be created with the
459
+ # given name. If a value is specified, the new method will
460
+ # return that value (as a string). Otherwise, the given block
461
+ # will be used to compute the value of the method.
462
+ #
463
+ # The original method, if it exists, will be aliased, with the
464
+ # new name being
465
+ # prefixed with "original_". This allows the new method to
466
+ # access the original value.
467
+ #
468
+ # This method is stolen from ActiveRecord.
469
+ #
470
+ # Example:
471
+ #
472
+ # class Foo < ActiveCouch::Base
473
+ # define_attr_method :database_name, 'foo'
474
+ # # OR
475
+ # define_attr_method(:database_name) do
476
+ # original_database_name + '_legacy'
477
+ # end
478
+ # end
479
+ def define_attr_method(name, value = nil, &block)
480
+ metaclass.send(:alias_method, "original_#{name}", name)
481
+ if block_given?
482
+ meta_def name, &block
483
+ else
484
+ metaclass.class_eval "def #{name}; #{value.to_s.inspect}; end"
485
+ end
486
+ end
487
+
488
+ def inherited(subklass)
489
+ subklass.class_eval do
490
+ include ActiveCouch::Callbacks
491
+ end
492
+
493
+ subklass.instance_eval do
494
+ @attributes = { :_id => nil, :_rev => nil }
495
+ @associations = {}
496
+ @callbacks = Hash.new([])
497
+ @connection = ActiveCouch::Base.instance_variable_get('@connection')
498
+ end
499
+
500
+ SPECIAL_MEMBERS.each do |k|
501
+ subklass.instance_eval "def #{k}; @#{k}; end"
502
+ end
503
+ end
504
+
505
+ def base_class
506
+ class_of_active_couch_descendant(self)
507
+ end
508
+
509
+ private
510
+ # Generate a class from a name
511
+ def get_klass(name, options)
512
+ klass = options[:class]
513
+ !klass.nil? && klass.is_a?(Class) ? klass : name.to_s.classify.constantize
514
+ end
515
+
516
+ # Returns the class descending directly from ActiveCouch in the inheritance hierarchy.
517
+ def class_of_active_couch_descendant(klass)
518
+ if klass.superclass == Base
519
+ klass
520
+ elsif klass.superclass.nil?
521
+ raise ActiveCouchError, "#{name} doesn't belong in a hierarchy descending from ActiveCouch"
522
+ else
523
+ class_of_active_couch_descendant(klass.superclass)
524
+ end
525
+ end
526
+
527
+ # Returns an array of ActiveCouch::Base objects by querying a CouchDB permanent view
528
+ def find_every(search_params, overriding_options = {})
529
+ case from = search_params[:from]
530
+ when String
531
+ path = "#{from}"
532
+ else
533
+ options = search_params.reject { |k,v| k == :params }
534
+ options.merge!(overriding_options)
535
+
536
+ path = "/#{database_name}/_view/#{query_string(search_params[:params], options)}"
537
+ end
538
+ instantiate_collection(connection.get(path))
539
+ end
540
+
541
+ def find_one(id)
542
+ path = "/#{database_name}/#{id}"
543
+ begin
544
+ instantiate_object(connection.get(path))
545
+ rescue ResourceNotFound
546
+ nil
547
+ end
548
+ end
549
+
550
+ # Generates a query string by using the ActiveCouch convention, which is to
551
+ # have the view defined by pre-pending the attribute to be queried with 'by_'
552
+ # So for example, if the params hash is :name => 'McLovin',
553
+ # the view associated with it will be /by_name/by_name?key="McLovin"
554
+ def query_string(search_params, options)
555
+ unless search_params.is_a?(Hash) || search_params.keys.size != 1
556
+ raise ArgumentError, "Wrong options for ActiveCouch::Base#find" and return
557
+ end
558
+
559
+ key = search_params.keys.first
560
+
561
+ query_string = "by_#{key}/by_#{key}?key=#{search_params[key].to_s.url_encode}"
562
+ query_string = "#{query_string}&skip=#{options[:offset]}" unless options[:offset].nil?
563
+ query_string = "#{query_string}&count=#{options[:limit]}" unless options[:limit].nil?
564
+
565
+ query_string
566
+ end
567
+
568
+ # Instantiates a collection of ActiveCouch::Base objects, based on the
569
+ # result obtained from a CouchDB View.
570
+ #
571
+ # As per the CouchDB Permanent View API, the result set will be contained
572
+ # within a JSON hash as an array, with the key 'rows'
573
+ # The actual CouchDB object which needs to be initialized is obtained with
574
+ # the key 'value'
575
+ def instantiate_collection(result)
576
+ hash = JSON.parse(result)
577
+ hash['rows'].collect { |row| self.new(row['value'].merge('_id' => row['id'])) }
578
+ end
579
+
580
+ # Instantiates an ActiveCouch::Base object, based on the result obtained from
581
+ # the GET URL
582
+ def instantiate_object(result)
583
+ hash = JSON.parse(result)
584
+ self.new(hash)
585
+ end
586
+ end # End class methods
587
+
588
+ private
589
+ def from_hash(hash)
590
+ hash.each do |property, value|
591
+ property = property.to_sym rescue property
592
+ # This means a has_many association
593
+ if value.is_a?(Array) && !(child_klass = @associations[property]).nil?
594
+ value.each do |child|
595
+ child.is_a?(Hash) ? child_obj = child_klass.new(child) : child_obj = child
596
+ self.send "add_#{property.to_s.singularize}", child_obj
597
+ end
598
+ # This means a has_one association
599
+ elsif value.is_a?(Hash) && !(child_klass = @associations[property]).nil?
600
+ self.send "add_#{property.to_s.singualize}", child_klass.new(value)
601
+ # This means this is a normal attribute
602
+ else
603
+ self.send("#{property}=", value) if respond_to?("#{property}=")
604
+ end
605
+ end
606
+ end
607
+ end # End class Base
608
+ end # End module ActiveCouch