removed CouchRest::Model, added more specs and fixed a bug with casted CR::ExtendedDocument

This commit is contained in:
Matt Aimonetti 2009-02-24 22:51:13 -08:00
parent 72542dc876
commit fe489f2d38
15 changed files with 740 additions and 1528 deletions

View file

@ -59,13 +59,7 @@ Creating and Querying Views:
## CouchRest::Model ## CouchRest::Model
CouchRest::Model is a module designed along the lines of DataMapper::Resource. CouchRest::Model has been deprecated and replaced by CouchRest::ExtendedDocument
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::ExtendedDocument ## CouchRest::ExtendedDocument

View file

@ -2,7 +2,7 @@
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = %q{couchrest} 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.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["J. Chris Anderson", "Matt Aimonetti"] 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.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.email = %q{jchris@apache.org}
s.extra_rdoc_files = ["README.md", "LICENSE", "THANKS.md"] 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.has_rdoc = true
s.homepage = %q{http://github.com/jchris/couchrest} s.homepage = %q{http://github.com/jchris/couchrest}
s.require_paths = ["lib"] s.require_paths = ["lib"]

View file

@ -1,31 +1,38 @@
require 'rubygems' require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'couchrest')
require 'couchrest'
def show obj def show obj
puts obj.inspect puts obj.inspect
puts puts
end end
CouchRest::Model.default_database = CouchRest.database!('couchrest-model-example') SERVER = CouchRest.new
SERVER.default_database = 'couchrest-extendeddoc-example'
class Author < CouchRest::Model class Author < CouchRest::ExtendedDocument
key_accessor :name use_database SERVER.default_database
property :name
def drink_scotch def drink_scotch
puts "... glug type glug ... I'm #{name} ... type glug glug ..." puts "... glug type glug ... I'm #{name} ... type glug glug ..."
end end
end end
class Post < CouchRest::Model class Post < CouchRest::ExtendedDocument
key_accessor :title, :body, :author use_database SERVER.default_database
cast :author, :as => 'Author' property :title
property :body
property :author, :cast_as => 'Author'
timestamps! timestamps!
end end
class Comment < CouchRest::Model class Comment < CouchRest::ExtendedDocument
cast :commenter, :as => 'Author' use_database SERVER.default_database
property :commenter, :cast_as => 'Author'
timestamps!
def post= post def post= post
self["post_id"] = post.id self["post_id"] = post.id
end end
@ -33,7 +40,6 @@ class Comment < CouchRest::Model
Post.get(self['post_id']) if self['post_id'] Post.get(self['post_id']) if self['post_id']
end end
timestamps!
end end
puts "Act I: CRUD" puts "Act I: CRUD"

View file

@ -27,7 +27,7 @@ require 'couchrest/monkeypatches'
# = CouchDB, close to the metal # = CouchDB, close to the metal
module CouchRest module CouchRest
VERSION = '0.14.2' VERSION = '0.15'
autoload :Server, 'couchrest/core/server' autoload :Server, 'couchrest/core/server'
autoload :Database, 'couchrest/core/database' autoload :Database, 'couchrest/core/database'

View file

@ -279,7 +279,7 @@ module CouchRest
private private
def uri_for_attachment doc, name def uri_for_attachment(doc, name)
if doc.is_a?(String) if doc.is_a?(String)
puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id" puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id"
docid = doc docid = doc

View file

@ -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 <tt>method_missing</tt>, 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 <tt>{}</tt> 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 <tt>view_by :date</tt>
# has the default option <tt>:descending => true</tt>.
#
# Article.by_date :limit => 20
#
# * The raw CouchDB view reduce result for the custom <tt>:tags</tt> 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 <tt>use_database</tt>
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<Hash>::
# View options, see <tt>CouchRest::Database#view</tt> 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 <tt>key=</tt> 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 <tt>key</tt> 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 <tt>updated_at</tt> and <tt>created_at</tt> 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 <tt>_id</tt>.
# 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 <tt>by</tt> and the keys joined by <tt>_and_</tt>
#
# ==== 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
#
# <tt>view_by :date</tt> 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 <tt>Post.by_date</tt> which accepts all
# valid options for CouchRest::Database#view. In addition, calling with
# the <tt>:raw => true</tt> option will return the view rows
# themselves. By default <tt>Post.by_date</tt> 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 <tt>:reduce => true</tt> to return
# reduce results. The default for custom views is to query with
# <tt>:reduce => false</tt>.
#
# 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
# <tt>spec/core/model_spec.rb</tt>.
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 <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
# document to be saved to a new <tt>_id</tt>.
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

View file

