public
Description: Ruby interface to CouchDB
Homepage: http://code.google.com/p/activecouch/
Clone URL: git://github.com/JackDanger/active_couch.git
arun.thampi (author)
Sat Feb 02 22:44:18 -0800 2008
commit  8b4e9c074f31df170a41605520f5e218be9e43a8
tree    fbac9eca0b8f2932e304095e2dfaae19590ecd63
parent  36a0f6f74f5c591b1695c83104de4ef424cc89f5
active_couch / lib / active_couch / base.rb
100644 522 lines (484 sloc) 19.009 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
module ActiveCouch
  class Base
    SPECIAL_MEMBERS = %w(attributes associations connection callbacks)
    DEFAULT_ATTRIBUTES = %w(id rev)
    
    # Initializes an ActiveCouch::Base object. The constructor accepts both a hash, as well as
    # a block to initialize attributes
    #
    # Examples:
    # class Person < ActiveCouch::Base
    # has :name
    # end
    #
    # person1 = Person.new(:name => "McLovin")
    # person1.name # => "McLovin"
    #
    # person2 = Person.new do |p|
    # p.name = "Seth"
    # end
    # person2.name # => "Seth"
    def initialize(params = {})
      # Object instance variable
      @attributes = {}; @associations = {}; @callbacks = Hash.new; @connection = self.class.connection
      klass_atts = self.class.attributes; klass_assocs = self.class.associations; klass_callbacks = self.class.callbacks
      # ActiveCouch::Connection object will be readable in every
      # object instantiated from a subclass of ActiveCouch::Base
      SPECIAL_MEMBERS.each do |k|
        self.instance_eval "def #{k}; @#{k}; end"
      end
      
      klass_atts.each_key do |k|
        @attributes[k] = klass_atts[k].dup
        self.instance_eval "def #{k}; attributes[:#{k}].value; end"
        self.instance_eval "def #{k}=(val); attributes[:#{k}].value = val; end"
      end
      
      klass_assocs.each_key do |k|
        @associations[k] = HasManyAssociation.new(klass_assocs[k].name, :class => klass_assocs[k].klass)
        self.instance_eval "def #{k}; associations[:#{k}].container; end"
        # If you have has_many :people, this will add a method called add_person to the object instantiated
        # from the class
        self.instance_eval "def add_#{k.singularize}(val); associations[:#{k}].push(val); end"
      end
      
      klass_callbacks.each_key do |k|
        @callbacks[k] = klass_callbacks[k].dup
      end
      
      DEFAULT_ATTRIBUTES.each do |x|
        self.instance_eval "def #{x}; _#{x}; end"
        self.instance_eval "def #{x}=(val); self._#{x}=(val); end"
      end
      
      # Set any instance variables if any, which are present in the params hash
      from_hash(params)
      # Now you can do stuff like
      # Person.new do |p|
      # p.name = 'McLovin'
      # end
      yield self if block_given?
    end
 
    # Generates a JSON representation of an instance of a subclass of ActiveCouch::Base.
    # Ignores attributes which have a nil value.
    #
    # Examples:
    # class Person < ActiveCouch::Base
    # has :name, :which_is => :text, :with_default_value => "McLovin"
    # end
    #
    # person = Person.new
    # person.to_json # {"name":"McLovin"}
    #
    # class AgedPerson < ActiveCouch::Base
    # has :age, :which_is => :decimal, :with_default_value => 3.5
    # end
    #
    # aged_person = AgedPerson.new
    # aged_person.id = 'abc-def'
    # aged_person.to_json # {"age":3.5, "_id":"abc-def"}
    def to_json
      hash = {}
      
      attributes.each_value { |v| hash.merge!(v.to_hash) unless v.nil? }
      associations.each_value { |v| hash.merge!(v.to_hash) }
      # and by the Power of Grayskull, convert the hash to json
      hash.to_json
    end
 
    # Saves a document into a CouchDB database. A document can be saved in two ways.
    # One if it has been set an ID by the user, in which case the connection object
    # needs to use an HTTP PUT request to the URL /database/user_generated_id.
    # For the document needs a CouchDB-generated ID, the connection object needs
    # to use an HTTP POST request to the URL /database.
    #
    # Examples:
    # class Person < ActiveCouch::Base
    # has :name, :which_is => :text
    # end
    #
    # person = Person.new(:name => 'McLovin')
    # person.id = 'abc'
    # person.save # true
    # person.new? # false
    def save
      if id
        response = connection.put("/#{self.class.database_name}/#{id}", to_json)
      else
        response = connection.post("/#{self.class.database_name}", to_json)
      end
      # Parse the JSON obtained from the body...
      results = JSON.parse(response.body)
      # ...and set the default id and rev attributes
      DEFAULT_ATTRIBUTES.each { |a| self.__send__("#{a}=", results[a]) }
      # Response sent will be 201, if the save was successful [201 corresponds to 'created']
      return response.code == '201'
    end
 
    # Checks to see if a document has been persisted in a CouchDB database.
    # If a document has been retrieved from CouchDB, or has been persisted in
    # a CouchDB database, the attribute _rev would not be nil.
    #
    # Examples:
    # class Person < ActiveCouch::Base
    # has :name, :which_is => :text
    # end
    #
    # person = Person.new(:name => 'McLovin')
    # person.id = 'abc'
    # person.save # true
    # person.new? # false
    def new?
      rev.nil?
    end
 
    # Deletes a document from a CouchDB database. This is an instance-level delete method.
    # Example:
    # class Person < ActiveCouch::Base
    # has :name
    # end
    #
    # person = Person.create(:name => 'McLovin')
    # person.delete # true
    def delete
      if new?
        raise ArgumentError, "You must specify a revision for the document to be deleted"
      elsif id.nil?
        raise ArgumentError, "You must specify an ID for the document to be deleted"
      end
      response = connection.delete("/#{self.class.database_name}/#{id}?rev=#{rev}")
      # Set the id and rev to nil, since the object has been successfully deleted from CouchDB
      if response.code == '202'
        self.id = nil; self.rev = nil
        true
      else
        false
      end
    end
 
    def marshal_dump # :nodoc:
      self.to_json
    end
 
    def marshal_load(str) # :nodoc:
      self.instance_eval do
        hash = JSON.parse(str)
        initialize(hash)
      end
      self
    end
    
    class << self # Class methods
 
      # Returns the CouchDB database name that's backing this model. The database name is guessed from the name of the
      # class somewhat similar to ActiveRecord conventions.
      #
      # Examples:
      # class Invoice < ActiveCouch::Base; end;
      # file class database_name
      # invoice.rb Invoice invoices
      #
      # class Invoice < ActiveCouch::Base; class Lineitem < ActiveCouch::Base; end; end;
      # file class database_name
      # invoice.rb Invoice::Lineitem invoice_lineitems
      #
      # module Invoice; class Lineitem < ActiveCouch::Base; end; end;
      # file class database_name
      # invoice/lineitem.rb Invoice::Lineitem lineitems
      #
      # You can override this method or use <tt>set_database_name</tt> to override this class method to allow for names
      # that can't be inferred.
      def database_name
        base = base_class
        name = (unless self == base
          base.database_name
        else
          # Nested classes are prefixed with singular parent database name.
          if parent < ActiveCouch::Base
            contained = parent.database_name.singularize
            contained << '_'
          end
          "#{contained}#{base.name.pluralize.demodulize.underscore}"
        end)
        set_database_name(name)
        name
      end
 
      # Sets the database name to the given value, or (if the value is nil or false) to the value returned by the
      # given block. Useful for setting database names that can't be automatically inferred from the class name.
      #
      # This method is aliased as <tt>database_name=</tt>.
      #
      # Example:
      #
      # class Post < ActiveCouch::Base
      # set_database_name 'legacy_posts'
      # end
      def set_database_name(database = nil, &block)
        define_attr_method(:database_name, database, &block)
      end
      alias :database_name= :set_database_name
 
      # Sets the site which the ActiveCouch object has to connect to, which
      # initializes an ActiveCouch::Connection object.
      #
      # Example:
      # class Person < ActiveCouch::Base
      # site 'localhost:5984'
      # end
      #
      # Person.connection.nil? # false
      def site(site)
        @connection = Connection.new(site)
      end
 
      # Defines an attribute for a subclass of ActiveCouch::Base. The parameters
      # for this method include name, which is the name of the attribute as well as
      # an options hash.
      #
      # The options hash can contain the key 'which_is' which can
      # have possible values :text, :decimal, :number. It can also contain the key
      # 'with_default_value' which can set a default value for each attribute defined
      # in the subclass of ActiveCouch::Base
      #
      # Examples:
      # class Person < ActiveCouch::Base
      # has :name
      # end
      #
      # person = Person.new
      # p.name.methods.include?(:name) # true
      # p.name.methods.include?(:name=) # false
      #
      # class AgedPerson < ActiveCouch::Base
      # has :age, :which_is => :number, :with_default_value = 18
      # end
      #
      # person = AgedPerson.new
      # person.age # 18
      def has(name, options = {})
        unless name.is_a?(String) || name.is_a?(Symbol)
          raise ArgumentError, "#{name} is neither a String nor a Symbol"
        end
        @attributes[name] = Attribute.new(name, options)
      end
 
      # Defines an array of objects which are 'children' of this class. The has_many
      # function guesses the class of the child, based on the name of the association,
      # but can be over-ridden by the :class key in the options hash.
      #
      # Examples:
      #
      # class Person < ActiveCouch::Base
      # has :name
      # end
      #
      # class GrandPerson < ActiveCouch::Base
      # has_many :people # which will create an empty array which can contain
      # # Person objects
      # end
      def has_many(name, options = {})
        unless name.is_a?(String) || name.is_a?(Symbol)
          raise ArgumentError, "#{name} is neither a String nor a Symbol"
        end
        @associations[name] = HasManyAssociation.new(name, options)
      end
 
      # Initializes an object of a subclass of ActiveCouch::Base based on a JSON
      # representation of the object.
      #
      # Example:
      # class Person < ActiveCouch::Base
      # has :name
      # end
      #
      # person = Person.from_json('{"name":"McLovin"}')
      # person.name # "McLovin"
      def from_json(json)
        hash = JSON.parse(json)
        # Create new based on parsed
        self.new(hash)
      end
 
      # Retrieves one or more object(s) from a CouchDB database, based on the search
      # parameters given.
      #
      # Example:
      # class Person < ActiveCouch::Base
      # has :name
      # end
      #
      # # This returns a single instance of an ActiveCouch::Base subclass
      # people = Person.find(:first, :params => {:name => "McLovin"})
      #
      # # This returns an array of ActiveCouch::Base subclass instances
      # person = Person.find(:all, :params => {:name => "McLovin"})
      def find(*arguments)
        scope = arguments.slice!(0)
        options = arguments.slice!(0) || {}
        
        case scope
          when :all then find_every(options)
          when :first then find_every(options).first
          else find_one(scope)
        end
      end
 
      # Retrieves the count of the number of objects in the CouchDB database, based on the
      # search parameters given.
      #
      # Example:
      # class Person < ActiveCouch::Base
      # has :name
      # end
      #
      # # This returns the count of the number of objects
      # people_count = Person.count(:params => {:name => "McLovin"})
      def count(params = {})
        result_set = find(:all, params)
        result_set.size
      end
 
      # Initializes a new subclass of ActiveCouch::Base and saves in the CouchDB database
      # as a new document
      #
      # Example:
      # class Person < ActiveCouch::Base
      # has :name
      # end
      #
      # person = Person.create(:name => "McLovin")
      # person.id.nil? # false
      # person.new? # false
      def create(arguments)
        unless arguments.is_a?(Hash)
          raise ArgumentError, "The arguments must be a Hash"
        else
          new_record = self.new(arguments)
          new_record.save
          new_record
        end
      end
 
      # Deletes a document from the CouchDB database, based on the id and rev parameters passed to it.
      # Returns true if the document has been deleted
      #
      # Example:
      # class Person < ActiveCouch::Base
      # has :name
      # end
      #
      # Person.delete(:id => 'abc-def', :rev => '1235')
      def delete(options = {})
        if options.nil? || !options.has_key?(:id) || !options.has_key?(:rev)
          raise ArgumentError, "You must specify both an id and a rev for the document to be deleted"
        end
        response = connection.delete("/#{self.database_name}/#{options[:id]}?rev=#{options[:rev]}")
        # Returns true if the
        response.code == '202'
      end
 
      # Defines an "attribute" method. A new (class) method will be created with the
      # given name. If a value is specified, the new method will
      # return that value (as a string). Otherwise, the given block
      # will be used to compute the value of the method.
      #
      # The original method, if it exists, will be aliased, with the
      # new name being
      # prefixed with "original_". This allows the new method to
      # access the original value.
      #
      # This method is stolen from ActiveRecord.
      #
      # Example:
      #
      # class Foo < ActiveCouch::Base
      # define_attr_method :database_name, 'foo'
      # # OR
      # define_attr_method(:database_name) do
      # original_database_name + '_legacy'
      # end
      # end
      def define_attr_method(name, value = nil, &block)
        metaclass.send(:alias_method, "original_#{name}", name)
        if block_given?
          meta_def name, &block
        else
          metaclass.class_eval "def #{name}; #{value.to_s.inspect}; end"
        end
      end
 
      def inherited(subklass)
        subklass.class_eval do
          include ActiveCouch::Callbacks
        end
        
        # TODO: Need a cleaner way to do this
        subklass.instance_variable_set "@attributes", { :_id => Attribute.new(:_id, :with_default_value => nil),
                                                        :_rev => Attribute.new(:_rev, :with_default_value => nil) }
        subklass.instance_variable_set "@associations", {}
        subklass.instance_variable_set "@callbacks", Hash.new([])
        subklass.instance_variable_set "@connections", nil
                                                        
        SPECIAL_MEMBERS.each do |k|
          subklass.instance_eval "def #{k}; @#{k}; end"
        end
      end
 
      def base_class
        class_of_active_couch_descendant(self)
      end
 
      private
      
        # Returns the class descending directly from ActiveCouch in the inheritance hierarchy.
        def class_of_active_couch_descendant(klass)
          if klass.superclass == Base
            klass
          elsif klass.superclass.nil?
            raise ActiveCouchError, "#{name} doesn't belong in a hierarchy descending from ActiveCouch"
          else
            class_of_active_couch_descendant(klass.superclass)
          end
        end
        
        # Returns an array of ActiveCouch::Base objects by querying a CouchDB permanent view
        def find_every(options)
          case from = options[:from]
          when String
            path = "#{from}"
          else
            path = "/#{database_name}/_view/#{query_string(options[:params])}"
          end
          instantiate_collection(connection.get(path))
        end
        
        def find_one(id)
          path = "/#{database_name}/#{id}"
          begin
            instantiate_object(connection.get(path))
          rescue ResourceNotFound
            nil
          end
        end
        
        # Generates a query string by using the ActiveCouch convention, which is to
        # have the view defined by pre-pending the attribute to be queried with 'by_'
        # So for example, if the params hash is :name => 'McLovin',
        # the view associated with it will be /by_name/by_name?key="McLovin"
        def query_string(params)
          # TODO : Shouldn't check for multiple arguments
          if params.is_a?(Hash)
            params.each { |k,v| return "by_#{k}/by_#{k}?key=#{v.to_s.url_encode}" }
          else
            raise ArgumentError, "The value for the key 'params' must be a Hash"
          end
        end
        
        # Instantiates a collection of ActiveCouch::Base objects, based on the
        # result obtained from a CouchDB View.
        #
        # As per the CouchDB Permanent View API, the result set will be contained
        # within a JSON hash as an array, with the key 'rows'
        # The actual CouchDB object which needs to be initialized is obtained with
        # the key 'value'
        def instantiate_collection(result)
          hash = JSON.parse(result)
          hash['rows'].collect { |row| self.new(row['value']) }
        end
        
        # Instantiates an ActiveCouch::Base object, based on the result obtained from
        # the GET URL
        def instantiate_object(result)
          hash = JSON.parse(result)
          self.new(hash)
        end
    end # End class methods
    
    private
      def from_hash(hash)
        # TODO:
        # - Clean this up. Doesn't look very nice
        # - Raise errors if attribute/association is not present
        hash.each do |k,v|
          k = k.to_sym rescue k
          if v.is_a?(Array) # This means this is a has_many association
            unless (assoc = @associations[k]).nil?
              name, child_klass = assoc.name, assoc.klass
              v.each do |child|
                child.is_a?(Hash) ? child_obj = child_klass.new(child) : child_obj = child
                self.send "add_#{name.singularize}", child_obj
              end
            end
          elsif v.is_a?(Hash) # This means this is a has_one association (which we might add later)
            # Do nothing for now. More later
          else # This means this is a normal attribute
            self.send("#{k}=", v) if respond_to?("#{k}=") # @attributes.has_key?(k) || @attributes.has_key?("_#{k}")
          end
        end
      end
  end # End class Base
end # End module ActiveCouch