From fe489f2d389b34c210fc570e512cfaee20b78068 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Tue, 24 Feb 2009 22:51:13 -0800 Subject: [PATCH] removed CouchRest::Model, added more specs and fixed a bug with casted CR::ExtendedDocument --- README.md | 8 +- couchrest.gemspec | 4 +- examples/model/example.rb | 32 +- lib/couchrest.rb | 2 +- lib/couchrest/core/database.rb | 2 +- lib/couchrest/core/model.rb | 615 ------------- lib/couchrest/more/extended_document.rb | 3 +- lib/couchrest/more/property.rb | 2 + spec/couchrest/core/database_spec.rb | 2 +- spec/couchrest/core/model_spec.rb | 856 ------------------ .../more/extended_doc_attachment_spec.rb | 129 +++ spec/couchrest/more/extended_doc_spec.rb | 384 +++++++- spec/couchrest/more/extended_doc_view_spec.rb | 204 +++++ spec/couchrest/more/property_spec.rb | 21 +- spec/spec_helper.rb | 4 + 15 files changed, 740 insertions(+), 1528 deletions(-) delete mode 100644 lib/couchrest/core/model.rb delete mode 100644 spec/couchrest/core/model_spec.rb create mode 100644 spec/couchrest/more/extended_doc_attachment_spec.rb create mode 100644 spec/couchrest/more/extended_doc_view_spec.rb diff --git a/README.md b/README.md index 0510efe..6315426 100644 --- a/README.md +++ b/README.md @@ -59,13 +59,7 @@ Creating and Querying Views: ## CouchRest::Model -CouchRest::Model is a module designed along the lines of DataMapper::Resource. -By subclassing, suddenly you get all sorts of powerful sugar, so that working -with CouchDB in your Rails or Merb app is no harder than working with the -standard SQL alternatives. See the CouchRest::Model documentation for an -example article class that illustrates usage. - -CouchRest::Model will be removed from this package. +CouchRest::Model has been deprecated and replaced by CouchRest::ExtendedDocument ## CouchRest::ExtendedDocument diff --git a/couchrest.gemspec b/couchrest.gemspec index a759d34..30e727f 100644 --- a/couchrest.gemspec +++ b/couchrest.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = %q{couchrest} - s.version = "0.14.2" + s.version = "0.15" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["J. Chris Anderson", "Matt Aimonetti"] @@ -10,7 +10,7 @@ Gem::Specification.new do |s| s.description = %q{CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments.} s.email = %q{jchris@apache.org} s.extra_rdoc_files = ["README.md", "LICENSE", "THANKS.md"] - s.files = ["LICENSE", "README.md", "Rakefile", "THANKS.md", "examples/model", "examples/model/example.rb", "examples/word_count", "examples/word_count/markov", "examples/word_count/views", "examples/word_count/views/books", "examples/word_count/views/books/chunked-map.js", "examples/word_count/views/books/united-map.js", "examples/word_count/views/markov", "examples/word_count/views/markov/chain-map.js", "examples/word_count/views/markov/chain-reduce.js", "examples/word_count/views/word_count", "examples/word_count/views/word_count/count-map.js", "examples/word_count/views/word_count/count-reduce.js", "examples/word_count/word_count.rb", "examples/word_count/word_count_query.rb", "examples/word_count/word_count_views.rb", "lib/couchrest", "lib/couchrest/commands", "lib/couchrest/commands/generate.rb", "lib/couchrest/commands/push.rb", "lib/couchrest/core", "lib/couchrest/core/database.rb", "lib/couchrest/core/design.rb", "lib/couchrest/core/document.rb", "lib/couchrest/core/model.rb", "lib/couchrest/core/response.rb", "lib/couchrest/core/server.rb", "lib/couchrest/core/view.rb", "lib/couchrest/helper", "lib/couchrest/helper/pager.rb", "lib/couchrest/helper/streamer.rb", "lib/couchrest/mixins", "lib/couchrest/mixins/attachments.rb", "lib/couchrest/mixins/callbacks.rb", "lib/couchrest/mixins/design_doc.rb", "lib/couchrest/mixins/document_queries.rb", "lib/couchrest/mixins/extended_attachments.rb", "lib/couchrest/mixins/extended_document_mixins.rb", "lib/couchrest/mixins/properties.rb", "lib/couchrest/mixins/validation.rb", "lib/couchrest/mixins/views.rb", "lib/couchrest/mixins.rb", "lib/couchrest/monkeypatches.rb", "lib/couchrest/more", "lib/couchrest/more/casted_model.rb", "lib/couchrest/more/extended_document.rb", "lib/couchrest/more/property.rb", "lib/couchrest/support", "lib/couchrest/support/blank.rb", "lib/couchrest/support/class.rb", "lib/couchrest/validation", "lib/couchrest/validation/auto_validate.rb", "lib/couchrest/validation/contextual_validators.rb", "lib/couchrest/validation/validation_errors.rb", "lib/couchrest/validation/validators", "lib/couchrest/validation/validators/absent_field_validator.rb", "lib/couchrest/validation/validators/confirmation_validator.rb", "lib/couchrest/validation/validators/format_validator.rb", "lib/couchrest/validation/validators/formats", "lib/couchrest/validation/validators/formats/email.rb", "lib/couchrest/validation/validators/formats/url.rb", "lib/couchrest/validation/validators/generic_validator.rb", "lib/couchrest/validation/validators/length_validator.rb", "lib/couchrest/validation/validators/method_validator.rb", "lib/couchrest/validation/validators/numeric_validator.rb", "lib/couchrest/validation/validators/required_field_validator.rb", "lib/couchrest.rb", "spec/couchrest", "spec/couchrest/core", "spec/couchrest/core/couchrest_spec.rb", "spec/couchrest/core/database_spec.rb", "spec/couchrest/core/design_spec.rb", "spec/couchrest/core/document_spec.rb", "spec/couchrest/core/model_spec.rb", "spec/couchrest/core/server_spec.rb", "spec/couchrest/helpers", "spec/couchrest/helpers/pager_spec.rb", "spec/couchrest/helpers/streamer_spec.rb", "spec/couchrest/more", "spec/couchrest/more/casted_model_spec.rb", "spec/couchrest/more/extended_doc_spec.rb", "spec/couchrest/more/property_spec.rb", "spec/fixtures", "spec/fixtures/attachments", "spec/fixtures/attachments/couchdb.png", "spec/fixtures/attachments/README", "spec/fixtures/attachments/test.html", "spec/fixtures/couchapp", "spec/fixtures/couchapp/_attachments", "spec/fixtures/couchapp/_attachments/index.html", "spec/fixtures/couchapp/doc.json", "spec/fixtures/couchapp/foo", "spec/fixtures/couchapp/foo/bar.txt", "spec/fixtures/couchapp/foo/test.json", "spec/fixtures/couchapp/test.json", "spec/fixtures/couchapp/views", "spec/fixtures/couchapp/views/example-map.js", "spec/fixtures/couchapp/views/example-reduce.js", "spec/fixtures/couchapp-test", "spec/fixtures/couchapp-test/my-app", "spec/fixtures/couchapp-test/my-app/_attachments", "spec/fixtures/couchapp-test/my-app/_attachments/index.html", "spec/fixtures/couchapp-test/my-app/foo", "spec/fixtures/couchapp-test/my-app/foo/bar.txt", "spec/fixtures/couchapp-test/my-app/views", "spec/fixtures/couchapp-test/my-app/views/example-map.js", "spec/fixtures/couchapp-test/my-app/views/example-reduce.js", "spec/fixtures/more", "spec/fixtures/more/card.rb", "spec/fixtures/more/invoice.rb", "spec/fixtures/more/service.rb", "spec/fixtures/views", "spec/fixtures/views/lib.js", "spec/fixtures/views/test_view", "spec/fixtures/views/test_view/lib.js", "spec/fixtures/views/test_view/only-map.js", "spec/fixtures/views/test_view/test-map.js", "spec/fixtures/views/test_view/test-reduce.js", "spec/spec.opts", "spec/spec_helper.rb", "utils/remap.rb", "utils/subset.rb"] + s.files = ["LICENSE", "README.md", "Rakefile", "THANKS.md", "examples/model", "examples/model/example.rb", "examples/word_count", "examples/word_count/markov", "examples/word_count/views", "examples/word_count/views/books", "examples/word_count/views/books/chunked-map.js", "examples/word_count/views/books/united-map.js", "examples/word_count/views/markov", "examples/word_count/views/markov/chain-map.js", "examples/word_count/views/markov/chain-reduce.js", "examples/word_count/views/word_count", "examples/word_count/views/word_count/count-map.js", "examples/word_count/views/word_count/count-reduce.js", "examples/word_count/word_count.rb", "examples/word_count/word_count_query.rb", "examples/word_count/word_count_views.rb", "lib/couchrest", "lib/couchrest/commands", "lib/couchrest/commands/generate.rb", "lib/couchrest/commands/push.rb", "lib/couchrest/core", "lib/couchrest/core/database.rb", "lib/couchrest/core/design.rb", "lib/couchrest/core/document.rb", "lib/couchrest/core/response.rb", "lib/couchrest/core/server.rb", "lib/couchrest/core/view.rb", "lib/couchrest/helper", "lib/couchrest/helper/pager.rb", "lib/couchrest/helper/streamer.rb", "lib/couchrest/mixins", "lib/couchrest/mixins/attachments.rb", "lib/couchrest/mixins/callbacks.rb", "lib/couchrest/mixins/design_doc.rb", "lib/couchrest/mixins/document_queries.rb", "lib/couchrest/mixins/extended_attachments.rb", "lib/couchrest/mixins/extended_document_mixins.rb", "lib/couchrest/mixins/properties.rb", "lib/couchrest/mixins/validation.rb", "lib/couchrest/mixins/views.rb", "lib/couchrest/mixins.rb", "lib/couchrest/monkeypatches.rb", "lib/couchrest/more", "lib/couchrest/more/casted_model.rb", "lib/couchrest/more/extended_document.rb", "lib/couchrest/more/property.rb", "lib/couchrest/support", "lib/couchrest/support/blank.rb", "lib/couchrest/support/class.rb", "lib/couchrest/validation", "lib/couchrest/validation/auto_validate.rb", "lib/couchrest/validation/contextual_validators.rb", "lib/couchrest/validation/validation_errors.rb", "lib/couchrest/validation/validators", "lib/couchrest/validation/validators/absent_field_validator.rb", "lib/couchrest/validation/validators/confirmation_validator.rb", "lib/couchrest/validation/validators/format_validator.rb", "lib/couchrest/validation/validators/formats", "lib/couchrest/validation/validators/formats/email.rb", "lib/couchrest/validation/validators/formats/url.rb", "lib/couchrest/validation/validators/generic_validator.rb", "lib/couchrest/validation/validators/length_validator.rb", "lib/couchrest/validation/validators/method_validator.rb", "lib/couchrest/validation/validators/numeric_validator.rb", "lib/couchrest/validation/validators/required_field_validator.rb", "lib/couchrest.rb", "spec/couchrest", "spec/couchrest/core", "spec/couchrest/core/couchrest_spec.rb", "spec/couchrest/core/database_spec.rb", "spec/couchrest/core/design_spec.rb", "spec/couchrest/core/document_spec.rb", "spec/couchrest/core/server_spec.rb", "spec/couchrest/helpers", "spec/couchrest/helpers/pager_spec.rb", "spec/couchrest/helpers/streamer_spec.rb", "spec/couchrest/more", "spec/couchrest/more/casted_model_spec.rb", "spec/couchrest/more/extended_doc_attachment_spec.rb", "spec/couchrest/more/extended_doc_spec.rb", "spec/couchrest/more/extended_doc_view_spec.rb", "spec/couchrest/more/property_spec.rb", "spec/fixtures", "spec/fixtures/attachments", "spec/fixtures/attachments/couchdb.png", "spec/fixtures/attachments/README", "spec/fixtures/attachments/test.html", "spec/fixtures/couchapp", "spec/fixtures/couchapp/_attachments", "spec/fixtures/couchapp/_attachments/index.html", "spec/fixtures/couchapp/doc.json", "spec/fixtures/couchapp/foo", "spec/fixtures/couchapp/foo/bar.txt", "spec/fixtures/couchapp/foo/test.json", "spec/fixtures/couchapp/test.json", "spec/fixtures/couchapp/views", "spec/fixtures/couchapp/views/example-map.js", "spec/fixtures/couchapp/views/example-reduce.js", "spec/fixtures/couchapp-test", "spec/fixtures/couchapp-test/my-app", "spec/fixtures/couchapp-test/my-app/_attachments", "spec/fixtures/couchapp-test/my-app/_attachments/index.html", "spec/fixtures/couchapp-test/my-app/foo", "spec/fixtures/couchapp-test/my-app/foo/bar.txt", "spec/fixtures/couchapp-test/my-app/views", "spec/fixtures/couchapp-test/my-app/views/example-map.js", "spec/fixtures/couchapp-test/my-app/views/example-reduce.js", "spec/fixtures/more", "spec/fixtures/more/article.rb", "spec/fixtures/more/card.rb", "spec/fixtures/more/course.rb", "spec/fixtures/more/event.rb", "spec/fixtures/more/invoice.rb", "spec/fixtures/more/person.rb", "spec/fixtures/more/question.rb", "spec/fixtures/more/service.rb", "spec/fixtures/views", "spec/fixtures/views/lib.js", "spec/fixtures/views/test_view", "spec/fixtures/views/test_view/lib.js", "spec/fixtures/views/test_view/only-map.js", "spec/fixtures/views/test_view/test-map.js", "spec/fixtures/views/test_view/test-reduce.js", "spec/spec.opts", "spec/spec_helper.rb", "utils/remap.rb", "utils/subset.rb"] s.has_rdoc = true s.homepage = %q{http://github.com/jchris/couchrest} s.require_paths = ["lib"] diff --git a/examples/model/example.rb b/examples/model/example.rb index 94c841b..10d1536 100644 --- a/examples/model/example.rb +++ b/examples/model/example.rb @@ -1,31 +1,38 @@ -require 'rubygems' -require 'couchrest' +require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'couchrest') def show obj puts obj.inspect puts end -CouchRest::Model.default_database = CouchRest.database!('couchrest-model-example') +SERVER = CouchRest.new +SERVER.default_database = 'couchrest-extendeddoc-example' -class Author < CouchRest::Model - key_accessor :name +class Author < CouchRest::ExtendedDocument + use_database SERVER.default_database + property :name + def drink_scotch puts "... glug type glug ... I'm #{name} ... type glug glug ..." end end -class Post < CouchRest::Model - key_accessor :title, :body, :author - - cast :author, :as => 'Author' +class Post < CouchRest::ExtendedDocument + use_database SERVER.default_database + + property :title + property :body + property :author, :cast_as => 'Author' timestamps! end -class Comment < CouchRest::Model - cast :commenter, :as => 'Author' - +class Comment < CouchRest::ExtendedDocument + use_database SERVER.default_database + + property :commenter, :cast_as => 'Author' + timestamps! + def post= post self["post_id"] = post.id end @@ -33,7 +40,6 @@ class Comment < CouchRest::Model Post.get(self['post_id']) if self['post_id'] end - timestamps! end puts "Act I: CRUD" diff --git a/lib/couchrest.rb b/lib/couchrest.rb index c900d32..af40a0b 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -27,7 +27,7 @@ require 'couchrest/monkeypatches' # = CouchDB, close to the metal module CouchRest - VERSION = '0.14.2' + VERSION = '0.15' autoload :Server, 'couchrest/core/server' autoload :Database, 'couchrest/core/database' diff --git a/lib/couchrest/core/database.rb b/lib/couchrest/core/database.rb index 5500327..43dc749 100644 --- a/lib/couchrest/core/database.rb +++ b/lib/couchrest/core/database.rb @@ -279,7 +279,7 @@ module CouchRest private - def uri_for_attachment doc, name + def uri_for_attachment(doc, name) if doc.is_a?(String) puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id" docid = doc diff --git a/lib/couchrest/core/model.rb b/lib/couchrest/core/model.rb deleted file mode 100644 index 3f208cd..0000000 --- a/lib/couchrest/core/model.rb +++ /dev/null @@ -1,615 +0,0 @@ -require 'rubygems' -begin - require 'extlib' -rescue - puts "CouchRest::Model requires extlib. This is left out of the gemspec on purpose." - raise -end -require 'digest/md5' -require File.dirname(__FILE__) + '/document' -require 'mime/types' - -# = CouchRest::Model - Document modeling, the CouchDB way -module CouchRest - # = CouchRest::Model - Document modeling, the CouchDB way - # - # CouchRest::Model provides an ORM-like interface for CouchDB documents. It - # avoids all usage of method_missing, and tries to strike a balance - # between usability and magic. See CouchRest::Model#view_by for - # documentation about the view-generation system. - # - # ==== Example - # - # This is an example class using CouchRest::Model. It is taken from the - # spec/couchrest/core/model_spec.rb file, which may be even more up to date - # than this example. - # - # class Article < CouchRest::Model - # use_database CouchRest.database!('http://127.0.0.1:5984/couchrest-model-test') - # unique_id :slug - # - # view_by :date, :descending => true - # view_by :user_id, :date - # - # view_by :tags, - # :map => - # "function(doc) { - # if (doc['couchrest-type'] == 'Article' && doc.tags) { - # doc.tags.forEach(function(tag){ - # emit(tag, 1); - # }); - # } - # }", - # :reduce => - # "function(keys, values, rereduce) { - # return sum(values); - # }" - # - # key_writer :date - # key_reader :slug, :created_at, :updated_at - # key_accessor :title, :tags - # - # timestamps! - # - # before(:create, :generate_slug_from_title) - # def generate_slug_from_title - # self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') - # end - # end - # - # ==== Examples of finding articles with these views: - # - # * All the articles by Barney published in the last 24 hours. Note that we - # use {} as a special value that sorts after all strings, - # numbers, and arrays. - # - # Article.by_user_id_and_date :startkey => ["barney", Time.now - 24 * 3600], :endkey => ["barney", {}] - # - # * The most recent 20 articles. Remember that the view_by :date - # has the default option :descending => true. - # - # Article.by_date :limit => 20 - # - # * The raw CouchDB view reduce result for the custom :tags view. - # In this case we'll get a count of the number of articles tagged "ruby". - # - # Article.by_tags :key => "ruby", :reduce => true - # - class Model < Document - - # instantiates the hash by converting all the keys to strings. - def initialize keys = {} - super(keys) - apply_defaults - cast_keys - unless self['_id'] && self['_rev'] - self['couchrest-type'] = self.class.to_s - end - end - - # this is the CouchRest::Database that model classes will use unless - # they override it with use_database - cattr_accessor :default_database - - class_inheritable_accessor :casts - class_inheritable_accessor :default_obj - class_inheritable_accessor :class_database - class_inheritable_accessor :design_doc - class_inheritable_accessor :design_doc_slug_cache - class_inheritable_accessor :design_doc_fresh - - class << self - # override the CouchRest::Model-wide default_database - def use_database db - self.class_database = db - end - - # returns the CouchRest::Database instance that this class uses - def database - self.class_database || CouchRest::Model.default_database - end - - # Load a document from the database by id - def get id - doc = database.get id - new(doc) - end - - # Load all documents that have the "couchrest-type" field equal to the - # name of the current class. Take the standard set of - # CouchRest::Database#view options. - def all opts = {}, &block - self.design_doc ||= Design.new(default_design_doc) - unless design_doc_fresh - refresh_design_doc - end - view :all, opts, &block - end - - # Load the first document that have the "couchrest-type" field equal to - # the name of the current class. - # - # ==== Returns - # Object:: The first object instance available - # or - # Nil:: if no instances available - # - # ==== Parameters - # opts:: - # View options, see CouchRest::Database#view options for more info. - def first opts = {} - first_instance = self.all(opts.merge!(:limit => 1)) - first_instance.empty? ? nil : first_instance.first - end - - # Cast a field as another class. The class must be happy to have the - # field's primitive type as the argument to it's constuctur. Classes - # which inherit from CouchRest::Model are happy to act as sub-objects - # for any fields that are stored in JSON as object (and therefore are - # parsed from the JSON as Ruby Hashes). - # - # Example: - # - # class Post < CouchRest::Model - # - # key_accessor :title, :body, :author - # - # cast :author, :as => 'Author' - # - # end - # - # post.author.class #=> Author - # - # Using the same example, if a Post should have many Comments, we - # would declare it like this: - # - # class Post < CouchRest::Model - # - # key_accessor :title, :body, :author, comments - # - # cast :author, :as => 'Author' - # cast :comments, :as => ['Comment'] - # - # end - # - # post.author.class #=> Author - # post.comments.class #=> Array - # post.comments.first #=> Comment - # - def cast field, opts = {} - self.casts ||= {} - self.casts[field.to_s] = opts - 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 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 - - # 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 - self.default_obj - end - - def set_default hash - self.default_obj = hash - 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(:save) do - self['updated_at'] = Time.now - self['created_at'] = self['updated_at'] if new_document? - 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['couchrest-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['couchrest-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_spec.rb. - - def view_by *keys - self.design_doc ||= Design.new(default_design_doc) - opts = keys.pop if keys.last.is_a?(Hash) - opts ||= {} - ducktype = opts.delete(:ducktype) - unless ducktype || opts[:map] - opts[:guards] ||= [] - opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')" - end - keys.push opts - self.design_doc.view_by(*keys) - self.design_doc_fresh = false - end - - def method_missing m, *args - if has_view?(m) - query = args.shift || {} - view(m, query, *args) - else - super - end - end - - # returns stored defaults if the there is a view named this in the design doc - def has_view?(view) - view = view.to_s - design_doc && design_doc['views'] && design_doc['views'][view] - end - - # Dispatches to any named view. - def view name, query={}, &block - unless design_doc_fresh - refresh_design_doc - end - query[:raw] = true if query[:reduce] - raw = query.delete(:raw) - fetch_view_with_docs(name, query, raw, &block) - end - - def all_design_doc_versions - database.documents :startkey => "_design/#{self.to_s}-", - :endkey => "_design/#{self.to_s}-\u9999" - end - - # Deletes any non-current design docs that were created by this class. - # Running this when you're deployed version of your application is steadily - # and consistently using the latest code, is the way to clear out old design - # docs. Running it to early could mean that live code has to regenerate - # potentially large indexes. - def cleanup_design_docs! - ddocs = all_design_doc_versions - ddocs["rows"].each do |row| - if (row['id'] != design_doc_id) - database.delete_doc({ - "_id" => row['id'], - "_rev" => row['value']['rev'] - }) - end - end - end - - private - - def fetch_view_with_docs name, opts, raw=false, &block - if raw - fetch_view name, opts, &block - else - begin - view = fetch_view name, opts.merge({:include_docs => true}), &block - view['rows'].collect{|r|new(r['doc'])} if view['rows'] - rescue - # fallback for old versions of couchdb that don't - # have include_docs support - view = fetch_view name, opts, &block - view['rows'].collect{|r|new(database.get(r['id']))} if view['rows'] - end - end - end - - def fetch_view view_name, opts, &block - retryable = true - begin - design_doc.view(view_name, opts, &block) - # 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/#{design_doc_slug}" - end - - def design_doc_slug - return design_doc_slug_cache if design_doc_slug_cache && design_doc_fresh - funcs = [] - design_doc['views'].each do |name, view| - funcs << "#{name}/#{view['map']}#{view['reduce']}" - end - md5 = Digest::MD5.hexdigest(funcs.sort.join('')) - self.design_doc_slug_cache = "#{self.to_s}-#{md5}" - end - - def default_design_doc - { - "language" => "javascript", - "views" => { - 'all' => { - 'map' => "function(doc) { - if (doc['couchrest-type'] == '#{self.to_s}') { - emit(null,null); - } - }" - } - } - } - end - - def refresh_design_doc - did = design_doc_id - saved = database.get(did) rescue nil - if saved - design_doc['views'].each do |name, view| - saved['views'][name] = view - end - database.save(saved) - self.design_doc = saved - else - design_doc['_id'] = did - design_doc.delete('_rev') - design_doc.database = database - design_doc.save - end - self.design_doc_fresh = true - end - - end # class << self - - # returns the database used by this model's class - def database - self.class.database - end - - # Takes a hash as argument, and applies the values by using writer methods - # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are - # missing. In case of error, no attributes are changed. - def update_attributes_without_saving hash - hash.each do |k, v| - raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=") - end - hash.each do |k, v| - self.send("#{k}=",v) - end - end - - # Takes a hash as argument, and applies the values by using writer methods - # for each key. Raises a NoMethodError if the corresponding methods are - # missing. In case of error, no attributes are changed. - def update_attributes hash - update_attributes_without_saving hash - save - end - - # for compatibility with old-school frameworks - alias :new_record? :new_document? - - # Overridden to set the unique ID. - # Returns a boolean value - def save bulk = false - set_unique_id if new_document? && self.respond_to?(:set_unique_id) - result = database.save_doc(self, bulk) - result["ok"] == true - end - - # Saves the document to the db using create or update. Raises an exception - # if the document is not saved properly. - def save! - raise "#{self.inspect} failed to save" unless self.save - end - - # Deletes the document from the database. Runs the :destroy callbacks. - # Removes the _id and _rev fields, preparing the - # document to be saved to a new _id. - def destroy - result = database.delete_doc self - if result['ok'] - self['_rev'] = nil - self['_id'] = nil - end - result['ok'] - end - - # creates a file attachment to the current doc - def create_attachment(args={}) - raise ArgumentError unless args[:file] && args[:name] - return if has_attachment?(args[:name]) - self['_attachments'] ||= {} - set_attachment_attr(args) - rescue ArgumentError => e - raise ArgumentError, 'You must specify :file and :name' - end - - # reads the data from an attachment - def read_attachment(attachment_name) - Base64.decode64(database.fetch_attachment(self.id, attachment_name)) - end - - # modifies a file attachment on the current doc - def update_attachment(args={}) - raise ArgumentError unless args[:file] && args[:name] - return unless has_attachment?(args[:name]) - delete_attachment(args[:name]) - set_attachment_attr(args) - rescue ArgumentError => e - raise ArgumentError, 'You must specify :file and :name' - end - - # deletes a file attachment from the current doc - def delete_attachment(attachment_name) - return unless self['_attachments'] - self['_attachments'].delete attachment_name - end - - # returns true if attachment_name exists - def has_attachment?(attachment_name) - !!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?) - end - - # returns URL to fetch the attachment from - def attachment_url(attachment_name) - return unless has_attachment?(attachment_name) - "#{database.root}/#{self.id}/#{attachment_name}" - end - - private - - def apply_defaults - return unless new_document? - if self.class.default - self.class.default.each do |k,v| - unless self.key?(k.to_s) - if v.class == Proc - self[k.to_s] = v.call - else - self[k.to_s] = Marshal.load(Marshal.dump(v)) - end - end - end - end - end - - def cast_keys - return unless self.class.casts - # TODO move the argument checking to the cast method for early crashes - self.class.casts.each do |k,v| - next unless self[k] - target = v[:as] - v[:send] || 'new' - if target.is_a?(Array) - klass = ::Extlib::Inflection.constantize(target[0]) - self[k] = self[k].collect do |value| - (!v[:send] && klass == Time) ? Time.parse(value) : klass.send((v[:send] || 'new'), value) - end - else - self[k] = if (!v[:send] && target == 'Time') - Time.parse(self[k]) - else - ::Extlib::Inflection.constantize(target).send((v[:send] || 'new'), self[k]) - end - end - end - end - - def encode_attachment(data) - Base64.encode64(data).gsub(/\r|\n/,'') - end - - def get_mime_type(file) - MIME::Types.type_for(file.path).empty? ? - 'text\/plain' : MIME::Types.type_for(file.path).first.content_type.gsub(/\//,'\/') - end - - def set_attachment_attr(args) - content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file]) - self['_attachments'][args[:name]] = { - 'content-type' => content_type, - 'data' => encode_attachment(args[:file].read) - } - end - - include ::Extlib::Hook - register_instance_hooks :save, :destroy - - end # class Model -end # module CouchRest diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 386c595..95d5d19 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -31,6 +31,7 @@ module CouchRest def initialize(keys={}) apply_defaults # defined in CouchRest::Mixins::Properties + keys ||= {} super cast_keys # defined in CouchRest::Mixins::Properties unless self['_id'] && self['_rev'] @@ -100,7 +101,7 @@ module CouchRest # missing. In case of error, no attributes are changed. def update_attributes_without_saving(hash) hash.each do |k, v| - raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=") + raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=") end hash.each do |k, v| self.send("#{k}=",v) diff --git a/lib/couchrest/more/property.rb b/lib/couchrest/more/property.rb index 05cdd13..096f03e 100644 --- a/lib/couchrest/more/property.rb +++ b/lib/couchrest/more/property.rb @@ -18,6 +18,8 @@ module CouchRest def parse_type(type) if type.nil? @type = 'String' + elsif type.is_a?(Array) && type.empty? + @type = 'Array' else @type = type.is_a?(Array) ? [type.first.to_s] : type.to_s end diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index 6863e6d..4a83b57 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -704,7 +704,7 @@ describe CouchRest::Database do describe "creating a database" do before(:each) do @db = @cr.database('couchrest-test-db_to_create') - @db.delete! + @db.delete! if @cr.databases.include?('couchrest-test-db_to_create') end it "should just work fine" do diff --git a/spec/couchrest/core/model_spec.rb b/spec/couchrest/core/model_spec.rb deleted file mode 100644 index 610a862..0000000 --- a/spec/couchrest/core/model_spec.rb +++ /dev/null @@ -1,856 +0,0 @@ -require File.dirname(__FILE__) + '/../../spec_helper' - -class Basic < CouchRest::Model -end - -class BasicWithValidation < CouchRest::Model - - before :save, :validate - key_accessor :name - - def validate - throw(:halt, false) unless name - end -end - -class WithTemplateAndUniqueID < CouchRest::Model - unique_id do |model| - model['important-field'] - end - set_default({ - :preset => 'value', - 'more-template' => [1,2,3] - }) - key_accessor :preset - key_accessor :has_no_default -end - -class Question < CouchRest::Model - key_accessor :q, :a - couchrest_type = 'Question' -end - -class Person < CouchRest::Model - key_accessor :name - def last_name - name.last - end -end - -class Course < CouchRest::Model - key_accessor :title - cast :questions, :as => ['Question'] - cast :professor, :as => 'Person' - cast :final_test_at, :as => 'Time' - view_by :title - view_by :dept, :ducktype => true -end - -class Article < CouchRest::Model - use_database CouchRest.database!('http://127.0.0.1:5984/couchrest-model-test') - unique_id :slug - - view_by :date, :descending => true - view_by :user_id, :date - - view_by :tags, - :map => - "function(doc) { - if (doc['couchrest-type'] == 'Article' && doc.tags) { - doc.tags.forEach(function(tag){ - emit(tag, 1); - }); - } - }", - :reduce => - "function(keys, values, rereduce) { - return sum(values); - }" - - key_writer :date - key_reader :slug, :created_at, :updated_at - key_accessor :title, :tags - - timestamps! - - before(:save, :generate_slug_from_title) - def generate_slug_from_title - self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new_document? - end -end - -class Player < CouchRest::Model - unique_id :email - - key_accessor :email, :name, :str, :coord, :int, :con, :spirit, :level, :xp, :points, :coins, :date, :items, :loc - - view_by :name, :descending => true - view_by :loc - - timestamps! -end - -class Event < CouchRest::Model - key_accessor :subject, :occurs_at - - cast :occurs_at, :as => 'Time', :send => 'parse' -end - -describe "save bug" do - before(:each) do - CouchRest::Model.default_database = reset_test_db! - end - - it "should fix" do - @p = Player.new - @p.email = 'insane@fakestreet.com' - @p.save - end -end - - -describe CouchRest::Model do - before(:all) do - @cr = CouchRest.new(COUCHHOST) - @db = @cr.database(TESTDB) - @db.delete! rescue nil - @db = @cr.create_db(TESTDB) rescue nil - @adb = @cr.database('couchrest-model-test') - @adb.delete! rescue nil - CouchRest.database!('http://127.0.0.1:5984/couchrest-model-test') - CouchRest::Model.default_database = CouchRest.database!('http://127.0.0.1:5984/couchrest-test') - end - - it "should use the default database" do - Basic.database.info['db_name'].should == 'couchrest-test' - end - - it "should override the default db" do - Article.database.info['db_name'].should == 'couchrest-model-test' - end - - describe "a new model" do - it "should be a new_record" do - @obj = Basic.new - @obj.rev.should be_nil - @obj.should be_a_new_record - end - end - - describe "a model with key_accessors" do - it "should allow reading keys" do - @art = Article.new - @art['title'] = 'My Article Title' - @art.title.should == 'My Article Title' - end - it "should allow setting keys" do - @art = Article.new - @art.title = 'My Article Title' - @art['title'].should == 'My Article Title' - end - end - - describe "a model with key_writers" do - it "should allow setting keys" do - @art = Article.new - t = Time.now - @art.date = t - @art['date'].should == t - end - it "should not allow reading keys" do - @art = Article.new - t = Time.now - @art.date = t - lambda{@art.date}.should raise_error - end - end - - describe "a model with key_readers" do - it "should allow reading keys" do - @art = Article.new - @art['slug'] = 'my-slug' - @art.slug.should == 'my-slug' - end - it "should not allow setting keys" do - @art = Article.new - lambda{@art.slug = 'My Article Title'}.should raise_error - end - end - - describe "update attributes without saving" do - before(:each) do - a = Article.get "big-bad-danger" rescue nil - a.destroy if a - @art = Article.new(:title => "big bad danger") - @art.save - end - it "should work for attribute= methods" do - @art['title'].should == "big bad danger" - @art.update_attributes('date' => Time.now, :title => "super danger") - @art['title'].should == "super danger" - end - - it "should flip out if an attribute= method is missing" do - lambda { - @art.update_attributes('slug' => "new-slug", :title => "super danger") - }.should raise_error - end - - it "should not change other attributes if there is an error" do - lambda { - @art.update_attributes('slug' => "new-slug", :title => "super danger") - }.should raise_error - @art['title'].should == "big bad danger" - end - - end - - describe "update attributes" do - before(:each) do - a = Article.get "big-bad-danger" rescue nil - a.destroy if a - @art = Article.new(:title => "big bad danger") - @art.save - end - it "should save" do - @art['title'].should == "big bad danger" - @art.update_attributes('date' => Time.now, :title => "super danger") - loaded = Article.get @art.id - loaded['title'].should == "super danger" - end - end - - describe "a model with template values" do - before(:all) do - @tmpl = WithTemplateAndUniqueID.new - @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'important-field' => '1') - end - it "should have fields set when new" do - @tmpl.preset.should == 'value' - end - it "shouldn't override explicitly set values" do - @tmpl2.preset.should == 'not_value' - end - it "shouldn't override existing documents" do - @tmpl2.save - tmpl2_reloaded = WithTemplateAndUniqueID.get(@tmpl2.id) - @tmpl2.preset.should == 'not_value' - tmpl2_reloaded.preset.should == 'not_value' - end - it "shouldn't fill in existing documents" do - @tmpl2.save - # If user adds a new default value, shouldn't be retroactively applied to - # documents upon fetching - WithTemplateAndUniqueID.set_default({:has_no_default => 'giraffe'}) - - tmpl2_reloaded = WithTemplateAndUniqueID.get(@tmpl2.id) - @tmpl2.has_no_default.should be_nil - tmpl2_reloaded.has_no_default.should be_nil - WithTemplateAndUniqueID.new.has_no_default.should == 'giraffe' - end - end - - describe "getting a model" do - before(:all) do - @art = Article.new(:title => 'All About Getting') - @art.save - end - it "should load and instantiate it" do - foundart = Article.get @art.id - foundart.title.should == "All About Getting" - end - end - - describe "getting a model with a subobjects array" do - before(:all) do - course_doc = { - "title" => "Metaphysics 200", - "questions" => [ - { - "q" => "Carve the ___ of reality at the ___.", - "a" => ["beast","joints"] - },{ - "q" => "Who layed the smack down on Leibniz's Law?", - "a" => "Willard Van Orman Quine" - } - ] - } - r = Course.database.save_doc course_doc - @course = Course.get r['id'] - end - it "should load the course" do - @course.title.should == "Metaphysics 200" - end - it "should instantiate them as such" do - @course["questions"][0].a[0].should == "beast" - end - end - - describe "finding all instances of a model" do - before(:all) do - WithTemplateAndUniqueID.new('important-field' => '1').save - WithTemplateAndUniqueID.new('important-field' => '2').save - WithTemplateAndUniqueID.new('important-field' => '3').save - WithTemplateAndUniqueID.new('important-field' => '4').save - end - it "should make the design doc" do - WithTemplateAndUniqueID.all - d = WithTemplateAndUniqueID.design_doc - d['views']['all']['map'].should include('WithTemplateAndUniqueID') - end - it "should find all" do - rs = WithTemplateAndUniqueID.all - rs.length.should == 4 - end - end - - describe "finding the first instance of a model" do - before(:each) do - @db = reset_test_db! - WithTemplateAndUniqueID.new('important-field' => '1').save - WithTemplateAndUniqueID.new('important-field' => '2').save - WithTemplateAndUniqueID.new('important-field' => '3').save - WithTemplateAndUniqueID.new('important-field' => '4').save - end - it "should make the design doc" do - WithTemplateAndUniqueID.all - d = WithTemplateAndUniqueID.design_doc - d['views']['all']['map'].should include('WithTemplateAndUniqueID') - end - it "should find first" do - rs = WithTemplateAndUniqueID.first - rs['important-field'].should == "1" - end - it "should return nil if no instances are found" do - WithTemplateAndUniqueID.all.each {|obj| obj.destroy } - WithTemplateAndUniqueID.first.should be_nil - end - end - - describe "getting a model with a subobject field" do - before(:all) do - course_doc = { - "title" => "Metaphysics 410", - "professor" => { - "name" => ["Mark", "Hinchliff"] - }, - "final_test_at" => "2008/12/19 13:00:00 +0800" - } - r = Course.database.save_doc course_doc - @course = Course.get r['id'] - end - it "should load the course" do - @course["professor"]["name"][1].should == "Hinchliff" - end - it "should instantiate the professor as a person" do - @course['professor'].last_name.should == "Hinchliff" - end - it "should instantiate the final_test_at as a Time" do - @course['final_test_at'].should == Time.parse("2008/12/19 13:00:00 +0800") - end - end - - describe "cast keys to any type" do - before(:all) do - event_doc = { :subject => "Some event", :occurs_at => Time.now } - e = Event.database.save_doc event_doc - - @event = Event.get e['id'] - end - it "should cast created_at to Time" do - @event['occurs_at'].should be_an_instance_of(Time) - end - end - - describe "saving a model" do - before(:all) do - @obj = Basic.new - @obj.save.should == true - end - - it "should save the doc" do - doc = @obj.database.get @obj.id - doc['_id'].should == @obj.id - end - - it "should be set for resaving" do - rev = @obj.rev - @obj['another-key'] = "some value" - @obj.save - @obj.rev.should_not == rev - end - - it "should set the id" do - @obj.id.should be_an_instance_of(String) - end - - it "should set the type" do - @obj['couchrest-type'].should == 'Basic' - end - end - - describe "saving a model with validation hooks added as extlib" do - before(:all) do - @obj = BasicWithValidation.new - end - - it "save should return false is the model doesn't save as expected" do - @obj.save.should be_false - end - - it "save! should raise and exception if the model doesn't save" do - lambda{ @obj.save!}.should raise_error("#{@obj.inspect} failed to save") - end - - end - - describe "saving a model with a unique_id configured" do - before(:each) do - @art = Article.new - @old = Article.database.get('this-is-the-title') rescue nil - Article.database.delete_doc(@old) if @old - end - - it "should be a new document" do - @art.should be_a_new_document - @art.title.should be_nil - end - - it "should require the title" do - lambda{@art.save}.should raise_error - @art.title = 'This is the title' - @art.save.should == true - end - - it "should not change the slug on update" do - @art.title = 'This is the title' - @art.save.should == true - @art.title = 'new title' - @art.save.should == true - @art.slug.should == 'this-is-the-title' - end - - it "should raise an error when the slug is taken" do - @art.title = 'This is the title' - @art.save.should == true - @art2 = Article.new(:title => 'This is the title!') - lambda{@art2.save}.should raise_error - end - - it "should set the slug" do - @art.title = 'This is the title' - @art.save.should == true - @art.slug.should == 'this-is-the-title' - end - - it "should set the id" do - @art.title = 'This is the title' - @art.save.should == true - @art.id.should == 'this-is-the-title' - end - end - - describe "saving a model with a unique_id lambda" do - before(:each) do - @templated = WithTemplateAndUniqueID.new - @old = WithTemplateAndUniqueID.get('very-important') rescue nil - @old.destroy if @old - end - - it "should require the field" do - lambda{@templated.save}.should raise_error - @templated['important-field'] = 'very-important' - @templated.save.should == true - end - - it "should save with the id" do - @templated['important-field'] = 'very-important' - @templated.save.should == true - t = WithTemplateAndUniqueID.get('very-important') - t.should == @templated - end - - it "should not change the id on update" do - @templated['important-field'] = 'very-important' - @templated.save.should == true - @templated['important-field'] = 'not-important' - @templated.save.should == true - t = WithTemplateAndUniqueID.get('very-important') - t.should == @templated - end - - it "should raise an error when the id is taken" do - @templated['important-field'] = 'very-important' - @templated.save.should == true - lambda{WithTemplateAndUniqueID.new('important-field' => 'very-important').save}.should raise_error - end - - it "should set the id" do - @templated['important-field'] = 'very-important' - @templated.save.should == true - @templated.id.should == 'very-important' - end - end - - describe "a model with timestamps" do - before(:each) do - oldart = Article.get "saving-this" rescue nil - oldart.destroy if oldart - @art = Article.new(:title => "Saving this") - @art.save - end - it "should set the time on create" do - (Time.now - @art.created_at).should < 2 - foundart = Article.get @art.id - foundart.created_at.should == foundart.updated_at - end - it "should set the time on update" do - @art.save - @art.created_at.should < @art.updated_at - end - end - - describe "a model with simple views and a default param" do - before(:all) do - written_at = Time.now - 24 * 3600 * 7 - @titles = ["this and that", "also interesting", "more fun", "some junk"] - @titles.each do |title| - a = Article.new(:title => title) - a.date = written_at - a.save - written_at += 24 * 3600 - end - end - - it "should have a design doc" do - Article.design_doc["views"]["by_date"].should_not be_nil - end - - it "should save the design doc" do - Article.by_date #rescue nil - doc = Article.database.get Article.design_doc.id - doc['views']['by_date'].should_not be_nil - end - - it "should return the matching raw view result" do - view = Article.by_date :raw => true - view['rows'].length.should == 4 - end - - it "should not include non-Articles" do - Article.database.save_doc({"date" => 1}) - view = Article.by_date :raw => true - view['rows'].length.should == 4 - end - - it "should return the matching objects (with default argument :descending => true)" do - articles = Article.by_date - articles.collect{|a|a.title}.should == @titles.reverse - end - - it "should allow you to override default args" do - articles = Article.by_date :descending => false - articles.collect{|a|a.title}.should == @titles - end - end - - describe "another model with a simple view" do - before(:all) do - Course.database.delete! rescue nil - @db = @cr.create_db(TESTDB) rescue nil - %w{aaa bbb ddd eee}.each do |title| - Course.new(:title => title).save - end - end - it "should make the design doc upon first query" do - Course.by_title - doc = Course.design_doc - doc['views']['all']['map'].should include('Course') - end - it "should can query via view" do - # register methods with method-missing, for local dispatch. method - # missing lookup table, no heuristics. - view = Course.view :by_title - designed = Course.by_title - view.should == designed - end - it "should get them" do - rs = Course.by_title - rs.length.should == 4 - end - it "should yield" do - courses = [] - rs = Course.by_title # remove me - Course.view(:by_title) do |course| - courses << course - end - courses[0]["doc"]["title"].should =='aaa' - end - end - - - describe "a ducktype view" do - before(:all) do - @id = @db.save_doc({:dept => true})['id'] - end - it "should setup" do - duck = Course.get(@id) # from a different db - duck["dept"].should == true - end - it "should make the design doc" do - @as = Course.by_dept - @doc = Course.design_doc - @doc["views"]["by_dept"]["map"].should_not include("couchrest") - end - it "should not look for class" do |variable| - @as = Course.by_dept - @as[0]['_id'].should == @id - end - end - - describe "a model with a compound key view" do - before(:all) do - written_at = Time.now - 24 * 3600 * 7 - @titles = ["uniq one", "even more interesting", "less fun", "not junk"] - @user_ids = ["quentin", "aaron"] - @titles.each_with_index do |title,i| - u = i % 2 - a = Article.new(:title => title, :user_id => @user_ids[u]) - a.date = written_at - a.save - written_at += 24 * 3600 - end - end - it "should create the design doc" do - Article.by_user_id_and_date rescue nil - doc = Article.design_doc - doc['views']['by_date'].should_not be_nil - end - it "should sort correctly" do - articles = Article.by_user_id_and_date - articles.collect{|a|a['user_id']}.should == ['aaron', 'aaron', 'quentin', - 'quentin'] - articles[1].title.should == 'not junk' - end - it "should be queryable with couchrest options" do - articles = Article.by_user_id_and_date :limit => 1, :startkey => 'quentin' - articles.length.should == 1 - articles[0].title.should == "even more interesting" - end - end - - describe "with a custom view" do - before(:all) do - @titles = ["very uniq one", "even less interesting", "some fun", - "really junk", "crazy bob"] - @tags = ["cool", "lame"] - @titles.each_with_index do |title,i| - u = i % 2 - a = Article.new(:title => title, :tags => [@tags[u]]) - a.save - end - end - it "should be available raw" do - view = Article.by_tags :raw => true - view['rows'].length.should == 5 - end - - it "should be default to :reduce => false" do - ars = Article.by_tags - ars.first.tags.first.should == 'cool' - end - - it "should be raw when reduce is true" do - view = Article.by_tags :reduce => true, :group => true - view['rows'].find{|r|r['key'] == 'cool'}['value'].should == 3 - end - end - - # TODO: moved to Design, delete - describe "adding a view" do - before(:each) do - Article.by_date - @design_docs = Article.database.documents :startkey => "_design/", - :endkey => "_design/\u9999" - end - it "should not create a design doc on view definition" do - Article.view_by :created_at - newdocs = Article.database.documents :startkey => "_design/", - :endkey => "_design/\u9999" - newdocs["rows"].length.should == @design_docs["rows"].length - end - it "should create a new design document on view access" do - Article.view_by :updated_at - Article.by_updated_at - newdocs = Article.database.documents :startkey => "_design/", - :endkey => "_design/\u9999" - # puts @design_docs.inspect - # puts newdocs.inspect - newdocs["rows"].length.should == @design_docs["rows"].length + 1 - end - end - - describe "with a lot of designs left around" do - before(:each) do - Article.by_date - Article.view_by :field - Article.by_field - end - it "should clean them up" do - Article.view_by :stream - Article.by_stream - ddocs = Article.all_design_doc_versions - ddocs["rows"].length.should > 1 - Article.cleanup_design_docs! - ddocs = Article.all_design_doc_versions - ddocs["rows"].length.should == 1 - end - end - - describe "destroying an instance" do - before(:each) do - @obj = Basic.new - @obj.save.should == true - end - it "should return true" do - result = @obj.destroy - result.should == true - end - it "should be resavable" do - @obj.destroy - @obj.rev.should be_nil - @obj.id.should be_nil - @obj.save.should == true - end - it "should make it go away" do - @obj.destroy - lambda{Basic.get(@obj.id)}.should raise_error - end - end - - describe "#has_attachment?" do - before(:each) do - @obj = Basic.new - @obj.save.should == true - @file = File.open(FIXTURE_PATH + '/attachments/test.html') - @attachment_name = 'my_attachment' - @obj.create_attachment(:file => @file, :name => @attachment_name) - end - - it 'should return false if there is no attachment' do - @obj.has_attachment?('bogus').should be_false - end - - it 'should return true if there is an attachment' do - @obj.has_attachment?(@attachment_name).should be_true - end - - it 'should return true if an object with an attachment is reloaded' do - @obj.save.should be_true - reloaded_obj = Basic.get(@obj.id) - reloaded_obj.has_attachment?(@attachment_name).should be_true - end - - it 'should return false if an attachment has been removed' do - @obj.delete_attachment(@attachment_name) - @obj.has_attachment?(@attachment_name).should be_false - end - end - - describe "creating an attachment" do - before(:each) do - @obj = Basic.new - @obj.save.should == true - @file_ext = File.open(FIXTURE_PATH + '/attachments/test.html') - @file_no_ext = File.open(FIXTURE_PATH + '/attachments/README') - @attachment_name = 'my_attachment' - @content_type = 'media/mp3' - end - - it "should create an attachment from file with an extension" do - @obj.create_attachment(:file => @file_ext, :name => @attachment_name) - @obj.save.should == true - reloaded_obj = Basic.get(@obj.id) - reloaded_obj['_attachments'][@attachment_name].should_not be_nil - end - - it "should create an attachment from file without an extension" do - @obj.create_attachment(:file => @file_no_ext, :name => @attachment_name) - @obj.save.should == true - reloaded_obj = Basic.get(@obj.id) - reloaded_obj['_attachments'][@attachment_name].should_not be_nil - end - - it 'should raise ArgumentError if :file is missing' do - lambda{ @obj.create_attachment(:name => @attachment_name) }.should raise_error - end - - it 'should raise ArgumentError if :name is missing' do - lambda{ @obj.create_attachment(:file => @file_ext) }.should raise_error - end - - it 'should set the content-type if passed' do - @obj.create_attachment(:file => @file_ext, :name => @attachment_name, :content_type => @content_type) - @obj['_attachments'][@attachment_name]['content-type'].should == @content_type - end - end - - describe 'reading, updating, and deleting an attachment' do - before(:each) do - @obj = Basic.new - @file = File.open(FIXTURE_PATH + '/attachments/test.html') - @attachment_name = 'my_attachment' - @obj.create_attachment(:file => @file, :name => @attachment_name) - @obj.save.should == true - @file.rewind - @content_type = 'media/mp3' - end - - it 'should read an attachment that exists' do - @obj.read_attachment(@attachment_name).should == @file.read - end - - it 'should update an attachment that exists' do - file = File.open(FIXTURE_PATH + '/attachments/README') - @file.should_not == file - @obj.update_attachment(:file => file, :name => @attachment_name) - @obj.save - reloaded_obj = Basic.get(@obj.id) - file.rewind - reloaded_obj.read_attachment(@attachment_name).should_not == @file.read - reloaded_obj.read_attachment(@attachment_name).should == file.read - end - - it 'should se the content-type if passed' do - file = File.open(FIXTURE_PATH + '/attachments/README') - @file.should_not == file - @obj.update_attachment(:file => file, :name => @attachment_name, :content_type => @content_type) - @obj['_attachments'][@attachment_name]['content-type'].should == @content_type - end - - it 'should delete an attachment that exists' do - @obj.delete_attachment(@attachment_name) - @obj.save - lambda{Basic.get(@obj.id).read_attachment(@attachment_name)}.should raise_error - end - end - - describe "#attachment_url" do - before(:each) do - @obj = Basic.new - @file = File.open(FIXTURE_PATH + '/attachments/test.html') - @attachment_name = 'my_attachment' - @obj.create_attachment(:file => @file, :name => @attachment_name) - @obj.save.should == true - end - - it 'should return nil if attachment does not exist' do - @obj.attachment_url('bogus').should be_nil - end - - it 'should return the attachment URL as specified by CouchDB HttpDocumentApi' do - @obj.attachment_url(@attachment_name).should == "#{Basic.database}/#{@obj.id}/#{@attachment_name}" - end - end - -end \ No newline at end of file diff --git a/spec/couchrest/more/extended_doc_attachment_spec.rb b/spec/couchrest/more/extended_doc_attachment_spec.rb new file mode 100644 index 0000000..c75fc0c --- /dev/null +++ b/spec/couchrest/more/extended_doc_attachment_spec.rb @@ -0,0 +1,129 @@ +require File.dirname(__FILE__) + '/../../spec_helper' + +describe "ExtendedDocument attachments" do + + describe "#has_attachment?" do + before(:each) do + @obj = Basic.new + @obj.save.should == true + @file = File.open(FIXTURE_PATH + '/attachments/test.html') + @attachment_name = 'my_attachment' + @obj.create_attachment(:file => @file, :name => @attachment_name) + end + + it 'should return false if there is no attachment' do + @obj.has_attachment?('bogus').should be_false + end + + it 'should return true if there is an attachment' do + @obj.has_attachment?(@attachment_name).should be_true + end + + it 'should return true if an object with an attachment is reloaded' do + @obj.save.should be_true + reloaded_obj = Basic.get(@obj.id) + reloaded_obj.has_attachment?(@attachment_name).should be_true + end + + it 'should return false if an attachment has been removed' do + @obj.delete_attachment(@attachment_name) + @obj.has_attachment?(@attachment_name).should be_false + end + end + + describe "creating an attachment" do + before(:each) do + @obj = Basic.new + @obj.save.should == true + @file_ext = File.open(FIXTURE_PATH + '/attachments/test.html') + @file_no_ext = File.open(FIXTURE_PATH + '/attachments/README') + @attachment_name = 'my_attachment' + @content_type = 'media/mp3' + end + + it "should create an attachment from file with an extension" do + @obj.create_attachment(:file => @file_ext, :name => @attachment_name) + @obj.save.should == true + reloaded_obj = Basic.get(@obj.id) + reloaded_obj['_attachments'][@attachment_name].should_not be_nil + end + + it "should create an attachment from file without an extension" do + @obj.create_attachment(:file => @file_no_ext, :name => @attachment_name) + @obj.save.should == true + reloaded_obj = Basic.get(@obj.id) + reloaded_obj['_attachments'][@attachment_name].should_not be_nil + end + + it 'should raise ArgumentError if :file is missing' do + lambda{ @obj.create_attachment(:name => @attachment_name) }.should raise_error + end + + it 'should raise ArgumentError if :name is missing' do + lambda{ @obj.create_attachment(:file => @file_ext) }.should raise_error + end + + it 'should set the content-type if passed' do + @obj.create_attachment(:file => @file_ext, :name => @attachment_name, :content_type => @content_type) + @obj['_attachments'][@attachment_name]['content-type'].should == @content_type + end + end + + describe 'reading, updating, and deleting an attachment' do + before(:each) do + @obj = Basic.new + @file = File.open(FIXTURE_PATH + '/attachments/test.html') + @attachment_name = 'my_attachment' + @obj.create_attachment(:file => @file, :name => @attachment_name) + @obj.save.should == true + @file.rewind + @content_type = 'media/mp3' + end + + it 'should read an attachment that exists' do + @obj.read_attachment(@attachment_name).should == @file.read + end + + it 'should update an attachment that exists' do + file = File.open(FIXTURE_PATH + '/attachments/README') + @file.should_not == file + @obj.update_attachment(:file => file, :name => @attachment_name) + @obj.save + reloaded_obj = Basic.get(@obj.id) + file.rewind + reloaded_obj.read_attachment(@attachment_name).should_not == @file.read + reloaded_obj.read_attachment(@attachment_name).should == file.read + end + + it 'should se the content-type if passed' do + file = File.open(FIXTURE_PATH + '/attachments/README') + @file.should_not == file + @obj.update_attachment(:file => file, :name => @attachment_name, :content_type => @content_type) + @obj['_attachments'][@attachment_name]['content-type'].should == @content_type + end + + it 'should delete an attachment that exists' do + @obj.delete_attachment(@attachment_name) + @obj.save + lambda{Basic.get(@obj.id).read_attachment(@attachment_name)}.should raise_error + end + end + + describe "#attachment_url" do + before(:each) do + @obj = Basic.new + @file = File.open(FIXTURE_PATH + '/attachments/test.html') + @attachment_name = 'my_attachment' + @obj.create_attachment(:file => @file, :name => @attachment_name) + @obj.save.should == true + end + + it 'should return nil if attachment does not exist' do + @obj.attachment_url('bogus').should be_nil + end + + it 'should return the attachment URL as specified by CouchDB HttpDocumentApi' do + @obj.attachment_url(@attachment_name).should == "#{Basic.database}/#{@obj.id}/#{@attachment_name}" + end + end +end \ No newline at end of file diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index 95a4ce6..fadb676 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -1,4 +1,7 @@ require File.dirname(__FILE__) + '/../../spec_helper' +require File.join(FIXTURE_PATH, 'more', 'article') +require File.join(FIXTURE_PATH, 'more', 'course') + describe "ExtendedDocument" do @@ -41,10 +44,86 @@ describe "ExtendedDocument" do end end + class WithTemplateAndUniqueID < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database + unique_id do |model| + model['important-field'] + end + property :preset, :default => 'value' + property :has_no_default + end + before(:each) do @obj = WithDefaultValues.new end + describe "instance database connection" do + it "should use the default database" do + @obj.database.name.should == 'couchrest-test' + end + + it "should override the default db" do + @obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test') + @obj.database.name.should == 'couchrest-extendedmodel-test' + @obj.database.delete! + end + end + + describe "a new model" do + it "should be a new_record" do + @obj = Basic.new + @obj.rev.should be_nil + @obj.should be_a_new_record + end + it "should be a new_document" do + @obj = Basic.new + @obj.rev.should be_nil + @obj.should be_a_new_document + end + end + + describe "update attributes without saving" do + before(:each) do + a = Article.get "big-bad-danger" rescue nil + a.destroy if a + @art = Article.new(:title => "big bad danger") + @art.save + end + it "should work for attribute= methods" do + @art['title'].should == "big bad danger" + @art.update_attributes_without_saving('date' => Time.now, :title => "super danger") + @art['title'].should == "super danger" + end + + it "should flip out if an attribute= method is missing" do + lambda { + @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") + }.should raise_error + end + + it "should not change other attributes if there is an error" do + lambda { + @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") + }.should raise_error + @art['title'].should == "big bad danger" + end + end + + describe "update attributes" do + before(:each) do + a = Article.get "big-bad-danger" rescue nil + a.destroy if a + @art = Article.new(:title => "big bad danger") + @art.save + end + it "should save" do + @art['title'].should == "big bad danger" + @art.update_attributes('date' => Time.now, :title => "super danger") + loaded = Article.get(@art.id) + loaded['title'].should == "super danger" + end + end + describe "with default" do it "should have the default value set at initalization" do @obj.preset.should == {:right => 10, :top_align => false} @@ -67,7 +146,137 @@ describe "ExtendedDocument" do end end + describe "a doc with template values (CR::Model spec)" do + before(:all) do + WithTemplateAndUniqueID.all.map{|o| o.destroy(true)} + WithTemplateAndUniqueID.database.bulk_delete + @tmpl = WithTemplateAndUniqueID.new + @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'important-field' => '1') + end + it "should have fields set when new" do + @tmpl.preset.should == 'value' + end + it "shouldn't override explicitly set values" do + @tmpl2.preset.should == 'not_value' + end + it "shouldn't override existing documents" do + @tmpl2.save + tmpl2_reloaded = WithTemplateAndUniqueID.get(@tmpl2.id) + @tmpl2.preset.should == 'not_value' + tmpl2_reloaded.preset.should == 'not_value' + end + end + + describe "getting a model" do + before(:all) do + @art = Article.new(:title => 'All About Getting') + @art.save + end + it "should load and instantiate it" do + foundart = Article.get @art.id + foundart.title.should == "All About Getting" + end + end + + describe "getting a model with a subobjects array" do + before(:all) do + course_doc = { + "title" => "Metaphysics 200", + "questions" => [ + { + "q" => "Carve the ___ of reality at the ___.", + "a" => ["beast","joints"] + },{ + "q" => "Who layed the smack down on Leibniz's Law?", + "a" => "Willard Van Orman Quine" + } + ] + } + r = Course.database.save_doc course_doc + @course = Course.get r['id'] + end + it "should load the course" do + @course.title.should == "Metaphysics 200" + end + it "should instantiate them as such" do + @course["questions"][0].a[0].should == "beast" + end + end + + describe "finding all instances of a model" do + before(:all) do + WithTemplateAndUniqueID.all.map{|o| o.destroy(true)} + WithTemplateAndUniqueID.database.bulk_delete + WithTemplateAndUniqueID.new('important-field' => '1').save + WithTemplateAndUniqueID.new('important-field' => '2').save + WithTemplateAndUniqueID.new('important-field' => '3').save + WithTemplateAndUniqueID.new('important-field' => '4').save + end + it "should make the design doc" do + WithTemplateAndUniqueID.all + d = WithTemplateAndUniqueID.design_doc + d['views']['all']['map'].should include('WithTemplateAndUniqueID') + end + it "should find all" do + rs = WithTemplateAndUniqueID.all + rs.length.should == 4 + end + end + + describe "finding the first instance of a model" do + before(:each) do + @db = reset_test_db! + WithTemplateAndUniqueID.new('important-field' => '1').save + WithTemplateAndUniqueID.new('important-field' => '2').save + WithTemplateAndUniqueID.new('important-field' => '3').save + WithTemplateAndUniqueID.new('important-field' => '4').save + end + it "should make the design doc" do + WithTemplateAndUniqueID.all + d = WithTemplateAndUniqueID.design_doc + d['views']['all']['map'].should include('WithTemplateAndUniqueID') + end + it "should find first" do + rs = WithTemplateAndUniqueID.first + rs['important-field'].should == "1" + end + it "should return nil if no instances are found" do + WithTemplateAndUniqueID.all.each {|obj| obj.destroy } + WithTemplateAndUniqueID.first.should be_nil + end + end + + describe "getting a model with a subobject field" do + before(:all) do + course_doc = { + "title" => "Metaphysics 410", + "professor" => { + "name" => ["Mark", "Hinchliff"] + }, + "final_test_at" => "2008/12/19 13:00:00 +0800" + } + r = Course.database.save_doc course_doc + @course = Course.get r['id'] + end + it "should load the course" do + @course["professor"]["name"][1].should == "Hinchliff" + end + it "should instantiate the professor as a person" do + @course['professor'].last_name.should == "Hinchliff" + end + it "should instantiate the final_test_at as a Time" do + @course['final_test_at'].should == Time.parse("2008/12/19 13:00:00 +0800") + end + end + describe "timestamping" do + before(:each) do + oldart = Article.get "saving-this" rescue nil + oldart.destroy if oldart + @art = Article.new(:title => "Saving this") + @art.save + end + it "should define the updated_at and created_at getters and set the values" do @obj.save obj = WithDefaultValues.get(@obj.id) @@ -76,9 +285,18 @@ describe "ExtendedDocument" do obj.updated_at.should be_an_instance_of(Time) obj.created_at.to_s.should == @obj.updated_at.to_s end + it "should set the time on create" do + (Time.now - @art.created_at).should < 2 + foundart = Article.get @art.id + foundart.created_at.should == foundart.updated_at + end + it "should set the time on update" do + @art.save + @art.created_at.should < @art.updated_at + end end - describe "saving and retrieving" do + describe "basic saving and retrieving" do it "should work fine" do @obj.name = "should be easily saved and retrieved" @obj.save @@ -96,6 +314,145 @@ describe "ExtendedDocument" do end end + describe "saving a model" do + before(:all) do + @sobj = Basic.new + @sobj.save.should == true + end + + it "should save the doc" do + doc = Basic.get(@sobj.id) + doc['_id'].should == @sobj.id + end + + it "should be set for resaving" do + rev = @obj.rev + @sobj['another-key'] = "some value" + @sobj.save + @sobj.rev.should_not == rev + end + + it "should set the id" do + @sobj.id.should be_an_instance_of(String) + end + + it "should set the type" do + @sobj['couchrest-type'].should == 'Basic' + end + end + + describe "saving a model with a unique_id configured" do + before(:each) do + @art = Article.new + @old = Article.database.get('this-is-the-title') rescue nil + Article.database.delete_doc(@old) if @old + end + + it "should be a new document" do + @art.should be_a_new_document + @art.title.should be_nil + end + + it "should require the title" do + lambda{@art.save}.should raise_error + @art.title = 'This is the title' + @art.save.should == true + end + + it "should not change the slug on update" do + @art.title = 'This is the title' + @art.save.should == true + @art.title = 'new title' + @art.save.should == true + @art.slug.should == 'this-is-the-title' + end + + it "should raise an error when the slug is taken" do + @art.title = 'This is the title' + @art.save.should == true + @art2 = Article.new(:title => 'This is the title!') + lambda{@art2.save}.should raise_error + end + + it "should set the slug" do + @art.title = 'This is the title' + @art.save.should == true + @art.slug.should == 'this-is-the-title' + end + + it "should set the id" do + @art.title = 'This is the title' + @art.save.should == true + @art.id.should == 'this-is-the-title' + end + end + + describe "saving a model with a unique_id lambda" do + before(:each) do + @templated = WithTemplateAndUniqueID.new + @old = WithTemplateAndUniqueID.get('very-important') rescue nil + @old.destroy if @old + end + + it "should require the field" do + lambda{@templated.save}.should raise_error + @templated['important-field'] = 'very-important' + @templated.save.should == true + end + + it "should save with the id" do + @templated['important-field'] = 'very-important' + @templated.save.should == true + t = WithTemplateAndUniqueID.get('very-important') + t.should == @templated + end + + it "should not change the id on update" do + @templated['important-field'] = 'very-important' + @templated.save.should == true + @templated['important-field'] = 'not-important' + @templated.save.should == true + t = WithTemplateAndUniqueID.get('very-important') + t.should == @templated + end + + it "should raise an error when the id is taken" do + @templated['important-field'] = 'very-important' + @templated.save.should == true + lambda{WithTemplateAndUniqueID.new('important-field' => 'very-important').save}.should raise_error + end + + it "should set the id" do + @templated['important-field'] = 'very-important' + @templated.save.should == true + @templated.id.should == 'very-important' + end + end + + describe "destroying an instance" do + before(:each) do + @dobj = Basic.new + @dobj.save.should == true + end + it "should return true" do + result = @dobj.destroy + result.should == true + end + it "should be resavable" do + pending "TO FIX" do + @dobj.destroy + @dobj.rev.should be_nil + @dobj.id.should be_nil + @dobj.save.should == true + end + end + it "should make it go away" do + @dobj.destroy + lambda{Basic.get(@dobj.id)}.should raise_error + end + end + + describe "callbacks" do before(:each) do @@ -103,28 +460,11 @@ describe "ExtendedDocument" do end describe "save" do - it "should not run the before filter before saving if the save failed" do - @doc.run_before_save.should be_nil - @doc.save.should be_true - @doc.run_before_save.should be_true - end - it "should not run the before filter before saving if the save failed" do - @doc.should_receive(:save).and_return(false) - @doc.run_before_save.should be_nil - @doc.save.should be_false - @doc.run_before_save.should be_nil - end it "should run the after filter after saving" do @doc.run_after_save.should be_nil @doc.save.should be_true @doc.run_after_save.should be_true end - it "should not run the after filter before saving if the save failed" do - @doc.should_receive(:save).and_return(false) - @doc.run_after_save.should be_nil - @doc.save.should be_false - @doc.run_after_save.should be_nil - end end describe "create" do it "should run the before save filter when creating" do @@ -132,14 +472,6 @@ describe "ExtendedDocument" do @doc.create.should_not be_nil @doc.run_before_save.should be_true end - it "should not run the before save filter when the object creation fails" do - pending "need to ask wycats about chainable callbacks" do - @doc.should_receive(:create_without_callbacks).and_return(false) - @doc.run_before_save.should be_nil - @doc.save - @doc.run_before_save.should be_nil - end - end it "should run the before create filter" do @doc.run_before_create.should be_nil @doc.create.should_not be_nil diff --git a/spec/couchrest/more/extended_doc_view_spec.rb b/spec/couchrest/more/extended_doc_view_spec.rb new file mode 100644 index 0000000..2e47442 --- /dev/null +++ b/spec/couchrest/more/extended_doc_view_spec.rb @@ -0,0 +1,204 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require File.join(FIXTURE_PATH, 'more', 'article') +require File.join(FIXTURE_PATH, 'more', 'course') + +describe "ExtendedDocument views" do + + describe "a model with simple views and a default param" do + before(:all) do + Article.all.map{|a| a.destroy(true)} + Article.database.bulk_delete + written_at = Time.now - 24 * 3600 * 7 + @titles = ["this and that", "also interesting", "more fun", "some junk"] + @titles.each do |title| + a = Article.new(:title => title) + a.date = written_at + a.save + written_at += 24 * 3600 + end + end + + it "should have a design doc" do + Article.design_doc["views"]["by_date"].should_not be_nil + end + + it "should save the design doc" do + Article.by_date #rescue nil + doc = Article.database.get Article.design_doc.id + doc['views']['by_date'].should_not be_nil + end + + it "should return the matching raw view result" do + view = Article.by_date :raw => true + view['rows'].length.should == 4 + end + + it "should not include non-Articles" do + Article.database.save_doc({"date" => 1}) + view = Article.by_date :raw => true + view['rows'].length.should == 4 + end + + it "should return the matching objects (with default argument :descending => true)" do + articles = Article.by_date + articles.collect{|a|a.title}.should == @titles.reverse + end + + it "should allow you to override default args" do + articles = Article.by_date :descending => false + articles.collect{|a|a.title}.should == @titles + end + end + + describe "another model with a simple view" do + before(:all) do + reset_test_db! + %w{aaa bbb ddd eee}.each do |title| + Course.new(:title => title).save + end + end + it "should make the design doc upon first query" do + Course.by_title + doc = Course.design_doc + doc['views']['all']['map'].should include('Course') + end + it "should can query via view" do + # register methods with method-missing, for local dispatch. method + # missing lookup table, no heuristics. + view = Course.view :by_title + designed = Course.by_title + view.should == designed + end + it "should get them" do + rs = Course.by_title + rs.length.should == 4 + end + it "should yield" do + courses = [] + rs = Course.by_title # remove me + Course.view(:by_title) do |course| + courses << course + end + courses[0]["doc"]["title"].should =='aaa' + end + end + + + describe "a ducktype view" do + before(:all) do + @id = TEST_SERVER.default_database.save_doc({:dept => true})['id'] + end + it "should setup" do + duck = Course.get(@id) # from a different db + duck["dept"].should == true + end + it "should make the design doc" do + @as = Course.by_dept + @doc = Course.design_doc + @doc["views"]["by_dept"]["map"].should_not include("couchrest") + end + it "should not look for class" do |variable| + @as = Course.by_dept + @as[0]['_id'].should == @id + end + end + + describe "a model with a compound key view" do + before(:all) do + written_at = Time.now - 24 * 3600 * 7 + @titles = ["uniq one", "even more interesting", "less fun", "not junk"] + @user_ids = ["quentin", "aaron"] + @titles.each_with_index do |title,i| + u = i % 2 + a = Article.new(:title => title, :user_id => @user_ids[u]) + a.date = written_at + a.save + written_at += 24 * 3600 + end + end + it "should create the design doc" do + Article.by_user_id_and_date rescue nil + doc = Article.design_doc + doc['views']['by_date'].should_not be_nil + end + it "should sort correctly" do + articles = Article.by_user_id_and_date + articles.collect{|a|a['user_id']}.should == ['aaron', 'aaron', 'quentin', + 'quentin'] + articles[1].title.should == 'not junk' + end + it "should be queryable with couchrest options" do + articles = Article.by_user_id_and_date :limit => 1, :startkey => 'quentin' + articles.length.should == 1 + articles[0].title.should == "even more interesting" + end + end + + describe "with a custom view" do + before(:all) do + @titles = ["very uniq one", "even less interesting", "some fun", + "really junk", "crazy bob"] + @tags = ["cool", "lame"] + @titles.each_with_index do |title,i| + u = i % 2 + a = Article.new(:title => title, :tags => [@tags[u]]) + a.save + end + end + it "should be available raw" do + view = Article.by_tags :raw => true + view['rows'].length.should == 5 + end + + it "should be default to :reduce => false" do + ars = Article.by_tags + ars.first.tags.first.should == 'cool' + end + + it "should be raw when reduce is true" do + view = Article.by_tags :reduce => true, :group => true + view['rows'].find{|r|r['key'] == 'cool'}['value'].should == 3 + end + end + + # TODO: moved to Design, delete + describe "adding a view" do + before(:each) do + Article.by_date + @design_docs = Article.database.documents :startkey => "_design/", + :endkey => "_design/\u9999" + end + it "should not create a design doc on view definition" do + Article.view_by :created_at + newdocs = Article.database.documents :startkey => "_design/", + :endkey => "_design/\u9999" + newdocs["rows"].length.should == @design_docs["rows"].length + end + it "should create a new design document on view access" do + Article.view_by :updated_at + Article.by_updated_at + newdocs = Article.database.documents :startkey => "_design/", + :endkey => "_design/\u9999" + # puts @design_docs.inspect + # puts newdocs.inspect + newdocs["rows"].length.should == @design_docs["rows"].length + 1 + end + end + + describe "with a lot of designs left around" do + before(:each) do + Article.by_date + Article.view_by :field + Article.by_field + end + it "should clean them up" do + Article.view_by :stream + Article.by_stream + ddocs = Article.all_design_doc_versions + ddocs["rows"].length.should > 1 + Article.cleanup_design_docs! + ddocs = Article.all_design_doc_versions + ddocs["rows"].length.should == 1 + end + end +end \ No newline at end of file diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 3da2ada..ec81d50 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -1,7 +1,8 @@ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'invoice') -require File.join(FIXTURE_PATH, 'more', 'service.rb') +require File.join(FIXTURE_PATH, 'more', 'service') +require File.join(FIXTURE_PATH, 'more', 'event') describe "ExtendedDocument properties" do @@ -43,7 +44,6 @@ describe "ExtendedDocument properties" do end describe "validation" do - before(:each) do @invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA") end @@ -79,11 +79,9 @@ describe "ExtendedDocument properties" do @invoice.save.should be_false @invoice.should be_new_document end - end describe "autovalidation" do - before(:each) do @service = Service.new(:name => "Coumpound analysis", :price => 3_000) end @@ -112,7 +110,20 @@ describe "ExtendedDocument properties" do @service.errors.on(:name).first.should == "Name must be between 4 and 19 characters long" end end - + end + + describe "casting" do + describe "cast keys to any type" do + before(:all) do + event_doc = { :subject => "Some event", :occurs_at => Time.now } + e = Event.database.save_doc event_doc + + @event = Event.get e['id'] + end + it "should cast created_at to Time" do + @event['occurs_at'].should be_an_instance_of(Time) + end + end end end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8aea0d3..4e4b507 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,10 @@ unless defined?(FIXTURE_PATH) TEST_SERVER.default_database = TESTDB end +class Basic < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database +end + def reset_test_db! cr = TEST_SERVER db = cr.database(TESTDB)