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)