diff --git a/lib/couchrest/core/model.rb b/lib/couchrest/core/model.rb index 811d0fd..3c6d4da 100644 --- a/lib/couchrest/core/model.rb +++ b/lib/couchrest/core/model.rb @@ -62,268 +62,272 @@ module CouchRest init_doc end end - + class << self # this is the CouchRest::Database that model classes will use unless # they override it with use_database attr_accessor :default_database attr_accessor :template - + # override the CouchRest::Model-wide default_database - def use_database db - @database = db - end + def use_database db + @database = db + end - # returns the CouchRest::Database instance that this class uses - def database - @database || CouchRest::Model.default_database - end + # returns the CouchRest::Database instance that this class uses + def database + @database || CouchRest::Model.default_database + end - # load a document from the database - def get id - doc = database.get id - new(doc) - end + # load a document from the database + def get id + doc = database.get id + new(doc) + end - # Defines methods for reading and writing from fields in the document. - # Uses key_writer and key_reader internally. - def key_accessor *keys - key_writer *keys - key_reader *keys - end + def cast field, opts = {} + + end - # For each argument key, define a method key= that sets the - # corresponding field on the CouchDB document. - def key_writer *keys - keys.each do |method| - key = method.to_s - define_method "#{method}=" do |value| - self[key] = value - end - end - end + # Defines methods for reading and writing from fields in the document. + # Uses key_writer and key_reader internally. + def key_accessor *keys + key_writer *keys + key_reader *keys + end - # For each argument key, define a method key that reads the - # corresponding field on the CouchDB document. - def key_reader *keys - keys.each do |method| - key = method.to_s - define_method method do - self[key] - end - end - end - - def default - @default - end - - def set_default hash - @default = hash - end + # For each argument key, define a method key= that sets the + # corresponding field on the CouchDB document. + def key_writer *keys + keys.each do |method| + key = method.to_s + define_method "#{method}=" do |value| + self[key] = value + end + end + end - # Automatically set updated_at and created_at fields - # on the document whenever saving occurs. CouchRest uses a pretty - # decent time format by default. See Time#to_json - def timestamps! - before(:create) do - self['updated_at'] = self['created_at'] = Time.now - end - before(:update) do - self['updated_at'] = Time.now - end - end + # For each argument key, define a method key that reads the + # corresponding field on the CouchDB document. + def key_reader *keys + keys.each do |method| + key = method.to_s + define_method method do + self[key] + end + end + end - # Name a method that will be called before the document is first saved, - # which returns a string to be used for the document's _id. - # Because CouchDB enforces a constraint that each id must be unique, - # this can be used to enforce eg: uniq usernames. Note that this id - # must be globally unique across all document types which share a - # database, so if you'd like to scope uniqueness to this class, you - # should use the class name as part of the unique id. - def unique_id method = nil, &block - if method - define_method :set_unique_id do - self['_id'] ||= self.send(method) - end - elsif block - define_method :set_unique_id do - uniqid = block.call(self) - raise ArgumentError, "unique_id block must not return nil" if uniqid.nil? - self['_id'] ||= uniqid - end - end - end - - # Define a CouchDB view. The name of the view will be the concatenation - # of by and the keys joined by _and_ - # - # ==== Example views: - # - # class Post - # # view with default options - # # query with Post.by_date - # view_by :date, :descending => true - # - # # view with compound sort-keys - # # query with Post.by_user_id_and_date - # view_by :user_id, :date - # - # # view with custom map/reduce functions - # # query with Post.by_tags :reduce => true - # view_by :tags, - # :map => - # "function(doc) { - # if (doc.type == 'Post' && doc.tags) { - # doc.tags.forEach(function(tag){ - # emit(doc.tag, 1); - # }); - # } - # }", - # :reduce => - # "function(keys, values, rereduce) { - # return sum(values); - # }" - # end - # - # view_by :date will create a view defined by this Javascript - # function: - # - # function(doc) { - # if (doc.type == 'Post' && doc.date) { - # emit(doc.date, null); - # } - # } - # - # It can be queried by calling Post.by_date which accepts all - # valid options for CouchRest::Database#view. In addition, calling with - # the :raw => true option will return the view rows - # themselves. By default Post.by_date will return the - # documents included in the generated view. - # - # CouchRest::Database#view options can be applied at view definition - # time as defaults, and they will be curried and used at view query - # time. Or they can be overridden at query time. - # - # Custom views can be queried with :reduce => true to return - # reduce results. The default for custom views is to query with - # :reduce => false. - # - # Views are generated (on a per-model basis) lazily on first-access. - # This means that if you are deploying changes to a view, the views for - # that model won't be available until generation is complete. This can - # take some time with large databases. Strategies are in the works. - # - # To understand the capabilities of this view system more compeletly, - # it is recommended that you read the RSpec file at - # spec/core/model.rb. - def view_by *keys - opts = keys.pop if keys.last.is_a?(Hash) - opts ||= {} - type = self.to_s + def default + @default + end - method_name = "by_#{keys.join('_and_')}" - @@design_doc ||= default_design_doc + def set_default hash + @default = hash + end - if opts[:map] - view = {} - view['map'] = opts.delete(:map) - if opts[:reduce] - view['reduce'] = opts.delete(:reduce) - opts[:reduce] = false - end - @@design_doc['views'][method_name] = view - else - doc_keys = keys.collect{|k|"doc['#{k}']"} - key_protection = doc_keys.join(' && ') - key_emit = doc_keys.length == 1 ? "#{doc_keys.first}" : "[#{doc_keys.join(', ')}]" - map_function = <<-JAVASCRIPT - function(doc) { - if (doc.type == '#{type}' && #{key_protection}) { - emit(#{key_emit}, null); - } - } - JAVASCRIPT - @@design_doc['views'][method_name] = { - 'map' => map_function - } - end + # Automatically set updated_at and created_at fields + # on the document whenever saving occurs. CouchRest uses a pretty + # decent time format by default. See Time#to_json + def timestamps! + before(:create) do + self['updated_at'] = self['created_at'] = Time.now + end + before(:update) do + self['updated_at'] = Time.now + end + end - @@design_doc_fresh = false + # Name a method that will be called before the document is first saved, + # which returns a string to be used for the document's _id. + # Because CouchDB enforces a constraint that each id must be unique, + # this can be used to enforce eg: uniq usernames. Note that this id + # must be globally unique across all document types which share a + # database, so if you'd like to scope uniqueness to this class, you + # should use the class name as part of the unique id. + def unique_id method = nil, &block + if method + define_method :set_unique_id do + self['_id'] ||= self.send(method) + end + elsif block + define_method :set_unique_id do + uniqid = block.call(self) + raise ArgumentError, "unique_id block must not return nil" if uniqid.nil? + self['_id'] ||= uniqid + end + end + end - self.meta_class.instance_eval do - define_method method_name do |*args| - query = opts.merge(args[0] || {}) - query[:raw] = true if query[:reduce] - unless @@design_doc_fresh - refresh_design_doc - end - raw = query.delete(:raw) - view_name = "#{type}/#{method_name}" + # Define a CouchDB view. The name of the view will be the concatenation + # of by and the keys joined by _and_ + # + # ==== Example views: + # + # class Post + # # view with default options + # # query with Post.by_date + # view_by :date, :descending => true + # + # # view with compound sort-keys + # # query with Post.by_user_id_and_date + # view_by :user_id, :date + # + # # view with custom map/reduce functions + # # query with Post.by_tags :reduce => true + # view_by :tags, + # :map => + # "function(doc) { + # if (doc.type == 'Post' && doc.tags) { + # doc.tags.forEach(function(tag){ + # emit(doc.tag, 1); + # }); + # } + # }", + # :reduce => + # "function(keys, values, rereduce) { + # return sum(values); + # }" + # end + # + # view_by :date will create a view defined by this Javascript + # function: + # + # function(doc) { + # if (doc.type == 'Post' && doc.date) { + # emit(doc.date, null); + # } + # } + # + # It can be queried by calling Post.by_date which accepts all + # valid options for CouchRest::Database#view. In addition, calling with + # the :raw => true option will return the view rows + # themselves. By default Post.by_date will return the + # documents included in the generated view. + # + # CouchRest::Database#view options can be applied at view definition + # time as defaults, and they will be curried and used at view query + # time. Or they can be overridden at query time. + # + # Custom views can be queried with :reduce => true to return + # reduce results. The default for custom views is to query with + # :reduce => false. + # + # Views are generated (on a per-model basis) lazily on first-access. + # This means that if you are deploying changes to a view, the views for + # that model won't be available until generation is complete. This can + # take some time with large databases. Strategies are in the works. + # + # To understand the capabilities of this view system more compeletly, + # it is recommended that you read the RSpec file at + # spec/core/model.rb. + def view_by *keys + opts = keys.pop if keys.last.is_a?(Hash) + opts ||= {} + type = self.to_s - view = fetch_view(view_name, query) - if raw - view - else - # TODO this can be optimized once the include-docs patch is applied - view['rows'].collect{|r|new(database.get(r['id']))} - end - end - end - end + method_name = "by_#{keys.join('_and_')}" + @@design_doc ||= default_design_doc - private + if opts[:map] + view = {} + view['map'] = opts.delete(:map) + if opts[:reduce] + view['reduce'] = opts.delete(:reduce) + opts[:reduce] = false + end + @@design_doc['views'][method_name] = view + else + doc_keys = keys.collect{|k|"doc['#{k}']"} + key_protection = doc_keys.join(' && ') + key_emit = doc_keys.length == 1 ? "#{doc_keys.first}" : "[#{doc_keys.join(', ')}]" + map_function = <<-JAVASCRIPT + function(doc) { + if (doc.type == '#{type}' && #{key_protection}) { + emit(#{key_emit}, null); + } + } + JAVASCRIPT + @@design_doc['views'][method_name] = { + 'map' => map_function + } + end - def fetch_view view_name, opts - retryable = true - begin - database.view(view_name, opts) - # the design doc could have been deleted by a rouge process - rescue RestClient::ResourceNotFound => e - if retryable - refresh_design_doc - retryable = false - retry - else - raise e - end - end - end + @@design_doc_fresh = false - def design_doc_id - "_design/#{self.to_s}" - end + self.meta_class.instance_eval do + define_method method_name do |*args| + query = opts.merge(args[0] || {}) + query[:raw] = true if query[:reduce] + unless @@design_doc_fresh + refresh_design_doc + end + raw = query.delete(:raw) + view_name = "#{type}/#{method_name}" - def default_design_doc - { - "_id" => design_doc_id, - "language" => "javascript", - "views" => {} - } - end + view = fetch_view(view_name, query) + if raw + view + else + # TODO this can be optimized once the include-docs patch is applied + view['rows'].collect{|r|new(database.get(r['id']))} + end + end + end + end - def refresh_design_doc - saved = database.get(design_doc_id) rescue nil - if saved - @@design_doc['views'].each do |name, view| - saved['views'][name] = view - end - database.save(saved) - else - database.save(@@design_doc) - end - @@design_doc_fresh = true - end + private + + def fetch_view view_name, opts + retryable = true + begin + database.view(view_name, opts) + # the design doc could have been deleted by a rouge process + rescue RestClient::ResourceNotFound => e + if retryable + refresh_design_doc + retryable = false + retry + else + raise e + end + end + end + + def design_doc_id + "_design/#{self.to_s}" + end + + def default_design_doc + { + "_id" => design_doc_id, + "language" => "javascript", + "views" => {} + } + end + + def refresh_design_doc + saved = database.get(design_doc_id) rescue nil + if saved + @@design_doc['views'].each do |name, view| + saved['views'][name] = view + end + database.save(saved) + else + database.save(@@design_doc) + end + @@design_doc_fresh = true + end end # class << self - - + + # returns the database used by this model's class def database self.class.database end - + # alias for self['_id'] def id self['_id'] @@ -333,12 +337,12 @@ module CouchRest def rev self['_rev'] end - + # returns true if the document has never been saved def new_record? !rev end - + # Saves the document to the db using create or update. Also runs the :save # callbacks. Sets the _id and _rev fields based on # CouchDB's response. @@ -363,21 +367,21 @@ module CouchRest end protected - + # Saves a document for the first time, after running the before(:create) # callbacks, and applying the unique_id. def create set_unique_id if respond_to?(:set_unique_id) # hack save_doc end - + # Saves the document and runs the :update callbacks. def update save_doc end - + private - + def save_doc result = database.save self if result['ok'] @@ -386,13 +390,13 @@ module CouchRest end result['ok'] end - + def init_doc self['type'] = self.class.to_s end - + include ::Extlib::Hook register_instance_hooks :save, :create, :update, :destroy - + end # class Model end # module CouchRest \ No newline at end of file