@ -31,6 +31,7 @@ module CouchRest
def initialize(keys={}) def initialize(keys={})
apply_defaults # defined in CouchRest::Mixins::Properties apply_defaults # defined in CouchRest::Mixins::Properties
keys ||= {}
super super
cast_keys # defined in CouchRest::Mixins::Properties cast_keys # defined in CouchRest::Mixins::Properties
unless self['_id'] && self['_rev'] unless self['_id'] && self['_rev']
@ -100,7 +101,7 @@ module CouchRest
# missing. In case of error, no attributes are changed. # missing. In case of error, no attributes are changed.
def update_attributes_without_saving(hash) def update_attributes_without_saving(hash)
hash.each do |k, v| 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 end
hash.each do |k, v| hash.each do |k, v|
self.send("#{k}=",v) self.send("#{k}=",v)

View file

@ -18,6 +18,8 @@ module CouchRest
def parse_type(type) def parse_type(type)
if type.nil? if type.nil?
@type = 'String' @type = 'String'
elsif type.is_a?(Array) && type.empty?
@type = 'Array'
else else
@type = type.is_a?(Array) ? [type.first.to_s] : type.to_s @type = type.is_a?(Array) ? [type.first.to_s] : type.to_s
end end

View file

@ -704,7 +704,7 @@ describe CouchRest::Database do
describe "creating a database" do describe "creating a database" do
before(:each) do before(:each) do
@db = @cr.database('couchrest-test-db_to_create') @db = @cr.database('couchrest-test-db_to_create')
@db.delete! @db.delete! if @cr.databases.include?('couchrest-test-db_to_create')
end end
it "should just work fine" do it "should just work fine" do

View file

@ -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

View file

@ -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

View file

@ -1,4 +1,7 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.dirname(__FILE__) + '/../../spec_helper'
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
describe "ExtendedDocument" do describe "ExtendedDocument" do
@ -41,10 +44,86 @@ describe "ExtendedDocument" do
end end
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 before(:each) do
@obj = WithDefaultValues.new @obj = WithDefaultValues.new
end 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 describe "with default" do
it "should have the default value set at initalization" do it "should have the default value set at initalization" do
@obj.preset.should == {:right => 10, :top_align => false} @obj.preset.should == {:right => 10, :top_align => false}
@ -67,7 +146,137 @@ describe "ExtendedDocument" do
end end
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 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 it "should define the updated_at and created_at getters and set the values" do
@obj.save @obj.save
obj = WithDefaultValues.get(@obj.id) obj = WithDefaultValues.get(@obj.id)
@ -76,9 +285,18 @@ describe "ExtendedDocument" do
obj.updated_at.should be_an_instance_of(Time) obj.updated_at.should be_an_instance_of(Time)
obj.created_at.to_s.should == @obj.updated_at.to_s obj.created_at.to_s.should == @obj.updated_at.to_s
end 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 end
describe "saving and retrieving" do describe "basic saving and retrieving" do
it "should work fine" do it "should work fine" do
@obj.name = "should be easily saved and retrieved" @obj.name = "should be easily saved and retrieved"
@obj.save @obj.save
@ -96,6 +314,145 @@ describe "ExtendedDocument" do
end end
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 describe "callbacks" do
before(:each) do before(:each) do
@ -103,28 +460,11 @@ describe "ExtendedDocument" do
end end
describe "save" do 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 it "should run the after filter after saving" do
@doc.run_after_save.should be_nil @doc.run_after_save.should be_nil
@doc.save.should be_true @doc.save.should be_true
@doc.run_after_save.should be_true @doc.run_after_save.should be_true
end 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 end
describe "create" do describe "create" do
it "should run the before save filter when creating" 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.create.should_not be_nil
@doc.run_before_save.should be_true @doc.run_before_save.should be_true
end 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 it "should run the before create filter" do
@doc.run_before_create.should be_nil @doc.run_before_create.should be_nil
@doc.create.should_not be_nil @doc.create.should_not be_nil

View file

@ -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

View file

@ -1,7 +1,8 @@
require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'invoice') 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 describe "ExtendedDocument properties" do
@ -43,7 +44,6 @@ describe "ExtendedDocument properties" do
end end
describe "validation" do describe "validation" do
before(:each) do before(:each) do
@invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA") @invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA")
end end
@ -79,11 +79,9 @@ describe "ExtendedDocument properties" do
@invoice.save.should be_false @invoice.save.should be_false
@invoice.should be_new_document @invoice.should be_new_document
end end
end end
describe "autovalidation" do describe "autovalidation" do
before(:each) do before(:each) do
@service = Service.new(:name => "Coumpound analysis", :price => 3_000) @service = Service.new(:name => "Coumpound analysis", :price => 3_000)
end end
@ -112,7 +110,20 @@ describe "ExtendedDocument properties" do
@service.errors.on(:name).first.should == "Name must be between 4 and 19 characters long" @service.errors.on(:name).first.should == "Name must be between 4 and 19 characters long"
end end
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
end end

View file

@ -14,6 +14,10 @@ unless defined?(FIXTURE_PATH)
TEST_SERVER.default_database = TESTDB TEST_SERVER.default_database = TESTDB
end end
class Basic < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
end
def reset_test_db! def reset_test_db!
cr = TEST_SERVER cr = TEST_SERVER
db = cr.database(TESTDB) db = cr.database(TESTDB)