Renaming to CouchRest Model

Refactored basic directory structure.
Moved to ActiveSupport for Validations and Callbacks.
Cleaned up older code, and removed support for text property types.
improve_associations
Sam Lown 2010-06-20 22:01:11 +02:00
parent 9f1eea8d32
commit c280b3a29b
70 changed files with 1725 additions and 3733 deletions

View File

@ -1,21 +1,25 @@
# CouchRest::ExtendedDocument: CouchDB, not too close to the metal
# CouchRest Model: CouchDB, close to shiny metal with rounded edges
CouchRest::ExtendedDocument adds additional functionality to the standard CouchRest Document class such as
CouchRest Models adds additional functionality to the standard CouchRest Document class such as
setting properties, callbacks, typecasting, and validations.
Note: CouchRest::ExtendedDocument only supports CouchDB 0.10.0 or newer.
Originally called ExtendedDocument, the new Model structure uses ActiveModel, part of Rails 3,
for validations and callbacks.
If your project is still running Rails 2.3, you'll have to continue using ExtendedDocument as
it is not possible to load ActiveModel into programs that do not use ActiveSupport 3.0.
CouchRest Model only supports CouchDB 0.10.0 or newer.
## Install
$ sudo gem install couchrest_extended_document
$ sudo gem install couchrest_model
## Usage
## General Usage
### General
require 'couchrest_model'
require 'couchrest_extended_document'
class Cat < CouchRest::ExtendedDocument
class Cat < CouchRest::Model::Base
property :name, String
property :lives, Integer, :default => 9
@ -43,9 +47,9 @@ Note: CouchRest::ExtendedDocument only supports CouchDB 0.10.0 or newer.
@cat.random_text # Raises error!
### Properties
## Properties
Only attributes with a property definition will be stored be ExtendedDocument (as opposed
Only attributes with a property definition will be stored be CouchRest Model (as opposed
to a normal CouchRest Document which will store everything). To help prevent confusion,
a property should be considered as the definition of an attribute. An attribute must be associated
with a property, but a property may not have any attributes associated if none have been set.
@ -56,7 +60,7 @@ will only create a getter and setter passing all attribute data directly to the
provided responds to +to_json+, there will not be any problems saving it, but when loading the
data back it will either be a string, number, array, or hash:
class Cat < CouchRest::ExtendedDocument
class Cat < CouchRest::Model::Base
property :name
property :birthday
end
@ -82,7 +86,7 @@ Properties create getters and setters similar to the following:
Properties can also have a type which
will be used for casting data retrieved from CouchDB when the attribute is set:
class Cat < CouchRest::ExtendedDocument
class Cat < CouchRest::Model::Base
property :name, String
property :last_fed_at, Time
end
@ -96,7 +100,7 @@ will be used for casting data retrieved from CouchDB when the attribute is set:
Booleans or TrueClass will also create a getter with question mark at the end:
class Cat < CouchRest::ExtendedDocument
class Cat < CouchRest::Model::Base
property :awake, TrueClass, :default => true
end
@ -108,7 +112,7 @@ Defining a property as read-only will mean that its value is set only when read
database and that it will not have a setter method. You can however update a read-only
attribute using the +write_attribute+ method:
class Cat < CouchRest::ExtendedDocument
class Cat < CouchRest::Model::Base
property :name, String
property :lives, Integer, :default => 9, :readonly => true
@ -123,12 +127,12 @@ attribute using the +write_attribute+ method:
@cat.lives # Now 8!
### Property Arrays
## Property Arrays
An attribute may also contain an array of data. ExtendedDocument handles this, along
An attribute may also contain an array of data. CouchRest Model handles this, along
with casting, by defining the class of the child attributes inside an Array:
class Cat < CouchRest::ExtendedDocument
class Cat < CouchRest::Model::Base
property :name, String
property :nicknames, [String]
end
@ -144,21 +148,21 @@ When anything other than a string is set as the class of a property, the array w
into special wrapper called a CastedArray. If the child objects respond to the 'casted_by' method
(such as those created with CastedModel, below) it will contain a reference to the parent.
### Casted Models
## Casted Models
ExtendedDocument allows you to take full advantage of CouchDB's ability to store complex
CouchRest Model allows you to take full advantage of CouchDB's ability to store complex
documents and retrieve them using the CastedModel module. Simply include the module in
a Hash (or other model that responds to the [] and []= methods) and set any properties
you'd like to use. For example:
class CatToy << Hash
include CouchRest::CastedModel
include CouchRest::Model::CastedModel
property :name, String
property :purchased, Date
end
class Cat << CouchRest::ExtendedDocument
class Cat << CouchRest::Model::Base
property :name, String
property :toys, [CatToy]
end
@ -174,10 +178,10 @@ Additionally, any hashes sent to the property will automatically be converted:
Of course, to use your own classes they *must* be defined before the parent uses them otherwise
Ruby will bring up a missing constant error. To avoid this, or if you have a really simple array of data
you'd like to model, the latest version of ExtendedDocument (> 1.0.0) supports creating
you'd like to model, the latest version of CouchRest Model (> 1.0.0) supports creating
anonymous classes:
class Cat << CouchRest::ExtendedDocument
class Cat << CouchRest::Model::Base
property :name, String
property :toys do |toy|
@ -192,26 +196,34 @@ anonymous classes:
Using this method of anonymous classes will *only* create arrays of objects.
### Notable Issues
## Notable Issues
ExtendedDocument uses active_support for some of its internals. Ensure you have a stable active support gem installed
CouchRest Model uses active_support for some of its internals. Ensure you have a stable active support gem installed
or at least 3.0.0.beta4.
JSON gem versions 1.4.X are kown to cause problems with stack overflows and general badness. Version 1.2.4 appears to work fine.
### Ruby on Rails
## Ruby on Rails
CouchRest::ExtendedDocument is compatible with rails and provides some ActiveRecord-like methods.
You might also be interested in the CouchRest companion rails project:
[http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails)
CouchRest Model is compatible with rails and provides some ActiveRecord-like methods.
#### Rails 2.X
The CouchRest companion rails project
[http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) is great
for provided default connection details for your database. At the time of writting however it
does not provide explicit support for CouchRest Model.
CouchRest Model and the original CouchRest ExtendedDocument do not share the same namespace,
as such you should not have any problems using them both at the same time. This might
help with migrations.
### Rails 3.0
In your environment.rb file require the gem as follows:
Rails::Initializer.run do |config|
....
config.gem "couchrest_extended_document"
config.gem "couchrest_model"
....
end
@ -223,12 +235,11 @@ CouchRest install, from the project root directory run `rake`, or `autotest`
## Docs
API: [http://rdoc.info/projects/couchrest/couchrest_extended_document](http://rdoc.info/projects/couchrest/couchrest_extended_document)
API: [http://rdoc.info/projects/couchrest/couchrest_model](http://rdoc.info/projects/couchrest/couchrest_model)
Check the wiki for documentation and examples [http://wiki.github.com/couchrest/couchrest](http://wiki.github.com/couchrest/couchrest)
## Contact
Please post bugs, suggestions and patches to the bug tracker at [http://github.com/couchrest/couchrest/issues](http://github.com/couchrest/couchrest/issues).

View File

@ -1,6 +1,8 @@
require 'rake'
require "rake/rdoctask"
require File.join(File.expand_path(File.dirname(__FILE__)),'lib','couchrest_extended_document')
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require 'couchrest_model'
begin
require 'spec/rake/spectask'
@ -15,19 +17,20 @@ end
begin
require 'jeweler'
Jeweler::Tasks.new do |gemspec|
gemspec.name = "couchrest_extended_document"
gemspec.summary = "Extend CouchRest Document class with useful features."
gemspec.description = "CouchRest::ExtendedDocument provides aditional features to the standard CouchRest::Document class such as properties, view designs, callbacks, typecasting and validations."
gemspec.name = "couchrest_model"
gemspec.summary = "Extends the CouchRest Document for advanced modelling."
gemspec.description = "CouchRest Model provides aditional features to the standard CouchRest Document class such as properties, view designs, associations, callbacks, typecasting and validations."
gemspec.email = "jchris@apache.org"
gemspec.homepage = "http://github.com/couchrest/couchrest_extended_document"
gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber"]
gemspec.homepage = "http://github.com/couchrest/couchrest_model"
gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber", "Sam Lown"]
gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md )
gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt couchrest.gemspec) + Dir["{examples,lib,spec,utils}/**/*"] - Dir["spec/tmp"]
gemspec.has_rdoc = true
gemspec.add_dependency("couchrest", ">= 1.0.0.beta")
gemspec.add_dependency("mime-types", ">= 1.15")
gemspec.add_dependency("activesupport", ">= 2.3.5")
gemspec.version = CouchRest::ExtendedDocument::VERSION
gemspec.add_dependency("activemodel", ">= 3.0.0.beta4")
gemspec.version = CouchRest::Model::VERSION
gemspec.date = "2008-11-22"
gemspec.require_path = "lib"
end

View File

@ -4,8 +4,22 @@
* Minor enhancements
== CouchRest Model 1.0.0.beta7
* Major enhancements
* Renamed ExtendedDocument to CouchRest::Model
* Added initial support for simple belongs_to associations
* Added support for basic collection_of association (unique to document databases!)
* Moved Validation to ActiveModel
* Moved Callbacks to ActiveModel
* Removed support for properties defined using a string for the type instead of a class
== 1.0.0.beta6
* Major enhancements
* Added support for anonymous CastedModels defined in Documents
* Minor enhancements
* Added 'find_by_*' alias for finding first item in view with matching key.
* Fixed issue with active_support in Rails3 and text in README for JSON.
@ -15,11 +29,6 @@
* Setting a property of type Array (or keyed hash) must be an array or an error will be raised.
* Now possible to set Array attribute from hash where keys determine order.
* Major enhancements
* Added support for anonymous CastedModels defined in Documents
* Added initial support for simple belongs_to associations
* Added support for basic collection_of association (unique to document databases!)
== 1.0.0.beta5
* Minor enhancements

View File

@ -1,261 +0,0 @@
require File.join(File.dirname(__FILE__), "property")
require File.join(File.dirname(__FILE__), "validation")
require File.join(File.dirname(__FILE__), 'mixins')
module CouchRest
# Same as CouchRest::Document but with properties and validations
class ExtendedDocument < Document
VERSION = "1.0.0.beta6"
include CouchRest::Mixins::Callbacks
include CouchRest::Mixins::DocumentQueries
include CouchRest::Mixins::Views
include CouchRest::Mixins::DesignDoc
include CouchRest::Mixins::ExtendedAttachments
include CouchRest::Mixins::ClassProxy
include CouchRest::Mixins::Collection
include CouchRest::Mixins::AttributeProtection
include CouchRest::Mixins::Attributes
include CouchRest::Mixins::Associations
# Including validation here does not work due to the way inheritance is handled.
#include CouchRest::Validation
def self.subclasses
@subclasses ||= []
end
def self.inherited(subklass)
super
subklass.send(:include, CouchRest::Mixins::Properties)
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.inherited(subklass)
super
subklass.properties = self.properties.dup
end
EOS
subclasses << subklass
end
# Accessors
attr_accessor :casted_by
# Callbacks
define_callbacks :create, "result == :halt"
define_callbacks :save, "result == :halt"
define_callbacks :update, "result == :halt"
define_callbacks :destroy, "result == :halt"
# Creates a new instance, bypassing attribute protection
#
#
# ==== Returns
# a document instance
def self.create_from_database(doc = {})
base = (doc['couchrest-type'].blank? || doc['couchrest-type'] == self.to_s) ? self : doc['couchrest-type'].constantize
base.new(doc, :directly_set_attributes => true)
end
# Instantiate a new ExtendedDocument by preparing all properties
# using the provided document hash.
#
# Options supported:
#
# * :directly_set_attributes: true when data comes directly from database
#
def initialize(doc = {}, options = {})
prepare_all_attributes(doc, options) # defined in CouchRest::Mixins::Attributes
super(doc)
unless self['_id'] && self['_rev']
self['couchrest-type'] = self.class.to_s
end
after_initialize if respond_to?(:after_initialize)
end
# Defines an instance and save it directly to the database
#
# ==== Returns
# returns the reloaded document
def self.create(options)
instance = new(options)
instance.create
instance
end
# Defines an instance and save it directly to the database
#
# ==== Returns
# returns the reloaded document or raises an exception
def self.create!(options)
instance = new(options)
instance.create!
instance
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 self.timestamps!
class_eval <<-EOS, __FILE__, __LINE__
property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
set_callback :save, :before do |object|
write_attribute('updated_at', Time.now)
write_attribute('created_at', Time.now) if object.new?
end
EOS
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 self.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
# Temp solution to make the view_by methods available
def self.method_missing(m, *args, &block)
if has_view?(m)
query = args.shift || {}
return view(m, query, *args, &block)
elsif m.to_s =~ /^find_(by_.+)/
view_name = $1
if has_view?(view_name)
return first_from_view(view_name, *args)
end
end
super
end
### instance methods
# Gets a reference to the actual document in the DB
# Calls up to the next document if there is one,
# Otherwise we're at the top and we return self
def base_doc
return self if base_doc?
@casted_by.base_doc
end
# Checks if we're the top document
def base_doc?
!@casted_by
end
# for compatibility with old-school frameworks
alias :new_record? :new?
alias :new_document? :new?
# Trigger the callbacks (before, after, around)
# and create the document
# It's important to have a create callback since you can't check if a document
# was new after you saved it
#
# When creating a document, both the create and the save callbacks will be triggered.
def create(bulk = false)
caught = catch(:halt) do
_run_create_callbacks do
_run_save_callbacks do
create_without_callbacks(bulk)
end
end
end
end
# unlike save, create returns the newly created document
def create_without_callbacks(bulk =false)
raise ArgumentError, "a document requires a database to be created to (The document or the #{self.class} default database were not set)" unless database
set_unique_id if new? && self.respond_to?(:set_unique_id)
result = database.save_doc(self, bulk)
(result["ok"] == true) ? self : false
end
# Creates the document in the db. Raises an exception
# if the document is not created properly.
def create!
raise "#{self.inspect} failed to save" unless self.create
end
# Trigger the callbacks (before, after, around)
# only if the document isn't new
def update(bulk = false)
caught = catch(:halt) do
if self.new?
save(bulk)
else
_run_update_callbacks do
_run_save_callbacks do
save_without_callbacks(bulk)
end
end
end
end
end
# Trigger the callbacks (before, after, around)
# and save the document
def save(bulk = false)
caught = catch(:halt) do
if self.new?
_run_save_callbacks do
save_without_callbacks(bulk)
end
else
update(bulk)
end
end
end
# Overridden to set the unique ID.
# Returns a boolean value
def save_without_callbacks(bulk = false)
raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database
set_unique_id if new? && self.respond_to?(:set_unique_id)
result = database.save_doc(self, bulk)
result["ok"] == true
end
# Saves the document to the db using save. Raises an exception
# if the document is not saved properly.
def save!
raise "#{self.inspect} failed to save" unless self.save
true
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(bulk=false)
caught = catch(:halt) do
_run_destroy_callbacks do
result = database.delete_doc(self, bulk)
if result['ok']
self.delete('_rev')
self.delete('_id')
end
result['ok']
end
end
end
end
end

View File

@ -1,13 +0,0 @@
mixins_dir = File.join(File.dirname(__FILE__), 'mixins')
require File.join(mixins_dir, 'callbacks')
require File.join(mixins_dir, 'properties')
require File.join(mixins_dir, 'document_queries')
require File.join(mixins_dir, 'views')
require File.join(mixins_dir, 'design_doc')
require File.join(mixins_dir, 'extended_attachments')
require File.join(mixins_dir, 'class_proxy')
require File.join(mixins_dir, 'collection')
require File.join(mixins_dir, 'attribute_protection')
require File.join(mixins_dir, 'attributes')
require File.join(mixins_dir, 'associations')

View File

@ -1,532 +0,0 @@
# Copyright (c) 2006-2009 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Extracted from ActiveSupport::NewCallbacks written by Yehuda Katz
# http://github.com/rails/rails/raw/d6e4113c83a9d55be6f2af247da2cecaa855f43b/activesupport/lib/active_support/new_callbacks.rb
# http://github.com/rails/rails/commit/1126a85aed576402d978e6f76eb393b6baaa9541
module CouchRest
module Mixins
# Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
# before or after an alteration of the object state.
#
# Mixing in this module allows you to define callbacks in your class.
#
# Example:
# class Storage
# include ActiveSupport::Callbacks
#
# define_callbacks :save
# end
#
# class ConfigStorage < Storage
# save_callback :before, :saving_message
# def saving_message
# puts "saving..."
# end
#
# save_callback :after do |object|
# puts "saved"
# end
#
# def save
# _run_save_callbacks do
# puts "- save"
# end
# end
# end
#
# config = ConfigStorage.new
# config.save
#
# Output:
# saving...
# - save
# saved
#
# Callbacks from parent classes are inherited.
#
# Example:
# class Storage
# include ActiveSupport::Callbacks
#
# define_callbacks :save
#
# save_callback :before, :prepare
# def prepare
# puts "preparing save"
# end
# end
#
# class ConfigStorage < Storage
# save_callback :before, :saving_message
# def saving_message
# puts "saving..."
# end
#
# save_callback :after do |object|
# puts "saved"
# end
#
# def save
# _run_save_callbacks do
# puts "- save"
# end
# end
# end
#
# config = ConfigStorage.new
# config.save
#
# Output:
# preparing save
# saving...
# - save
# saved
module Callbacks
def self.included(klass)
klass.extend ClassMethods
end
def run_callbacks(kind, options = {}, &blk)
send("_run_#{kind}_callbacks", &blk)
end
class Callback
@@_callback_sequence = 0
attr_accessor :filter, :kind, :name, :options, :per_key, :klass
def initialize(filter, kind, options, klass)
@kind, @klass = kind, klass
normalize_options!(options)
@per_key = options.delete(:per_key)
@raw_filter, @options = filter, options
@filter = _compile_filter(filter)
@compiled_options = _compile_options(options)
@callback_id = next_id
_compile_per_key_options
end
def clone(klass)
obj = super()
obj.klass = klass
obj.per_key = @per_key.dup
obj.options = @options.dup
obj.per_key[:if] = @per_key[:if].dup
obj.per_key[:unless] = @per_key[:unless].dup
obj.options[:if] = @options[:if].dup
obj.options[:unless] = @options[:unless].dup
obj
end
def normalize_options!(options)
options[:if] = Array.wrap(options[:if])
options[:unless] = Array.wrap(options[:unless])
options[:per_key] ||= {}
options[:per_key][:if] = Array.wrap(options[:per_key][:if])
options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
end
def next_id
@@_callback_sequence += 1
end
def matches?(_kind, _filter)
@kind == _kind &&
@filter == _filter
end
def _update_filter(filter_options, new_options)
filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
end
def recompile!(_options, _per_key)
_update_filter(self.options, _options)
_update_filter(self.per_key, _per_key)
@callback_id = next_id
@filter = _compile_filter(@raw_filter)
@compiled_options = _compile_options(@options)
_compile_per_key_options
end
def _compile_per_key_options
key_options = _compile_options(@per_key)
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def _one_time_conditions_valid_#{@callback_id}?
true #{key_options[0]}
end
RUBY_EVAL
end
# This will supply contents for before and around filters, and no
# contents for after filters (for the forward pass).
def start(key = nil, options = {})
object, terminator = (options || {}).values_at(:object, :terminator)
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
terminator ||= false
# options[0] is the compiled form of supplied conditions
# options[1] is the "end" for the conditional
if @kind == :before || @kind == :around
if @kind == :before
# if condition # before_save :filter_name, :if => :condition
# filter_name
# end
filter = <<-RUBY_EVAL
unless halted
result = #{@filter}
halted = (#{terminator})
end
RUBY_EVAL
[@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
else
# Compile around filters with conditions into proxy methods
# that contain the conditions.
#
# For `around_save :filter_name, :if => :condition':
#
# def _conditional_callback_save_17
# if condition
# filter_name do
# yield self
# end
# else
# yield self
# end
# end
name = "_conditional_callback_#{@kind}_#{next_id}"
txt, line = <<-RUBY_EVAL, __LINE__ + 1
def #{name}(halted)
#{@compiled_options[0] || "if true"} && !halted
#{@filter} do
yield self
end
else
yield self
end
end
RUBY_EVAL
@klass.class_eval(txt, __FILE__, line)
"#{name}(halted) do"
end
end
end
# This will supply contents for around and after filters, but not
# before filters (for the backward pass).
def end(key = nil, options = {})
object = (options || {})[:object]
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
if @kind == :around || @kind == :after
# if condition # after_save :filter_name, :if => :condition
# filter_name
# end
if @kind == :after
[@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
else
"end"
end
end
end
private
# Options support the same options as filters themselves (and support
# symbols, string, procs, and objects), so compile a conditional
# expression based on the options
def _compile_options(options)
return [] if options[:if].empty? && options[:unless].empty?
conditions = []
unless options[:if].empty?
conditions << Array.wrap(_compile_filter(options[:if]))
end
unless options[:unless].empty?
conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
end
["if #{conditions.flatten.join(" && ")}", "end"]
end
# Filters support:
# Arrays:: Used in conditions. This is used to specify
# multiple conditions. Used internally to
# merge conditions from skip_* filters
# Symbols:: A method to call
# Strings:: Some content to evaluate
# Procs:: A proc to call with the object
# Objects:: An object with a before_foo method on it to call
#
# All of these objects are compiled into methods and handled
# the same after this point:
# Arrays:: Merged together into a single filter
# Symbols:: Already methods
# Strings:: class_eval'ed into methods
# Procs:: define_method'ed into methods
# Objects::
# a method is created that calls the before_foo method
# on the object.
def _compile_filter(filter)
method_name = "_callback_#{@kind}_#{next_id}"
case filter
when Array
filter.map {|f| _compile_filter(f)}
when Symbol
filter
when String
"(#{filter})"
when Proc
@klass.send(:define_method, method_name, &filter)
return method_name if filter.arity == 0
method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
else
@klass.send(:define_method, "#{method_name}_object") { filter }
_normalize_legacy_filter(kind, filter)
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{method_name}(&blk)
#{method_name}_object.send(:#{kind}, self, &blk)
end
RUBY_EVAL
method_name
end
end
def _normalize_legacy_filter(kind, filter)
if !filter.respond_to?(kind) && filter.respond_to?(:filter)
filter.class_eval(
"def #{kind}(context, &block) filter(context, &block) end",
__FILE__, __LINE__ - 1)
elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around
def filter.around(context)
should_continue = before(context)
yield if should_continue
after(context)
end
end
end
end
# An Array with a compile method
class CallbackChain < Array
def initialize(symbol)
@symbol = symbol
end
def compile(key = nil, options = {})
method = []
method << "halted = false"
each do |callback|
method << callback.start(key, options)
end
method << "yield self if block_given? && !halted"
reverse_each do |callback|
method << callback.end(key, options)
end
method.compact.join("\n")
end
def clone(klass)
chain = CallbackChain.new(@symbol)
chain.push(*map {|c| c.clone(klass)})
end
end
module ClassMethods
#CHAINS = {:before => :before, :around => :before, :after => :after}
# Make the _run_save_callbacks method. The generated method takes
# a block that it'll yield to. It'll call the before and around filters
# in order, yield the block, and then run the after filters.
#
# _run_save_callbacks do
# save
# end
#
# The _run_save_callbacks method can optionally take a key, which
# will be used to compile an optimized callback method for each
# key. See #define_callbacks for more information.
def _define_runner(symbol)
body = send("_#{symbol}_callback").
compile(nil, :terminator => send("_#{symbol}_terminator"))
body, line = <<-RUBY_EVAL, __LINE__ + 1
def _run_#{symbol}_callbacks(key = nil, &blk)
if key
name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks"
unless respond_to?(name)
self.class._create_keyed_callback(name, :#{symbol}, self, &blk)
end
send(name, &blk)
else
#{body}
end
end
RUBY_EVAL
undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks")
class_eval body, __FILE__, line
end
# This is called the first time a callback is called with a particular
# key. It creates a new callback method for the key, calculating
# which callbacks can be omitted because of per_key conditions.
def _create_keyed_callback(name, kind, obj, &blk)
@_keyed_callbacks ||= {}
@_keyed_callbacks[name] ||= begin
str = send("_#{kind}_callback").
compile(name, :object => obj, :terminator => send("_#{kind}_terminator"))
class_eval "def #{name}() #{str} end", __FILE__, __LINE__
true
end
end
# Define callbacks.
#
# Creates a <name>_callback method that you can use to add callbacks.
#
# Syntax:
# save_callback :before, :before_meth
# save_callback :after, :after_meth, :if => :condition
# save_callback :around {|r| stuff; yield; stuff }
#
# The <name>_callback method also updates the _run_<name>_callbacks
# method, which is the public API to run the callbacks.
#
# Also creates a skip_<name>_callback method that you can use to skip
# callbacks.
#
# When creating or skipping callbacks, you can specify conditions that
# are always the same for a given key. For instance, in ActionPack,
# we convert :only and :except conditions into per-key conditions.
#
# before_filter :authenticate, :except => "index"
# becomes
# dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
#
# Per-Key conditions are evaluated only once per use of a given key.
# In the case of the above example, you would do:
#
# run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
#
# In that case, each action_name would get its own compiled callback
# method that took into consideration the per_key conditions. This
# is a speed improvement for ActionPack.
def _update_callbacks(name, filters = CallbackChain.new(name), block = nil)
type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
options = filters.last.is_a?(Hash) ? filters.pop : {}
filters.unshift(block) if block
callbacks = send("_#{name}_callback")
yield callbacks, type, filters, options if block_given?
_define_runner(name)
end
alias_method :_reset_callbacks, :_update_callbacks
def set_callback(name, *filters, &block)
_update_callbacks(name, filters, block) do |callbacks, type, filters, options|
filters.map! do |filter|
# overrides parent class
callbacks.delete_if {|c| c.matches?(type, filter) }
Callback.new(filter, type, options.dup, self)
end
options[:prepend] ? callbacks.unshift(*filters) : callbacks.push(*filters)
end
end
def skip_callback(name, *filters, &block)
_update_callbacks(name, filters, block) do |callbacks, type, filters, options|
filters.each do |filter|
callbacks = send("_#{name}_callback=", callbacks.clone(self))
filter = callbacks.find {|c| c.matches?(type, filter) }
if filter && options.any?
filter.recompile!(options, options[:per_key] || {})
else
callbacks.delete(filter)
end
end
end
end
def define_callbacks(*symbols)
terminator = symbols.pop if symbols.last.is_a?(String)
symbols.each do |symbol|
extlib_inheritable_accessor("_#{symbol}_terminator") { terminator }
extlib_inheritable_accessor("_#{symbol}_callback") do
CallbackChain.new(symbol)
end
_define_runner(symbol)
# Define more convenient callback methods
# set_callback(:save, :before) becomes before_save
[:before, :after, :around].each do |filter|
self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def self.#{filter}_#{symbol}(*symbols, &blk)
_alias_callbacks(symbols, blk) do |callback, options|
set_callback(:#{symbol}, :#{filter}, callback, options)
end
end
RUBY_EVAL
end
end
end
def _alias_callbacks(callbacks, block)
options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
callbacks.push(block) if block
callbacks.each do |callback|
yield callback, options
end
end
end
end
end
end

10
lib/couchrest/model.rb Normal file
View File

@ -0,0 +1,10 @@
module CouchRest
module Model
VERSION = "1.0.0.beta7"
end
end

View File

@ -1,7 +1,5 @@
module CouchRest
module Mixins
module Model
module Associations
# Basic support for relationships between ExtendedDocuments
@ -47,12 +45,12 @@ module CouchRest
# collection_of :groups
#
# creates a pseudo property called "groups" which allows access
# to a CollectionProxy object. Adding, replacing or removing entries in this
# to a CollectionOfProxy object. Adding, replacing or removing entries in this
# proxy will cause the matching property array, in this case "group_ids", to
# be kept in sync.
#
# Any manual changes made to the collection ids property (group_ids), unless replaced, will require
# a reload of the CollectionProxy for the two sets of data to be in sync:
# a reload of the CollectionOfProxy for the two sets of data to be in sync:
#
# group_ids = ['123']
# groups == [Group.get('123')]
@ -63,7 +61,7 @@ module CouchRest
# Of course, saving the parent record will store the collection ids as they are
# found.
#
# The CollectionProxy supports the following array functions, anything else will cause
# The CollectionOfProxy supports the following array functions, anything else will cause
# a mismatch between the collection objects and collection ids:
#
# groups << obj
@ -128,7 +126,7 @@ module CouchRest
### collection_of support methods
def create_collection_of_property_setter(attrib, property, options)
# ensure CollectionProxy is nil, ready to be reloaded on request
# ensure CollectionOfProxy is nil, ready to be reloaded on request
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{options[:foreign_key]}=(value)
@#{attrib} = nil
@ -143,7 +141,7 @@ module CouchRest
def #{attrib}(reload = false)
return @#{attrib} unless @#{attrib}.nil? or reload
ary = self.#{options[:foreign_key]}.collect{|i| #{base}.get(i)}
@#{attrib} = ::CouchRest::CollectionProxy.new(ary, self, '#{options[:foreign_key]}')
@#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
end
EOS
end
@ -151,7 +149,7 @@ module CouchRest
def create_collection_of_setter(attrib, property, options)
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{attrib}=(value)
@#{attrib} = ::CouchRest::CollectionProxy.new(value, self, '#{options[:foreign_key]}')
@#{attrib} = ::CouchRest::CollectionOfProxy.new(value, self, '#{options[:foreign_key]}')
end
EOS
end
@ -163,7 +161,7 @@ module CouchRest
# Special proxy for a collection of items so that adding and removing
# to the list automatically updates the associated property.
class CollectionProxy < Array
class CollectionOfProxy < Array
attr_accessor :property
attr_accessor :casted_by

View File

@ -1,5 +1,5 @@
module CouchRest
module Mixins
module Model
module AttributeProtection
# Attribute protection from mass assignment to CouchRest properties
#

View File

@ -1,5 +1,5 @@
module CouchRest
module Mixins
module Model
module Attributes
## Support for handling attributes

View File

@ -0,0 +1,93 @@
module CouchRest
module Model
class Base < Document
extend ActiveModel::Naming
include CouchRest::Model::Persistence
include CouchRest::Model::Callbacks
include CouchRest::Model::DocumentQueries
include CouchRest::Model::Views
include CouchRest::Model::DesignDoc
include CouchRest::Model::ExtendedAttachments
include CouchRest::Model::ClassProxy
include CouchRest::Model::Collection
include CouchRest::Model::AttributeProtection
include CouchRest::Model::Attributes
include CouchRest::Model::Associations
include CouchRest::Model::Validations
def self.subclasses
@subclasses ||= []
end
def self.inherited(subklass)
super
subklass.send(:include, CouchRest::Model::Properties)
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.inherited(subklass)
super
subklass.properties = self.properties.dup
# This is nasty:
subklass._validators = self._validators.dup
end
EOS
subclasses << subklass
end
# Accessors
attr_accessor :casted_by
# Instantiate a new ExtendedDocument by preparing all properties
# using the provided document hash.
#
# Options supported:
#
# * :directly_set_attributes: true when data comes directly from database
#
def initialize(doc = {}, options = {})
prepare_all_attributes(doc, options)
super(doc)
unless self['_id'] && self['_rev']
self['couchrest-type'] = self.class.to_s
end
after_initialize if respond_to?(:after_initialize)
end
# Temp solution to make the view_by methods available
def self.method_missing(m, *args, &block)
if has_view?(m)
query = args.shift || {}
return view(m, query, *args, &block)
elsif m.to_s =~ /^find_(by_.+)/
view_name = $1
if has_view?(view_name)
return first_from_view(view_name, *args)
end
end
super
end
### instance methods
# Gets a reference to the actual document in the DB
# Calls up to the next document if there is one,
# Otherwise we're at the top and we return self
def base_doc
return self if base_doc?
@casted_by.base_doc
end
# Checks if we're the top document
def base_doc?
!@casted_by
end
# for compatibility with old-school frameworks
alias :new_record? :new?
alias :new_document? :new?
end
end
end

View File

@ -0,0 +1,27 @@
# encoding: utf-8
module CouchRest #:nodoc:
module Model #:nodoc:
module Callbacks
extend ActiveSupport::Concern
included do
extend ActiveModel::Callbacks
define_model_callbacks \
:create,
:destroy,
:save,
:update,
:validate
end
def valid?(*) #nodoc
_run_validation_callbacks { super }
end
end
end
end

View File

@ -3,7 +3,7 @@
# elements of the array.
#
module CouchRest
module CouchRest::Model
class CastedArray < Array
attr_accessor :casted_by
attr_accessor :property

View File

@ -1,13 +1,16 @@
module CouchRest
module CouchRest::Model
module CastedModel
def self.included(base)
base.send(:include, ::CouchRest::Mixins::AttributeProtection)
base.send(:include, ::CouchRest::Mixins::Attributes)
base.send(:include, ::CouchRest::Mixins::Callbacks)
base.send(:include, ::CouchRest::Mixins::Properties)
base.send(:include, ::CouchRest::Mixins::Associations)
base.send(:attr_accessor, :casted_by)
extend ActiveSupport::Concern
included do
include CouchRest::Model::AttributeProtection
include CouchRest::Model::Attributes
include CouchRest::Model::Callbacks
include CouchRest::Model::Properties
include CouchRest::Model::Associations
include CouchRest::Model::Validations
attr_accessor :casted_by
end
def initialize(keys = {})

View File

@ -1,5 +1,5 @@
module CouchRest
module Mixins
module Model
module ClassProxy
def self.included(base)
@ -36,7 +36,7 @@ module CouchRest
@database = database
end
# ExtendedDocument
# Base
def new(*args)
doc = @klass.new(*args)
@ -57,7 +57,7 @@ module CouchRest
super
end
# Mixins::DocumentQueries
# DocumentQueries
def all(opts = {}, &block)
docs = @klass.all({:database => @database}.merge(opts), &block)
@ -82,7 +82,7 @@ module CouchRest
end
alias :find :get
# Mixins::Views
# Views
def has_view?(view)
@klass.has_view?(view)
@ -102,7 +102,7 @@ module CouchRest
doc
end
# Mixins::DesignDoc
# DesignDoc
def design_doc
@klass.design_doc
@ -116,16 +116,6 @@ module CouchRest
@klass.save_design_doc(@database)
end
# DEPRICATED
def all_design_doc_versions
@klass.all_design_doc_versions(@database)
end
def stored_design_doc
@klass.stored_design_doc(@database)
end
alias :model_design_doc :stored_design_doc
end
end
end

View File

@ -1,5 +1,5 @@
module CouchRest
module Mixins
module Model
module Collection
def self.included(base)

View File

@ -1,7 +1,6 @@
require 'digest/md5'
# encoding: utf-8
module CouchRest
module Mixins
module Model
module DesignDoc
def self.included(base)

View File

@ -1,5 +1,5 @@
module CouchRest
module Mixins
module Model
module DocumentQueries
def self.included(base)

View File

@ -0,0 +1,23 @@
# encoding: utf-8
module CouchRest
module Model
module Errors
class CouchRestModelError < StandardError; end
# Raised when a persisence method ending in ! fails validation. The message
# will contain the full error messages from the +Document+ in question.
#
# Example:
#
# <tt>Validations.new(person.errors)</tt>
class Validations < CouchRestModelError
attr_reader :document
def initialize(document)
@document = document
super("Validation Failed: #{@document.errors.full_messages.join(", ")}")
end
end
end
end
end

View File

@ -1,5 +1,5 @@
module CouchRest
module Mixins
module Model
module ExtendedAttachments
# Add a file attachment to the current document. Expects

View File

@ -0,0 +1,141 @@
module CouchRest
module Model
module Persistence
extend ActiveSupport::Concern
# Create the document. Validation is enabled by default and will return
# false if the document is not valid. If all goes well, the document will
# be returned.
def create(options = {})
return false unless perform_validations(options)
_run_create_callbacks do
_run_save_callbacks do
set_unique_id if new? && self.respond_to?(:set_unique_id)
result = database.save_doc(self)
(result["ok"] == true) ? self : false
end
end
end
# Creates the document in the db. Raises an exception
# if the document is not created properly.
def create!
self.class.fail_validate!(self) unless self.create
end
# Trigger the callbacks (before, after, around)
# only if the document isn't new
def update(options = {})
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
return false unless perform_validations(options)
_run_update_callbacks do
_run_save_callbacks do
result = database.save_doc(self)
result["ok"] == true
end
end
end
# Trigger the callbacks (before, after, around) and save the document
def save(options = {})
self.new? ? create(options) : update(options)
end
# Saves the document to the db using save. Raises an exception
# if the document is not saved properly.
def save!
self.class.fail_validate!(self) unless self.save
true
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> if required.
def destroy
_run_destroy_callbacks do
result = database.delete_doc(self)
if result['ok']
self.delete('_rev')
self.delete('_id')
end
result['ok']
end
end
protected
def perform_validations(options = {})
perform_validation = case options
when Hash
options[:validate] != false
else
options
end
perform_validation ? valid? : true
end
module ClassMethods
# Creates a new instance, bypassing attribute protection
#
#
# ==== Returns
# a document instance
def create_from_database(doc = {})
base = (doc['couchrest-type'].blank? || doc['couchrest-type'] == self.to_s) ? self : doc['couchrest-type'].constantize
base.new(doc, :directly_set_attributes => true)
end
# Defines an instance and save it directly to the database
#
# ==== Returns
# returns the reloaded document
def create(attributes = {})
instance = new(attributes)
instance.create
instance
end
# Defines an instance and save it directly to the database
#
# ==== Returns
# returns the reloaded document or raises an exception
def create!(attributes = {})
instance = new(attributes)
instance.create!
instance
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
# Raise an error if validation failed.
def fail_validate!(document)
raise Errors::Validations.new(document)
end
end
end
end
end

View File

@ -1,9 +1,6 @@
require 'time'
require File.join(File.dirname(__FILE__), '..', 'property')
require File.join(File.dirname(__FILE__), '..', 'casted_array')
# encoding: utf-8
module CouchRest
module Mixins
module Model
module Properties
class IncludeError < StandardError; end
@ -30,7 +27,7 @@ module CouchRest
end
def write_attribute(property, value)
prop = property.is_a?(::CouchRest::Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
raise "Missing property definition for #{property.to_s}" unless prop
self[prop.to_s] = prop.cast(self, value)
end
@ -59,6 +56,21 @@ module CouchRest
define_property(name, opts, &block)
end
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!
class_eval <<-EOS, __FILE__, __LINE__
property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
set_callback :save, :before do |object|
write_attribute('updated_at', Time.now)
write_attribute('created_at', Time.now) if object.new?
end
EOS
end
protected
@ -74,9 +86,12 @@ module CouchRest
type.class_eval { yield type }
type = [type] # inject as an array
end
property = CouchRest::Property.new(name, type, options)
property = Property.new(name, type, options)
create_property_getter(property)
create_property_setter(property) unless property.read_only == true
if property.type_class.respond_to?(:validates_casted_model)
validates_casted_model property.name
end
properties << property
property
end

View File

@ -1,14 +1,10 @@
require File.join(File.dirname(__FILE__), 'mixins', 'typecast')
module CouchRest
# Basic attribute support for adding getter/setter + validation
# encoding: utf-8
module CouchRest::Model
class Property
include ::CouchRest::Mixins::Typecast
include ::CouchRest::Model::Typecast
attr_reader :name, :type, :read_only, :alias, :default, :casted, :init_method, :options
attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :options
# Attribute to define.
# All Properties are assumed casted unless the type is nil.
@ -42,7 +38,7 @@ module CouchRest
end
arr = value.collect { |data| cast_value(parent, data) }
# allow casted_by calls to be passed up chain by wrapping in CastedArray
value = type_class != String ? ::CouchRest::CastedArray.new(arr, self) : arr
value = type_class != String ? CastedArray.new(arr, self) : arr
value.casted_by = parent if value.respond_to?(:casted_by)
elsif !value.nil?
value = cast_value(parent, value)
@ -66,17 +62,6 @@ module CouchRest
end
end
# Always provide the basic type as a class. If the type
# is an array, the class will be extracted.
def type_class
return String unless casted # This is rubbish, to handle validations
return @type_class unless @type_class.nil?
base = @type.is_a?(Array) ? @type.first : @type
base = String if base.nil?
base = TrueClass if base.is_a?(String) && base.downcase == 'boolean'
@type_class = base.is_a?(Class) ? base : base.constantize
end
private
def associate_casted_value_to_parent(parent, value)
@ -88,7 +73,12 @@ module CouchRest
if type.nil?
@casted = false
@type = nil
@type_class = nil
else
base = type.is_a?(Array) ? type.first : type
base = Object if base.nil?
raise "Defining a property type as a #{type.class.name.humanize} is not supported in CouchRest Model!" if base.class != Class
@type_class = base
@type = type
end
end

View File

@ -5,13 +5,13 @@ module CouchRest
alias :delete_old! :delete!
def delete!
clear_extended_doc_fresh_cache
clear_model_fresh_cache
delete_old!
end
# If the database is deleted, ensure that the design docs will be refreshed.
def clear_extended_doc_fresh_cache
::CouchRest::ExtendedDocument.subclasses.each{|klass| klass.req_design_doc_refresh if klass.respond_to?(:req_design_doc_refresh)}
def clear_model_fresh_cache
::CouchRest::Model::Base.subclasses.each{|klass| klass.req_design_doc_refresh if klass.respond_to?(:req_design_doc_refresh)}
end
end

View File

@ -21,7 +21,7 @@ CouchRest::Document.class_eval do
alias_method :kind_of?, :is_a?
end
CouchRest::CastedModel.class_eval do
CouchRest::Model::CastedModel.class_eval do
# The to_param method is needed for rails to generate resourceful routes.
# In your controller, remember that it's actually the id of the document.
def id
@ -30,13 +30,3 @@ CouchRest::CastedModel.class_eval do
end
alias_method :to_param, :id
end
require Pathname.new(File.dirname(__FILE__)).join('..', 'validation', 'validation_errors')
CouchRest::Validation::ValidationErrors.class_eval do
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such.
# This method is called by error_messages_for
def count
errors.values.inject(0) { |error_count, errors_for_attribute| error_count + errors_for_attribute.size }
end
end

View File

@ -1,7 +1,3 @@
require 'time'
require 'bigdecimal'
require 'bigdecimal/util'
class Time
# returns a local time value much faster than Time.parse
def self.mktime_with_offset(string)
@ -22,7 +18,7 @@ class Time
end
module CouchRest
module Mixins
module Model
module Typecast
def typecast_value(value, property) # klass, init_method)

View File

@ -0,0 +1,32 @@
# encoding: utf-8
require "couchrest/model/validations/casted_model"
module CouchRest
module Model
# Validations may be applied to both Model::Base and Model::CastedModel
module Validations
extend ActiveSupport::Concern
included do
include ActiveModel::Validations
end
module ClassMethods
# Validates the associated casted model. This method should not be
# used within your code as it is automatically included when a CastedModel
# is used inside the model.
#
def validates_casted_model(*args)
validates_with(CastedModelValidator, _merge_attributes(args))
end
# TODO: Here will lie validates_uniqueness_of
end
end
end
end

View File

@ -0,0 +1,14 @@
module CouchRest
module Model
module Validations
class CastedModelValidator < ActiveModel::EachValidator
def validate_each(document, attribute, value)
values = value.is_a?(Array) ? value : [value]
return if values.collect {|doc| doc.nil? || doc.valid? }.all?
document.errors.add(attribute, :invalid, :default => options[:message], :value => value)
end
end
end
end
end

View File

@ -1,10 +1,7 @@
module CouchRest
module Mixins
module Model
module Views
def self.included(base)
base.extend(ClassMethods)
end
extend ActiveSupport::Concern
module ClassMethods
# Define a CouchDB view. The name of the view will be the concatenation

View File

@ -1,244 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class Object
def validatable?
false
end
end
require 'pathname'
dir = File.join(Pathname(__FILE__).dirname.expand_path, 'validation')
require File.join(dir, 'validation_errors')
require File.join(dir, 'contextual_validators')
require File.join(dir, 'auto_validate')
require File.join(dir, 'validators', 'generic_validator')
require File.join(dir, 'validators', 'required_field_validator')
require File.join(dir, 'validators', 'absent_field_validator')
require File.join(dir, 'validators', 'format_validator')
require File.join(dir, 'validators', 'length_validator')
require File.join(dir, 'validators', 'numeric_validator')
require File.join(dir, 'validators', 'method_validator')
require File.join(dir, 'validators', 'confirmation_validator')
module CouchRest
module Validation
def self.included(base)
base.extlib_inheritable_accessor(:auto_validation)
base.class_eval <<-EOS, __FILE__, __LINE__ + 1
# Callbacks
define_callbacks :validate
# Turn off auto validation by default
self.auto_validation ||= false
# Force the auto validation for the class properties
# This feature is still not fully ported over,
# test are lacking, so please use with caution
def self.auto_validate!
self.auto_validation = true
end
# share the validations with subclasses
def self.inherited(subklass)
self.validators.contexts.each do |k, v|
subklass.validators.contexts[k] = v.dup
end
super
end
EOS
base.extend(ClassMethods)
base.class_eval <<-EOS, __FILE__, __LINE__ + 1
define_callbacks :validate
if method_defined?(:_run_save_callbacks)
set_callback :save, :before, :check_validations
end
EOS
base.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def self.define_property(name, options={}, &block)
property = super
auto_generate_validations(property) unless property.nil?
end
RUBY_EVAL
end
# Ensures the object is valid for the context provided, and otherwise
# throws :halt and returns false.
#
def check_validations(context = :default)
throw(:halt, false) unless context.nil? || valid?(context)
end
# Return the ValidationErrors
#
def errors
@errors ||= ValidationErrors.new
end
# Mark this resource as validatable. When we validate associations of a
# resource we can check if they respond to validatable? before trying to
# recursivly validate them
#
def validatable?
true
end
# Alias for valid?(:default)
#
def valid_for_default?
valid?(:default)
end
# Check if a resource is valid in a given context
#
def valid?(context = :default)
recursive_valid?(self, context, true)
end
# checking on casted objects
def validate_casted_arrays
result = true
array_casted_properties = self.class.properties.select { |property| property.casted && property.type.instance_of?(Array) }
array_casted_properties.each do |property|
casted_values = self.send(property.name)
next unless casted_values.is_a?(Array) && casted_values.first.respond_to?(:valid?)
casted_values.each do |value|
result = (result && value.valid?) if value.respond_to?(:valid?)
end
end
result
end
# Do recursive validity checking
#
def recursive_valid?(target, context, state)
valid = state
target.each do |key, prop|
if prop.is_a?(Array)
prop.each do |item|
if item.validatable?
valid = recursive_valid?(item, context, valid) && valid
end
end
elsif prop.validatable?
valid = recursive_valid?(prop, context, valid) && valid
end
end
target._run_validate_callbacks do
target.class.validators.execute(context, target) && valid
end
end
def validation_property_value(name)
self.respond_to?(name, true) ? self.send(name) : nil
end
# Get the corresponding Object property, if it exists.
def validation_property(field_name)
properties.find{|p| p.name == field_name}
end
module ClassMethods
include CouchRest::Validation::ValidatesPresent
include CouchRest::Validation::ValidatesAbsent
include CouchRest::Validation::ValidatesIsConfirmed
# include CouchRest::Validation::ValidatesIsPrimitive
# include CouchRest::Validation::ValidatesIsAccepted
include CouchRest::Validation::ValidatesFormat
include CouchRest::Validation::ValidatesLength
# include CouchRest::Validation::ValidatesWithin
include CouchRest::Validation::ValidatesIsNumber
include CouchRest::Validation::ValidatesWithMethod
# include CouchRest::Validation::ValidatesWithBlock
# include CouchRest::Validation::ValidatesIsUnique
include CouchRest::Validation::AutoValidate
# Return the set of contextual validators or create a new one
#
def validators
@validations ||= ContextualValidators.new
end
# Clean up the argument list and return a opts hash, including the
# merging of any default opts. Set the context to default if none is
# provided. Also allow :context to be aliased to :on, :when & group
#
def opts_from_validator_args(args, defaults = nil)
opts = args.last.kind_of?(Hash) ? args.pop : {}
context = :default
context = opts[:context] if opts.has_key?(:context)
context = opts.delete(:on) if opts.has_key?(:on)
context = opts.delete(:when) if opts.has_key?(:when)
context = opts.delete(:group) if opts.has_key?(:group)
opts[:context] = context
opts.merge!(defaults) unless defaults.nil?
opts
end
# Given a new context create an instance method of
# valid_for_<context>? which simply calls valid?(context)
# if it does not already exist
#
def create_context_instance_methods(context)
name = "valid_for_#{context.to_s}?" # valid_for_signup?
if !self.instance_methods.include?(name)
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name} # def valid_for_signup?
valid?('#{context.to_s}'.to_sym) # valid?('signup'.to_sym)
end # end
EOS
end
end
# Create a new validator of the given klazz and push it onto the
# requested context for each of the attributes in the fields list
#
def add_validator_to_context(opts, fields, klazz)
fields.each do |field|
validator = klazz.new(field.to_sym, opts)
if opts[:context].is_a?(Symbol)
unless validators.context(opts[:context]).include?(validator)
validators.context(opts[:context]) << validator
create_context_instance_methods(opts[:context])
end
elsif opts[:context].is_a?(Array)
opts[:context].each do |c|
unless validators.context(c).include?(validator)
validators.context(c) << validator
create_context_instance_methods(c)
end
end
end
end
end
end # module ClassMethods
end # module Validation
end # module CouchRest

View File

@ -1,156 +0,0 @@
# Ported from dm-migrations
module CouchRest
class Property
# flag letting us know if we already checked the autovalidation settings
attr_accessor :autovalidation_check
@autovalidation_check = false
end
module Validation
module AutoValidate
# # Force the auto validation for the class properties
# # This feature is still not fully ported over,
# # test are lacking, so please use with caution
# def auto_validate!
# auto_validation = true
# end
# adds message for validator
def options_with_message(base_options, property, validator_name)
options = base_options.clone
opts = property.options
options[:message] = if opts[:messages]
if opts[:messages].is_a?(Hash) and msg = opts[:messages][validator_name]
msg
else
nil
end
elsif opts[:message]
opts[:message]
else
nil
end
options
end
##
# Auto-generate validations for a given property. This will only occur
# if the option :auto_validation is either true or left undefined.
#
# @details [Triggers]
# Triggers that generate validator creation
#
# :nullable => false
# Setting the option :nullable to false causes a
# validates_presence_of validator to be automatically created on
# the property
#
# :size => 20 or :length => 20
# Setting the option :size or :length causes a validates_length_of
# validator to be automatically created on the property. If the
# value is a Integer the validation will set :maximum => value if
# the value is a Range the validation will set :within => value
#
# :format => :predefined / lambda / Proc
# Setting the :format option causes a validates_format_of
# validator to be automatically created on the property
#
# :set => ["foo", "bar", "baz"]
# Setting the :set option causes a validates_within
# validator to be automatically created on the property
#
# Integer type
# Using a Integer type causes a validates_numericality_of
# validator to be created for the property. integer_only
# is set to true
#
# Float type
# Using a Integer type causes a validates_is_number
# validator to be created for the property. integer_only
# is set to false, and precision/scale match the property
#
#
# Messages
#
# :messages => {..}
# Setting :messages hash replaces standard error messages
# with custom ones. For instance:
# :messages => {:presence => "Field is required",
# :format => "Field has invalid format"}
# Hash keys are: :presence, :format, :length, :is_unique,
# :is_number, :is_primitive
#
# :message => "Some message"
# It is just shortcut if only one validation option is set
#
def auto_generate_validations(property)
return unless ((property.autovalidation_check != true) && self.auto_validation)
return if (property.options && (property.options.has_key?(:auto_validation) && !property.options[:auto_validation]) || property.read_only)
# value is set by the storage system
opts = {}
opts[:context] = property.options[:validates] if property.options.has_key?(:validates)
# presence
if opts[:allow_nil] == false
validates_presence_of property.name, options_with_message(opts, property, :presence)
end
# length
if property.type_class == String
# XXX: maybe length should always return a Range, with the min defaulting to 1
# 52 being the max set
len = property.options.fetch(:length, property.options.fetch(:size, 52))
if len.is_a?(Range)
opts[:within] = len
else
opts[:maximum] = len
end
validates_length_of property.name, options_with_message(opts, property, :length)
end
# format
if property.options.has_key?(:format)
opts[:with] = property.options[:format]
# validates_format property.name, opts
validates_format property.name, options_with_message(opts, property, :format)
end
# uniqueness validator
if property.options.has_key?(:unique)
value = property.options[:unique]
if value.is_a?(Array) || value.is_a?(Symbol)
# validates_is_unique property.name, :scope => Array(value)
validates_is_unique property.name, options_with_message({:scope => Array(value)}, property, :is_unique)
elsif value.is_a?(TrueClass)
# validates_is_unique property.name
validates_is_unique property.name, options_with_message({}, property, :is_unique)
end
end
# within validator
if property.options.has_key?(:set)
validates_within property.name, options_with_message({:set => property.options[:set]}, property, :within)
end
# numeric validator
if property.type_class == Integer
opts[:integer_only] = true
validates_numericality_of property.name, options_with_message(opts, property, :is_number)
elsif Float == property.type_class
opts[:precision] = property.precision
opts[:scale] = property.scale
validates_numericality_of property.name, options_with_message(opts, property, :is_number)
end
# marked the property as checked
property.autovalidation_check = true
end
end # module AutoValidate
end # module Validation
end # module CouchRest

View File

@ -1,78 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
##
#
# @author Guy van den Berg
# @since 0.9
class ContextualValidators
def dump
contexts.each_pair do |key, context|
puts "Key=#{key} Context: #{context}"
end
end
# Get a hash of named context validators for the resource
#
# @return <Hash> a hash of validators <GenericValidator>
def contexts
@contexts ||= {}
end
# Return an array of validators for a named context
#
# @return <Array> An array of validators
def context(name)
contexts[name] ||= []
end
# Clear all named context validators off of the resource
#
def clear!
contexts.clear
end
# Execute all validators in the named context against the target
#
# @param <Symbol> named_context the context we are validating against
# @param <Object> target the resource that we are validating
# @return <Boolean> true if all are valid, otherwise false
def execute(named_context, target)
raise(ArgumentError, 'invalid context specified') if !named_context || (contexts.length > 0 && !contexts[named_context])
target.errors.clear!
result = true
context(named_context).each do |validator|
next unless validator.execute?(target)
result = false unless validator.call(target)
end
result
end
end # module ContextualValidators
end # module Validation
end # module CouchRest

View File

@ -1,125 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
##
#
# @author Guy van den Berg
# @since 0.9
class ValidationErrors
include Enumerable
@@default_error_messages = {
:absent => '%s must be absent',
:inclusion => '%s must be one of [%s]',
:invalid => '%s has an invalid format',
:confirmation => '%s does not match the confirmation',
:accepted => "%s is not accepted",
:nil => '%s must not be nil',
:blank => '%s must not be blank',
:length_between => '%s must be between %s and %s characters long',
:too_long => '%s must be less than %s characters long',
:too_short => '%s must be more than %s characters long',
:wrong_length => '%s must be %s characters long',
:taken => '%s is already taken',
:not_a_number => '%s must be a number',
:not_an_integer => '%s must be an integer',
:greater_than => '%s must be greater than %s',
:greater_than_or_equal_to => "%s must be greater than or equal to %s",
:equal_to => "%s must be equal to %s",
:less_than => '%s must be less than %s',
:less_than_or_equal_to => "%s must be less than or equal to %s",
:value_between => '%s must be between %s and %s',
:primitive => '%s must be of type %s'
}
# Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
cattr_writer :default_error_messages
def self.default_error_message(key, field, *values)
field = field.to_s.humanize
@@default_error_messages[key] % [field, *values].flatten
end
# Clear existing validation errors.
def clear!
errors.clear
end
# Add a validation error. Use the field_name :general if the errors does
# not apply to a specific field of the Resource.
#
# @param <Symbol> field_name the name of the field that caused the error
# @param <String> message the message to add
def add(field_name, message)
(errors[field_name.to_sym] ||= []) << message
end
# Collect all errors into a single list.
def full_messages
errors.inject([]) do |list, pair|
list += pair.last
end
end
# Return validation errors for a particular field_name.
#
# @param <Symbol> field_name the name of the field you want an error for
def on(field_name)
errors_for_field = errors[field_name.to_sym]
errors_for_field.blank? ? nil : errors_for_field
end
def each
errors.map.each do |k, v|
next if v.blank?
yield(v)
end
end
def empty?
entries.empty?
end
# Return size of errors hash
#
# Allows us to play nicely with Rails' helpers
def count
errors.size
end
def method_missing(meth, *args, &block)
errors.send(meth, *args, &block)
end
private
def errors
@errors ||= {}
end
end # class ValidationErrors
end # module Validation
end # module CouchRest

View File

@ -1,74 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
##
#
# @author Guy van den Berg
class AbsentFieldValidator < GenericValidator
def initialize(field_name, options={})
super
@field_name, @options = field_name, options
end
def call(target)
value = target.send(field_name)
return true if (value.nil? || (value.respond_to?(:empty?) && value.empty?))
error_message = @options[:message] || ValidationErrors.default_error_message(:absent, field_name)
add_error(target, error_message, field_name)
return false
end
end # class AbsentFieldValidator
module ValidatesAbsent
##
#
# @example [Usage]
#
# class Page
#
# property :unwanted_attribute, String
# property :another_unwanted, String
# property :yet_again, String
#
# validates_absent :unwanted_attribute
# validates_absent :another_unwanted, :yet_again
#
# # a call to valid? will return false unless
# # all three attributes are blank
# end
#
def validates_absent(*fields)
opts = opts_from_validator_args(fields)
add_validator_to_context(opts, fields, CouchRest::Validation::AbsentFieldValidator)
end
end # module ValidatesAbsent
end # module Validation
end # module CouchRest

View File

@ -1,107 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
##
#
# @author Guy van den Berg
# @since 0.9
class ConfirmationValidator < GenericValidator
def initialize(field_name, options = {})
super
@options = options
@field_name, @confirm_field_name = field_name, (options[:confirm] || "#{field_name}_confirmation").to_sym
@options[:allow_nil] = true unless @options.has_key?(:allow_nil)
end
def call(target)
unless valid?(target)
error_message = @options[:message] || ValidationErrors.default_error_message(:confirmation, field_name)
add_error(target, error_message, field_name)
return false
end
return true
end
def valid?(target)
field_value = target.send(field_name)
return true if @options[:allow_nil] && field_value.nil?
return false if !@options[:allow_nil] && field_value.nil?
confirm_value = target.instance_variable_get("@#{@confirm_field_name}")
field_value == confirm_value
end
end # class ConfirmationValidator
module ValidatesIsConfirmed
##
# Validates that the given attribute is confirmed by another attribute.
# A common use case scenario is when you require a user to confirm their
# password, for which you use both password and password_confirmation
# attributes.
#
# @option :allow_nil<Boolean> true/false (default is true)
# @option :confirm<Symbol> the attribute that you want to validate
# against (default is firstattr_confirmation)
#
# @example [Usage]
#
# class Page < Hash
# include CouchRest::ExtendedModel
# include CouchRest::Validations
#
# property :password, String
# property :email, String
# attr_accessor :password_confirmation
# attr_accessor :email_repeated
#
# validates_confirmation_of :password
# validates_confirmation_of :email, :confirm => :email_repeated
#
# # a call to valid? will return false unless:
# # password == password_confirmation
# # and
# # email == email_repeated
#
def validates_confirmation_of(*fields)
opts = opts_from_validator_args(fields)
add_validator_to_context(opts, fields, CouchRest::Validation::ConfirmationValidator)
end
def validates_is_confirmed(*fields)
warn "[DEPRECATION] `validates_is_confirmed` is deprecated. Please use `validates_confirmation_of` instead."
validates_confirmation_of(*fields)
end
end # module ValidatesIsConfirmed
end # module Validation
end # module CouchRest

View File

@ -1,122 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require 'pathname'
require Pathname(__FILE__).dirname.expand_path + 'formats/email'
require Pathname(__FILE__).dirname.expand_path + 'formats/url'
module CouchRest
module Validation
##
#
# @author Guy van den Berg
# @since 0.9
class FormatValidator < GenericValidator
FORMATS = {}
include CouchRest::Validation::Format::Email
include CouchRest::Validation::Format::Url
def initialize(field_name, options = {}, &b)
super(field_name, options)
@field_name, @options = field_name, options
@options[:allow_nil] = false unless @options.has_key?(:allow_nil)
end
def call(target)
value = target.validation_property_value(field_name)
return true if @options[:allow_nil] && value.nil?
validation = @options[:as] || @options[:with]
raise "No such predefined format '#{validation}'" if validation.is_a?(Symbol) && !FORMATS.has_key?(validation)
validator = validation.is_a?(Symbol) ? FORMATS[validation][0] : validation
valid = case validator
when Proc then validator.call(value)
when Regexp then value =~ validator
else
raise UnknownValidationFormat, "Can't determine how to validate #{target.class}##{field_name} with #{validator.inspect}"
end
return true if valid
error_message = @options[:message] || ValidationErrors.default_error_message(:invalid, field_name)
field = field_name.to_s.humanize
error_message = error_message.call(field, value) if error_message.respond_to?(:call)
add_error(target, error_message, field_name)
false
end
#class UnknownValidationFormat < StandardError; end
end # class FormatValidator
module ValidatesFormat
##
# Validates that the attribute is in the specified format. You may use the
# :as (or :with, it's an alias) option to specify the pre-defined format
# that you want to validate against. You may also specify your own format
# via a Proc or Regexp passed to the the :as or :with options.
#
# @option :allow_nil<Boolean> true/false (default is true)
# @option :as<Format, Proc, Regexp> the pre-defined format, Proc or Regexp to validate against
# @option :with<Format, Proc, Regexp> an alias for :as
#
# @details [Pre-defined Formats]
# :email_address (format is specified in DataMapper::Validation::Format::Email)
# :url (format is specified in DataMapper::Validation::Format::Url)
#
# @example [Usage]
#
# class Page
#
# property :email, String
# property :zip_code, String
#
# validates_format_of :email, :as => :email_address
# validates_format_of :zip_code, :with => /^\d{5}$/
#
# # a call to valid? will return false unless:
# # email is formatted like an email address
# # and
# # zip_code is a string of 5 digits
#
def validates_format_of(*fields)
opts = opts_from_validator_args(fields)
add_validator_to_context(opts, fields, CouchRest::Validation::FormatValidator)
end
def validates_format(*fields)
warn "[DEPRECATION] `validates_format` is deprecated. Please use `validates_format_of` instead."
validates_format_of(*fields)
end
end # module ValidatesFormat
end # module Validation
end # module CouchRest

View File

@ -1,66 +0,0 @@
# encoding: binary
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
module Format
module Email
def self.included(base)
CouchRest::Validation::FormatValidator::FORMATS.merge!(
:email_address => [ EmailAddress, lambda { |field, value| '%s is not a valid email address'.t(value) }]
)
end
# RFC2822 (No attribution reference available)
EmailAddress = begin
alpha = "a-zA-Z"
digit = "0-9"
atext = "[#{alpha}#{digit}\!\#\$\%\&\'\*+\/\=\?\^\_\`\{\|\}\~\-]"
dot_atom_text = "#{atext}+([.]#{atext}*)*"
dot_atom = "#{dot_atom_text}"
qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
text = "[\\x01-\\x09\\x11\\x12\\x14-\\x7f]"
quoted_pair = "(\\x5c#{text})"
qcontent = "(?:#{qtext}|#{quoted_pair})"
quoted_string = "[\"]#{qcontent}+[\"]"
atom = "#{atext}+"
word = "(?:#{atom}|#{quoted_string})"
obs_local_part = "#{word}([.]#{word})*"
local_part = "(?:#{dot_atom}|#{quoted_string}|#{obs_local_part})"
no_ws_ctl = "\\x01-\\x08\\x11\\x12\\x14-\\x1f\\x7f"
dtext = "[#{no_ws_ctl}\\x21-\\x5a\\x5e-\\x7e]"
dcontent = "(?:#{dtext}|#{quoted_pair})"
domain_literal = "\\[#{dcontent}+\\]"
obs_domain = "#{atom}([.]#{atom})*"
domain = "(?:#{dot_atom}|#{domain_literal}|#{obs_domain})"
addr_spec = "#{local_part}\@#{domain}"
pattern = /^#{addr_spec}$/
end
end # module Email
end # module Format
end # module Validation
end # module CouchRest

View File

@ -1,43 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
module Format
module Url
def self.included(base)
CouchRest::Validation::FormatValidator::FORMATS.merge!(
:url => [ Url, lambda { |field, value| '%s is not a valid URL'.t(value) }]
)
end
Url = begin
# Regex from http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/
/(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix
end
end # module Url
end # module Format
end # module Validation
end # module CouchRest

View File

@ -1,120 +0,0 @@
# -*- coding: utf-8 -*-
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
# All validators extend this base class. Validators must:
#
# * Implement the initialize method to capture its parameters, also calling
# super to have this parent class capture the optional, general :if and
# :unless parameters.
# * Implement the call method, returning true or false. The call method
# provides the validation logic.
#
# @author Guy van den Berg
class GenericValidator
attr_accessor :if_clause, :unless_clause
attr_reader :field_name
# Construct a validator. Capture the :if and :unless clauses when present.
#
# @param field<String, Symbol> The property specified for validation
#
# @option :if<Symbol, Proc> The name of a method or a Proc to call to
# determine if the validation should occur.
# @option :unless<Symbol, Proc> The name of a method or a Proc to call to
# determine if the validation should not occur
# All additional key/value pairs are passed through to the validator
# that is sub-classing this GenericValidator
#
def initialize(field, opts = {})
@if_clause = opts.delete(:if)
@unless_clause = opts.delete(:unless)
end
# Add an error message to a target resource. If the error corresponds to a
# specific field of the resource, add it to that field, otherwise add it
# as a :general message.
#
# @param <Object> target the resource that has the error
# @param <String> message the message to add
# @param <Symbol> field_name the name of the field that caused the error
#
# TODO - should the field_name for a general message be :default???
#
def add_error(target, message, field_name = :general)
target.errors.add(field_name, message)
end
# Call the validator. "call" is used so the operation is BoundMethod and
# Block compatible. This must be implemented in all concrete classes.
#
# @param <Object> target the resource that the validator must be called
# against
# @return <Boolean> true if valid, otherwise false
def call(target)
raise NotImplementedError, "CouchRest::Validation::GenericValidator::call must be overriden in a subclass"
end
# Determines if this validator should be run against the
# target by evaluating the :if and :unless clauses
# optionally passed while specifying any validator.
#
# @param <Object> target the resource that we check against
# @return <Boolean> true if should be run, otherwise false
def execute?(target)
if unless_clause = self.unless_clause
if unless_clause.is_a?(Symbol)
return false if target.send(unless_clause)
elsif unless_clause.respond_to?(:call)
return false if unless_clause.call(target)
end
end
if if_clause = self.if_clause
if if_clause.is_a?(Symbol)
return target.send(if_clause)
elsif if_clause.respond_to?(:call)
return if_clause.call(target)
end
end
true
end
def ==(other)
self.class == other.class &&
self.field_name == other.field_name &&
self.class == other.class &&
self.if_clause == other.if_clause &&
self.unless_clause == other.unless_clause &&
self.instance_variable_get(:@options) == other.instance_variable_get(:@options)
end
end # class GenericValidator
end # module Validation
end # module CouchRest

View File

@ -1,139 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
##
#
# @author Guy van den Berg
# @since 0.9
class LengthValidator < GenericValidator
def initialize(field_name, options)
super
@field_name = field_name
@options = options
@min = options[:minimum] || options[:min]
@max = options[:maximum] || options[:max]
@equal = options[:is] || options[:equals]
@range = options[:within] || options[:in]
@validation_method ||= :range if @range
@validation_method ||= :min if @min && @max.nil?
@validation_method ||= :max if @max && @min.nil?
@validation_method ||= :equals unless @equal.nil?
end
def call(target)
field_value = target.validation_property_value(field_name)
return true if @options[:allow_nil] && field_value.nil?
field_value = '' if field_value.nil?
# XXX: HACK seems hacky to do this on every validation, probably should
# do this elsewhere?
field = field_name.to_s.humanize
min = @range ? @range.min : @min
max = @range ? @range.max : @max
equal = @equal
case @validation_method
when :range then
unless valid = @range.include?(field_value.size)
error_message = ValidationErrors.default_error_message(:length_between, field, min, max)
end
when :min then
unless valid = field_value.size >= min
error_message = ValidationErrors.default_error_message(:too_short, field, min)
end
when :max then
unless valid = field_value.size <= max
error_message = ValidationErrors.default_error_message(:too_long, field, max)
end
when :equals then
unless valid = field_value.size == equal
error_message = ValidationErrors.default_error_message(:wrong_length, field, equal)
end
end
error_message = @options[:message] || error_message
add_error(target, error_message, field_name) unless valid
return valid
end
end # class LengthValidator
module ValidatesLength
# Validates that the length of the attribute is equal to, less than,
# greater than or within a certain range (depending upon the options
# you specify).
#
# @option :allow_nil<Boolean> true/false (default is true)
# @option :minimum ensures that the attribute's length is greater than
# or equal to the supplied value
# @option :min alias for :minimum
# @option :maximum ensures the attribute's length is less than or equal
# to the supplied value
# @option :max alias for :maximum
# @option :equals ensures the attribute's length is equal to the
# supplied value
# @option :is alias for :equals
# @option :in<Range> given a Range, ensures that the attributes length is
# include?'ed in the Range
# @option :within<Range> alias for :in
#
# @example [Usage]
#
# class Page
#
# property high, Integer
# property low, Integer
# property just_right, Integer
#
# validates_length_of :high, :min => 100000000000
# validates_length_of :low, :equals => 0
# validates_length_of :just_right, :within => 1..10
#
# # a call to valid? will return false unless:
# # high is greater than or equal to 100000000000
# # low is equal to 0
# # just_right is between 1 and 10 (inclusive of both 1 and 10)
#
def validates_length_of(*fields)
opts = opts_from_validator_args(fields)
add_validator_to_context(opts, fields, CouchRest::Validation::LengthValidator)
end
def validates_length(*fields)
warn "[DEPRECATION] `validates_length` is deprecated. Please use `validates_length_of` instead."
validates_length_of(*fields)
end
end # module ValidatesLength
end # module Validation
end # module CouchRest

View File

@ -1,89 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
##
#
# @author Guy van den Berg
# @since 0.9
class MethodValidator < GenericValidator
def initialize(field_name, options={})
super
@field_name, @options = field_name, options.clone
@options[:method] = @field_name unless @options.has_key?(:method)
end
def call(target)
result, message = target.send(@options[:method])
add_error(target, message, field_name) unless result
result
end
def ==(other)
@options[:method] == other.instance_variable_get(:@options)[:method] && super
end
end # class MethodValidator
module ValidatesWithMethod
##
# Validate using the given method. The method given needs to return:
# [result::<Boolean>, Error Message::<String>]
#
# @example [Usage]
#
# class Page
#
# property :zip_code, String
#
# validates_with_method :in_the_right_location?
#
# def in_the_right_location?
# if @zip_code == "94301"
# return true
# else
# return [false, "You're in the wrong zip code"]
# end
# end
#
# # A call to valid? will return false and
# # populate the object's errors with "You're in the
# # wrong zip code" unless zip_code == "94301"
#
# # You can also specify field:
#
# validates_with_method :zip_code, :in_the_right_location?
#
# # it will add returned error message to :zip_code field
#
def validates_with_method(*fields)
opts = opts_from_validator_args(fields)
add_validator_to_context(opts, fields, CouchRest::Validation::MethodValidator)
end
end # module ValidatesWithMethod
end # module Validation
end # module CouchRest

View File

@ -1,109 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
##
#
# @author Guy van den Berg
# @since 0.9
class NumericValidator < GenericValidator
def initialize(field_name, options={})
super
@field_name, @options = field_name, options
@options[:integer_only] = false unless @options.has_key?(:integer_only)
end
def call(target)
value = target.send(field_name)
return true if @options[:allow_nil] && value.nil?
value = (defined?(BigDecimal) && value.kind_of?(BigDecimal)) ? value.to_s('F') : value.to_s
error_message = @options[:message]
precision = @options[:precision]
scale = @options[:scale]
if @options[:integer_only]
return true if value =~ /\A[+-]?\d+\z/
error_message ||= ValidationErrors.default_error_message(:not_an_integer, field_name)
else
# FIXME: if precision and scale are not specified, can we assume that it is an integer?
# probably not, as floating point numbers don't have hard
# defined scale. the scale floats with the length of the
# integral and precision. Ie. if precision = 10 and integral
# portion of the number is 9834 (4 digits), the max scale will
# be 6 (10 - 4). But if the integral length is 1, max scale
# will be (10 - 1) = 9, so 1.234567890.
if precision && scale
#handles both Float when it has scale specified and BigDecimal
if precision > scale && scale > 0
return true if value =~ /\A[+-]?(?:\d{1,#{precision - scale}}|\d{0,#{precision - scale}}\.\d{1,#{scale}})\z/
elsif precision > scale && scale == 0
return true if value =~ /\A[+-]?(?:\d{1,#{precision}}(?:\.0)?)\z/
elsif precision == scale
return true if value =~ /\A[+-]?(?:0(?:\.\d{1,#{scale}})?)\z/
else
raise ArgumentError, "Invalid precision #{precision.inspect} and scale #{scale.inspect} for #{field_name} (value: #{value.inspect} #{value.class})"
end
elsif precision && scale.nil?
# for floats, if scale is not set
#total number of digits is less or equal precision
return true if value.gsub(/[^\d]/, '').length <= precision
#number of digits before decimal == precision, and the number is x.0. same as scale = 0
return true if value =~ /\A[+-]?(?:\d{1,#{precision}}(?:\.0)?)\z/
else
return true if value =~ /\A[+-]?(?:\d+|\d*\.\d+)\z/
end
error_message ||= ValidationErrors.default_error_message(:not_a_number, field_name)
end
add_error(target, error_message, field_name)
# TODO: check the gt, gte, lt, lte, and eq options
return false
end
end # class NumericValidator
module ValidatesIsNumber
# Validate whether a field is numeric
#
def validates_numericality_of(*fields)
opts = opts_from_validator_args(fields)
add_validator_to_context(opts, fields, CouchRest::Validation::NumericValidator)
end
def validates_is_number(*fields)
warn "[DEPRECATION] `validates_is_number` is deprecated. Please use `validates_numericality_of` instead."
validates_numericality_of(*fields)
end
end # module ValidatesIsNumber
end # module Validation
end # module CouchRest

View File

@ -1,114 +0,0 @@
# Extracted from dm-validations 0.9.10
#
# Copyright (c) 2007 Guy van den Berg
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module CouchRest
module Validation
##
#
# @author Guy van den Berg
# @since 0.9
class RequiredFieldValidator < GenericValidator
def initialize(field_name, options={})
super
@field_name, @options = field_name, options
end
def call(target)
value = target.validation_property_value(field_name)
property = target.validation_property(field_name.to_s)
return true if present?(value, property)
error_message = @options[:message] || default_error(property)
add_error(target, error_message, field_name)
false
end
protected
# Boolean property types are considered present if non-nil.
# Other property types are considered present if non-blank.
# Non-properties are considered present if non-blank.
def present?(value, property)
boolean_type?(property) ? !value.nil? : !value.blank?
end
def default_error(property)
actual = boolean_type?(property) ? :nil : :blank
ValidationErrors.default_error_message(actual, field_name)
end
# Is +property+ a boolean property?
#
# Returns true for Boolean, ParanoidBoolean, TrueClass, etc. properties.
# Returns false for other property types.
# Returns false for non-properties.
def boolean_type?(property)
property ? property.type == 'Boolean' : false
end
end # class RequiredFieldValidator
module ValidatesPresent
##
# Validates that the specified attribute is present.
#
# For most property types "being present" is the same as being "not
# blank" as determined by the attribute's #blank? method. However, in
# the case of Boolean, "being present" means not nil; i.e. true or
# false.
#
# @note
# dm-core's support lib adds the blank? method to many classes,
# @see lib/dm-core/support/blank.rb (dm-core) for more information.
#
# @example [Usage]
#
# class Page
#
# property :required_attribute, String
# property :another_required, String
# property :yet_again, String
#
# validates_presence_of :required_attribute
# validates_presence_of :another_required, :yet_again
#
# # a call to valid? will return false unless
# # all three attributes are !blank?
# end
def validates_presence_of(*fields)
opts = opts_from_validator_args(fields)
add_validator_to_context(opts, fields, CouchRest::Validation::RequiredFieldValidator)
end
def validates_present(*fields)
warn "[DEPRECATION] `validates_present` is deprecated. Please use `validates_presence_of` instead."
validates_presence_of(*fields)
end
end # module ValidatesPresent
end # module Validation
end # module CouchRest

View File

@ -1,22 +0,0 @@
# require File.join(File.dirname(__FILE__), "couchrest", "extended_document")
gem 'couchrest'
require 'couchrest'
require 'active_support/core_ext'
require 'active_support/json'
require 'mime/types'
require "enumerator"
# Monkey patches applied to couchrest
require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'couchrest')
# Base libraries
require File.join(File.dirname(__FILE__), 'couchrest', 'extended_document')
require File.join(File.dirname(__FILE__), 'couchrest', 'casted_model')
# Add rails support *after* everything has loaded
require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'rails') if defined?(Rails)

56
lib/couchrest_model.rb Normal file
View File

@ -0,0 +1,56 @@
# require File.join(File.dirname(__FILE__), "couchrest", "extended_document")
gem 'couchrest'
require 'couchrest'
require 'active_support/core_ext'
require 'active_support/json'
require 'active_model'
require "active_model/callbacks"
require "active_model/conversion"
require "active_model/deprecated_error_methods"
require "active_model/errors"
require "active_model/naming"
require "active_model/serialization"
require "active_model/translation"
require "active_model/validator"
require "active_model/validations"
require 'mime/types'
require "enumerator"
require "time"
require 'digest/md5'
require 'bigdecimal' # used in typecast
require 'bigdecimal/util' # used in typecast
require 'couchrest/model'
require 'couchrest/model/errors'
require "couchrest/model/persistence"
require "couchrest/model/typecast"
require "couchrest/model/property"
require "couchrest/model/casted_array"
require "couchrest/model/properties"
require "couchrest/model/validations"
require "couchrest/model/callbacks"
require "couchrest/model/document_queries"
require "couchrest/model/views"
require "couchrest/model/design_doc"
require "couchrest/model/extended_attachments"
require "couchrest/model/class_proxy"
require "couchrest/model/collection"
require "couchrest/model/attribute_protection"
require "couchrest/model/attributes"
require "couchrest/model/associations"
# Monkey patches applied to couchrest
require "couchrest/model/support/couchrest"
# Base libraries
require "couchrest/model/casted_model"
require "couchrest/model/base"
# Add rails support *after* everything has loaded
require "couchrest/model/support/rails" if defined?(Rails)

View File

@ -1,21 +1,21 @@
# encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__)
class Client < CouchRest::ExtendedDocument
class Client < CouchRest::Model::Base
use_database DB
property :name
property :tax_code
end
class SaleEntry < CouchRest::ExtendedDocument
class SaleEntry < CouchRest::Model::Base
use_database DB
property :description
property :price
end
class SaleInvoice < CouchRest::ExtendedDocument
class SaleInvoice < CouchRest::Model::Base
use_database DB
belongs_to :client
@ -64,7 +64,7 @@ describe "Assocations" do
it "should raise error if class name does not exist" do
lambda {
class TestBadAssoc < CouchRest::ExtendedDocument
class TestBadAssoc < CouchRest::Model::Base
belongs_to :test_bad_item
end
}.should raise_error
@ -99,7 +99,7 @@ describe "Assocations" do
it "should create an associated property and collection proxy" do
@invoice.respond_to?('entry_ids')
@invoice.respond_to?('entry_ids=')
@invoice.entries.class.should eql(::CouchRest::CollectionProxy)
@invoice.entries.class.should eql(::CouchRest::CollectionOfProxy)
end
it "should allow replacement of objects" do

View File

@ -1,12 +1,12 @@
require File.expand_path('../../spec_helper', __FILE__)
describe "ExtendedDocument attachments" do
describe "Model attachments" do
describe "#has_attachment?" do
before(:each) do
reset_test_db!
@obj = Basic.new
@obj.save.should == true
@obj.save.should be_true
@file = File.open(FIXTURE_PATH + '/attachments/test.html')
@attachment_name = 'my_attachment'
@obj.create_attachment(:file => @file, :name => @attachment_name)
@ -35,7 +35,7 @@ describe "ExtendedDocument attachments" do
describe "creating an attachment" do
before(:each) do
@obj = Basic.new
@obj.save.should == true
@obj.save.should be_true
@file_ext = File.open(FIXTURE_PATH + '/attachments/test.html')
@file_no_ext = File.open(FIXTURE_PATH + '/attachments/README')
@attachment_name = 'my_attachment'
@ -44,14 +44,14 @@ describe "ExtendedDocument attachments" do
it "should create an attachment from file with an extension" do
@obj.create_attachment(:file => @file_ext, :name => @attachment_name)
@obj.save.should == true
@obj.save.should be_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
@obj.save.should be_true
reloaded_obj = Basic.get(@obj.id)
reloaded_obj['_attachments'][@attachment_name].should_not be_nil
end
@ -89,7 +89,7 @@ describe "ExtendedDocument attachments" do
@file = File.open(FIXTURE_PATH + '/attachments/test.html')
@attachment_name = 'my_attachment'
@obj.create_attachment(:file => @file, :name => @attachment_name)
@obj.save.should == true
@obj.save.should be_true
@file.rewind
@content_type = 'media/mp3'
end
@ -129,7 +129,7 @@ describe "ExtendedDocument attachments" do
@file = File.open(FIXTURE_PATH + '/attachments/test.html')
@attachment_name = 'my_attachment'
@obj.create_attachment(:file => @file, :name => @attachment_name)
@obj.save.should == true
@obj.save.should be_true
end
it 'should return nil if attachment does not exist' do

View File

@ -1,150 +1,153 @@
require File.expand_path("../../spec_helper", __FILE__)
describe "ExtendedDocument", "no declarations" do
class NoProtection < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :name
property :phone
end
describe "Model Attributes" do
it "should not protect anything through new" do
user = NoProtection.new(:name => "will", :phone => "555-5555")
describe "no declarations" do
class NoProtection < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name
property :phone
end
user.name.should == "will"
user.phone.should == "555-5555"
end
it "should not protect anything through new" do
user = NoProtection.new(:name => "will", :phone => "555-5555")
it "should not protect anything through attributes=" do
user = NoProtection.new
user.attributes = {:name => "will", :phone => "555-5555"}
user.name.should == "will"
user.phone.should == "555-5555"
end
user.name.should == "will"
user.phone.should == "555-5555"
end
it "should recreate from the database properly" do
user = NoProtection.new
user.name = "will"
user.phone = "555-5555"
user.save!
it "should not protect anything through attributes=" do
user = NoProtection.new
user.attributes = {:name => "will", :phone => "555-5555"}
user.name.should == "will"
user.phone.should == "555-5555"
end
user = NoProtection.get(user.id)
user.name.should == "will"
user.phone.should == "555-5555"
end
end
describe "ExtendedDocument", "accessible flag" do
class WithAccessible < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :name, :accessible => true
property :admin, :default => false
end
it "should recognize accessible properties" do
props = WithAccessible.accessible_properties.map { |prop| prop.name}
props.should include("name")
props.should_not include("admin")
end
it "should protect non-accessible properties set through new" do
user = WithAccessible.new(:name => "will", :admin => true)
user.name.should == "will"
user.admin.should == false
end
it "should protect non-accessible properties set through attributes=" do
user = WithAccessible.new
user.attributes = {:name => "will", :admin => true}
user.name.should == "will"
user.admin.should == false
end
end
describe "ExtendedDocument", "protected flag" do
class WithProtected < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :name
property :admin, :default => false, :protected => true
end
it "should recognize protected properties" do
props = WithProtected.protected_properties.map { |prop| prop.name}
props.should_not include("name")
props.should include("admin")
end
it "should protect non-accessible properties set through new" do
user = WithProtected.new(:name => "will", :admin => true)
user.name.should == "will"
user.admin.should == false
end
it "should protect non-accessible properties set through attributes=" do
user = WithProtected.new
user.attributes = {:name => "will", :admin => true}
user.name.should == "will"
user.admin.should == false
end
end
describe "ExtendedDocument", "protected flag" do
class WithBoth < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :name, :accessible => true
property :admin, :default => false, :protected => true
end
it "should raise an error when both are set" do
lambda { WithBoth.new }.should raise_error
end
end
describe "ExtendedDocument", "from database" do
class WithProtected < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :name
property :admin, :default => false, :protected => true
view_by :name
end
before(:each) do
@user = WithProtected.new
@user.name = "will"
@user.admin = true
@user.save!
end
def verify_attrs(user)
user.name.should == "will"
user.admin.should == true
end
it "ExtendedDocument#get should not strip protected attributes" do
reloaded = WithProtected.get( @user.id )
verify_attrs reloaded
end
it "ExtendedDocument#get! should not strip protected attributes" do
reloaded = WithProtected.get!( @user.id )
verify_attrs reloaded
end
it "ExtendedDocument#all should not strip protected attributes" do
# all creates a CollectionProxy
docs = WithProtected.all(:key => @user.id)
docs.size.should == 1
reloaded = docs.first
verify_attrs reloaded
end
it "views should not strip protected attributes" do
docs = WithProtected.by_name(:startkey => "will", :endkey => "will")
reloaded = docs.first
verify_attrs reloaded
it "should recreate from the database properly" do
user = NoProtection.new
user.name = "will"
user.phone = "555-5555"
user.save!
user = NoProtection.get(user.id)
user.name.should == "will"
user.phone.should == "555-5555"
end
end
describe "Model Base", "accessible flag" do
class WithAccessible < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name, :accessible => true
property :admin, :default => false
end
it "should recognize accessible properties" do
props = WithAccessible.accessible_properties.map { |prop| prop.name}
props.should include("name")
props.should_not include("admin")
end
it "should protect non-accessible properties set through new" do
user = WithAccessible.new(:name => "will", :admin => true)
user.name.should == "will"
user.admin.should == false
end
it "should protect non-accessible properties set through attributes=" do
user = WithAccessible.new
user.attributes = {:name => "will", :admin => true}
user.name.should == "will"
user.admin.should == false
end
end
describe "Model Base", "protected flag" do
class WithProtected < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name
property :admin, :default => false, :protected => true
end
it "should recognize protected properties" do
props = WithProtected.protected_properties.map { |prop| prop.name}
props.should_not include("name")
props.should include("admin")
end
it "should protect non-accessible properties set through new" do
user = WithProtected.new(:name => "will", :admin => true)
user.name.should == "will"
user.admin.should == false
end
it "should protect non-accessible properties set through attributes=" do
user = WithProtected.new
user.attributes = {:name => "will", :admin => true}
user.name.should == "will"
user.admin.should == false
end
end
describe "protected flag" do
class WithBoth < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name, :accessible => true
property :admin, :default => false, :protected => true
end
it "should raise an error when both are set" do
lambda { WithBoth.new }.should raise_error
end
end
describe "from database" do
class WithProtected < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name
property :admin, :default => false, :protected => true
view_by :name
end
before(:each) do
@user = WithProtected.new
@user.name = "will"
@user.admin = true
@user.save!
end
def verify_attrs(user)
user.name.should == "will"
user.admin.should == true
end
it "Base#get should not strip protected attributes" do
reloaded = WithProtected.get( @user.id )
verify_attrs reloaded
end
it "Base#get! should not strip protected attributes" do
reloaded = WithProtected.get!( @user.id )
verify_attrs reloaded
end
it "Base#all should not strip protected attributes" do
# all creates a CollectionProxy
docs = WithProtected.all(:key => @user.id)
docs.size.should == 1
reloaded = docs.first
verify_attrs reloaded
end
it "views should not strip protected attributes" do
docs = WithProtected.by_name(:startkey => "will", :endkey => "will")
reloaded = docs.first
verify_attrs reloaded
end
end
end

403
spec/couchrest/base_spec.rb Normal file
View File

@ -0,0 +1,403 @@
# encoding: utf-8
require File.expand_path("../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'base')
describe "Model Base" do
before(:each) do
@obj = WithDefaultValues.new
end
describe "instance database connection" do
it "should use the default database" do
@obj.database.name.should == 'couchrest-model-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 document" do
@obj = Basic.new
@obj.rev.should be_nil
@obj.should be_new
@obj.should be_new_document
@obj.should be_new_record
end
it "should not failed on a nil value in argument" do
@obj = Basic.new(nil)
@obj.should == { 'couchrest-type' => 'Basic' }
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 silently ignore _id" do
@art.update_attributes_without_saving('_id' => 'foobar')
@art['_id'].should_not == 'foobar'
end
it "should silently ignore _rev" do
@art.update_attributes_without_saving('_rev' => 'foobar')
@art['_rev'].should_not == 'foobar'
end
it "should silently ignore created_at" do
@art.update_attributes_without_saving('created_at' => 'foobar')
@art['created_at'].should_not == 'foobar'
end
it "should silently ignore updated_at" do
@art.update_attributes_without_saving('updated_at' => 'foobar')
@art['updated_at'].should_not == 'foobar'
end
it "should also work using attributes= alias" do
@art.respond_to?(:attributes=).should be_true
@art.attributes = {'date' => Time.now, :title => "something else"}
@art['title'].should == "something else"
end
it "should not flip out if an attribute= method is missing and ignore it" do
lambda {
@art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
}.should_not raise_error
@art.slug.should == "big-bad-danger"
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}
end
it "should have the default false value explicitly assigned" do
@obj.default_false.should == false
end
it "should automatically call a proc default at initialization" do
@obj.set_by_proc.should be_an_instance_of(Time)
@obj.set_by_proc.should == @obj.set_by_proc
@obj.set_by_proc.should < Time.now
end
it "should let you overwrite the default values" do
obj = WithDefaultValues.new(:preset => 'test')
obj.preset = 'test'
end
it "should work with a default empty array" do
obj = WithDefaultValues.new(:tags => ['spec'])
obj.tags.should == ['spec']
end
it "should set default value of read-only property" do
obj = WithDefaultValues.new
obj.read_only_with_default.should == 'generic'
end
end
describe "simplified way of setting property types" do
it "should set defaults" do
obj = WithSimplePropertyType.new
obj.preset.should eql('none')
end
it "should handle arrays" do
obj = WithSimplePropertyType.new(:tags => ['spec'])
obj.tags.should == ['spec']
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 "finding all instances of a model" do
before(:all) do
WithTemplateAndUniqueID.req_design_doc_refresh
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 "counting all instances of a model" do
before(:each) do
@db = reset_test_db!
WithTemplateAndUniqueID.req_design_doc_refresh
end
it ".count should return 0 if there are no docuemtns" do
WithTemplateAndUniqueID.count.should == 0
end
it ".count should return the number of documents" do
WithTemplateAndUniqueID.new('important-field' => '1').save
WithTemplateAndUniqueID.new('important-field' => '2').save
WithTemplateAndUniqueID.new('important-field' => '3').save
WithTemplateAndUniqueID.count.should == 3
end
end
describe "finding the first instance of a model" do
before(:each) do
@db = reset_test_db!
# WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically
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 "lazily refreshing the design document" do
before(:all) do
@db = reset_test_db!
WithTemplateAndUniqueID.new('important-field' => '1').save
end
it "should not save the design doc twice" do
WithTemplateAndUniqueID.all
WithTemplateAndUniqueID.req_design_doc_refresh
WithTemplateAndUniqueID.refresh_design_doc
rev = WithTemplateAndUniqueID.design_doc['_rev']
WithTemplateAndUniqueID.req_design_doc_refresh
WithTemplateAndUniqueID.refresh_design_doc
WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev)
end
end
describe "getting a model with a subobject field" do
before(:all) do
course_doc = {
"title" => "Metaphysics 410",
"professor" => {
"name" => ["Mark", "Hinchliff"]
},
"ends_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 ends_at as a Time" do
@course['ends_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)
obj.should be_an_instance_of(WithDefaultValues)
obj.created_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
end
it "should not change created_at on update" do
2.times do
lambda do
@art.save
end.should_not change(@art, :created_at)
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
describe "getter and setter methods" do
it "should try to call the arg= method before setting :arg in the hash" do
@doc = WithGetterAndSetterMethods.new(:arg => "foo")
@doc['arg'].should be_nil
@doc[:arg].should be_nil
@doc.other_arg.should == "foo-foo"
end
end
describe "initialization" do
it "should call after_initialize method if available" do
@doc = WithAfterInitializeMethod.new
@doc['some_value'].should eql('value')
end
end
describe "recursive validation on a model" do
before :each do
reset_test_db!
@cat = Cat.new(:name => 'Sockington')
end
it "should not save if a nested casted model is invalid" do
@cat.favorite_toy = CatToy.new
@cat.should_not be_valid
@cat.save.should be_false
lambda{@cat.save!}.should raise_error
end
it "should save when nested casted model is valid" do
@cat.favorite_toy = CatToy.new(:name => 'Squeaky')
@cat.should be_valid
@cat.save.should be_true
lambda{@cat.save!}.should_not raise_error
end
it "should not save when nested collection contains an invalid casted model" do
@cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new]
@cat.should_not be_valid
@cat.save.should be_false
lambda{@cat.save!}.should raise_error
end
it "should save when nested collection contains valid casted models" do
@cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')]
@cat.should be_valid
@cat.save.should be_true
lambda{@cat.save!}.should_not raise_error
end
it "should not fail if the nested casted model doesn't have validation" do
Cat.property :trainer, Person
Cat.validates_presence_of :name
cat = Cat.new(:name => 'Mr Bigglesworth')
cat.trainer = Person.new
cat.should be_valid
cat.save.should be_true
end
end
describe "searching the contents of an extended document" do
before :each do
@db = reset_test_db!
names = ["Fuzzy", "Whiskers", "Mr Bigglesworth", "Sockington", "Smitty", "Sammy", "Samson", "Simon"]
names.each { |name| Cat.create(:name => name) }
search_function = { 'defaults' => {'store' => 'no', 'index' => 'analyzed_no_norms'},
'index' => "function(doc) { ret = new Document(); ret.add(doc['name'], {'field':'name'}); return ret; }" }
@db.save_doc({'_id' => '_design/search', 'fulltext' => {'cats' => search_function}})
end
it "should be able to paginate through a large set of search results" do
if couchdb_lucene_available?
names = []
Cat.paginated_each(:design_doc => "search", :view_name => "cats",
:q => 'name:S*', :search => true, :include_docs => true, :per_page => 3) do |cat|
cat.should_not be_nil
names << cat.name
end
names.size.should == 5
names.should include('Sockington')
names.should include('Smitty')
names.should include('Sammy')
names.should include('Samson')
names.should include('Simon')
end
end
end
end

View File

@ -1,39 +1,33 @@
# encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__)
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'question')
require File.join(FIXTURE_PATH, 'more', 'course')
class WithCastedModelMixin < Hash
include CouchRest::CastedModel
include CouchRest::Model::CastedModel
property :name
property :no_value
property :details, :type => 'Object', :default => {}
property :casted_attribute, :cast_as => 'WithCastedModelMixin'
property :details, Object, :default => {}
property :casted_attribute, WithCastedModelMixin
end
class DummyModel < CouchRest::ExtendedDocument
class DummyModel < CouchRest::Model::Base
use_database TEST_SERVER.default_database
raise "Default DB not set" if TEST_SERVER.default_database.nil?
property :casted_attribute, WithCastedModelMixin
property :keywords, ["String"]
property :keywords, [String]
property :sub_models do |child|
child.property :title
end
end
class CastedCallbackDoc < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
raise "Default DB not set" if TEST_SERVER.default_database.nil?
property :callback_model, :cast_as => 'WithCastedCallBackModel'
end
class WithCastedCallBackModel < Hash
include CouchRest::CastedModel
include CouchRest::Validation
include CouchRest::Model::CastedModel
property :name
property :run_before_validate
property :run_after_validate
@ -46,7 +40,13 @@ class WithCastedCallBackModel < Hash
end
end
describe CouchRest::CastedModel do
class CastedCallbackDoc < CouchRest::Model::Base
use_database TEST_SERVER.default_database
raise "Default DB not set" if TEST_SERVER.default_database.nil?
property :callback_model, WithCastedCallBackModel
end
describe CouchRest::Model::CastedModel do
describe "A non hash class including CastedModel" do
it "should fail raising and include error" do
@ -227,7 +227,7 @@ describe CouchRest::CastedModel do
@cat.toys.push(toy)
@cat.save.should be_true
@cat = Cat.get @cat.id
@cat.toys.class.should == CouchRest::CastedArray
@cat.toys.class.should == CouchRest::Model::CastedArray
@cat.toys.first.class.should == CatToy
@cat.toys.first.should === toy
end
@ -240,7 +240,7 @@ describe CouchRest::CastedModel do
end
it "should not fail if the casted model doesn't have validation" do
Cat.property :masters, :cast_as => ['Person'], :default => []
Cat.property :masters, [Person], :default => []
Cat.validates_presence_of :name
cat = Cat.new(:name => 'kitty')
cat.should be_valid

View File

@ -3,7 +3,7 @@ require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'card')
class Driver < CouchRest::ExtendedDocument
class Driver < CouchRest::Model::Base
use_database TEST_SERVER.default_database
# You have to add a casted_by accessor if you want to reach a casted extended doc parent
attr_accessor :casted_by
@ -11,12 +11,11 @@ class Driver < CouchRest::ExtendedDocument
property :name
end
class Car < CouchRest::ExtendedDocument
class Car < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name
property :driver, :cast_as => 'Driver'
property :backseat_driver, :cast_as => Driver
property :driver, Driver
end
describe "casting an extended document" do
@ -24,17 +23,14 @@ describe "casting an extended document" do
before(:each) do
@driver = Driver.new(:name => 'Matt')
@car = Car.new(:name => 'Renault 306', :driver => @driver)
@car2 = Car.new(:name => 'Renault 306', :backseat_driver => @driver.dup)
end
it "should retain all properties of the casted attribute" do
@car.driver.should == @driver
@car2.backseat_driver.should == @driver
end
it "should let the casted document know who casted it" do
@car.driver.casted_by.should == @car
@car2.backseat_driver.casted_by.should == @car2
end
end
@ -61,7 +57,7 @@ describe "assigning a value to casted attribute after initializing an object" do
end
describe "casting an extended document from parsed JSON" do
describe "casting a model from parsed JSON" do
before(:each) do
@driver = Driver.new(:name => 'Matt')

View File

@ -1,6 +1,6 @@
require File.expand_path("../../spec_helper", __FILE__)
class UnattachedDoc < CouchRest::ExtendedDocument
class UnattachedDoc < CouchRest::Model::Base
# Note: no use_database here
property :title
property :questions

View File

@ -1,869 +0,0 @@
# encoding: utf-8
require File.expand_path("../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'cat')
describe "ExtendedDocument" do
class WithDefaultValues < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :preset, :type => 'Object', :default => {:right => 10, :top_align => false}
property :set_by_proc, :default => Proc.new{Time.now}, :cast_as => 'Time'
property :tags, :type => ['String'], :default => []
property :read_only_with_default, :default => 'generic', :read_only => true
property :default_false, :type => 'Boolean', :default => false
property :name
timestamps!
end
class WithSimplePropertyType < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :name, String
property :preset, String, :default => 'none'
property :tags, [String]
timestamps!
end
class WithCallBacks < CouchRest::ExtendedDocument
include ::CouchRest::Validation
use_database TEST_SERVER.default_database
property :name
property :run_before_validate
property :run_after_validate
property :run_before_save
property :run_after_save
property :run_before_create
property :run_after_create
property :run_before_update
property :run_after_update
before_validate do |object|
object.run_before_validate = true
end
after_validate do |object|
object.run_after_validate = true
end
before_save do |object|
object.run_before_save = true
end
after_save do |object|
object.run_after_save = true
end
before_create do |object|
object.run_before_create = true
end
after_create do |object|
object.run_after_create = true
end
before_update do |object|
object.run_before_update = true
end
after_update do |object|
object.run_after_update = true
end
property :run_one
property :run_two
property :run_three
before_save :run_one_method, :run_two_method do |object|
object.run_three = true
end
def run_one_method
self.run_one = true
end
def run_two_method
self.run_two = true
end
attr_accessor :run_it
property :conditional_one
property :conditional_two
before_save :conditional_one_method, :conditional_two_method, :if => proc { self.run_it }
def conditional_one_method
self.conditional_one = true
end
def conditional_two_method
self.conditional_two = true
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
class WithGetterAndSetterMethods < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :other_arg
def arg
other_arg
end
def arg=(value)
self.other_arg = "foo-#{value}"
end
end
class WithAfterInitializeMethod < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :some_value
def after_initialize
self.some_value ||= "value"
end
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 document" do
@obj = Basic.new
@obj.rev.should be_nil
@obj.should be_new
@obj.should be_new_document
@obj.should be_new_record
end
it "should not failed on a nil value in argument" do
@obj = Basic.new(nil)
@obj.should == { 'couchrest-type' => 'Basic' }
end
end
describe "creating a new document" do
it "should instantialize and save a document" do
article = Article.create(:title => 'my test')
article.title.should == 'my test'
article.should_not be_new
end
it "should trigger the create callbacks" do
doc = WithCallBacks.create(:name => 'my other test')
doc.run_before_create.should be_true
doc.run_after_create.should be_true
doc.run_before_save.should be_true
doc.run_after_save.should be_true
end
end
describe "creating a new document from database" do
it "should instantialize" do
doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'couchrest-type' => 'Article', 'name' => 'my test'})
doc.class.should eql(Article)
end
it "should instantialize of same class if no couchrest-type included from DB" do
doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'name' => 'my test'})
doc.class.should eql(Article)
end
it "should instantialize document of different type" do
doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, 'couchrest-type' => 'WithCallBacks', 'name' => 'my test'})
doc.class.should eql(WithCallBacks)
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 silently ignore _id" do
@art.update_attributes_without_saving('_id' => 'foobar')
@art['_id'].should_not == 'foobar'
end
it "should silently ignore _rev" do
@art.update_attributes_without_saving('_rev' => 'foobar')
@art['_rev'].should_not == 'foobar'
end
it "should silently ignore created_at" do
@art.update_attributes_without_saving('created_at' => 'foobar')
@art['created_at'].should_not == 'foobar'
end
it "should silently ignore updated_at" do
@art.update_attributes_without_saving('updated_at' => 'foobar')
@art['updated_at'].should_not == 'foobar'
end
it "should also work using attributes= alias" do
@art.respond_to?(:attributes=).should be_true
@art.attributes = {'date' => Time.now, :title => "something else"}
@art['title'].should == "something else"
end
it "should not flip out if an attribute= method is missing and ignore it" do
lambda {
@art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
}.should_not raise_error
@art.slug.should == "big-bad-danger"
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}
end
it "should have the default false value explicitly assigned" do
@obj.default_false.should == false
end
it "should automatically call a proc default at initialization" do
@obj.set_by_proc.should be_an_instance_of(Time)
@obj.set_by_proc.should == @obj.set_by_proc
@obj.set_by_proc.should < Time.now
end
it "should let you overwrite the default values" do
obj = WithDefaultValues.new(:preset => 'test')
obj.preset = 'test'
end
it "should work with a default empty array" do
obj = WithDefaultValues.new(:tags => ['spec'])
obj.tags.should == ['spec']
end
it "should set default value of read-only property" do
obj = WithDefaultValues.new
obj.read_only_with_default.should == 'generic'
end
end
describe "simplified way of setting property types" do
it "should set defaults" do
obj = WithSimplePropertyType.new
obj.preset.should eql('none')
end
it "should handle arrays" do
obj = WithSimplePropertyType.new(:tags => ['spec'])
obj.tags.should == ['spec']
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
it "should load and instantiate with find" do
foundart = Article.find @art.id
foundart.title.should == "All About Getting"
end
it "should return nil if `get` is used and the document doesn't exist" do
foundart = Article.get 'matt aimonetti'
foundart.should be_nil
end
it "should raise an error if `get!` is used and the document doesn't exist" do
lambda{foundart = Article.get!('matt aimonetti')}.should raise_error
end
it "should raise an error if `find!` is used and the document doesn't exist" do
lambda{foundart = Article.find!('matt aimonetti')}.should raise_error
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.req_design_doc_refresh
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 "counting all instances of a model" do
before(:each) do
@db = reset_test_db!
WithTemplateAndUniqueID.req_design_doc_refresh
end
it ".count should return 0 if there are no docuemtns" do
WithTemplateAndUniqueID.count.should == 0
end
it ".count should return the number of documents" do
WithTemplateAndUniqueID.new('important-field' => '1').save
WithTemplateAndUniqueID.new('important-field' => '2').save
WithTemplateAndUniqueID.new('important-field' => '3').save
WithTemplateAndUniqueID.count.should == 3
end
end
describe "finding the first instance of a model" do
before(:each) do
@db = reset_test_db!
# WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically
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 "lazily refreshing the design document" do
before(:all) do
@db = reset_test_db!
WithTemplateAndUniqueID.new('important-field' => '1').save
end
it "should not save the design doc twice" do
WithTemplateAndUniqueID.all
WithTemplateAndUniqueID.req_design_doc_refresh
WithTemplateAndUniqueID.refresh_design_doc
rev = WithTemplateAndUniqueID.design_doc['_rev']
WithTemplateAndUniqueID.req_design_doc_refresh
WithTemplateAndUniqueID.refresh_design_doc
WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev)
end
end
describe "getting a model with a subobject field" do
before(:all) do
course_doc = {
"title" => "Metaphysics 410",
"professor" => {
"name" => ["Mark", "Hinchliff"]
},
"ends_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 ends_at as a Time" do
@course['ends_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)
obj.should be_an_instance_of(WithDefaultValues)
obj.created_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
end
it "should not change created_at on update" do
2.times do
lambda do
@art.save
end.should_not change(@art, :created_at)
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
describe "basic saving and retrieving" do
it "should work fine" do
@obj.name = "should be easily saved and retrieved"
@obj.save
saved_obj = WithDefaultValues.get(@obj.id)
saved_obj.should_not be_nil
end
it "should parse the Time attributes automatically" do
@obj.name = "should parse the Time attributes automatically"
@obj.set_by_proc.should be_an_instance_of(Time)
@obj.save
@obj.set_by_proc.should be_an_instance_of(Time)
saved_obj = WithDefaultValues.get(@obj.id)
saved_obj.set_by_proc.should be_an_instance_of(Time)
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
describe "save!" do
before(:each) do
@sobj = Card.new(:first_name => "Marcos", :last_name => "Tapajós")
end
it "should return true if save the document" do
@sobj.save!.should == true
end
it "should raise error if don't save the document" do
@sobj.first_name = nil
lambda { @sobj.save!.should == true }.should raise_error(RuntimeError)
end
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_new
@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
@dobj.destroy
@dobj.rev.should be_nil
@dobj.id.should be_nil
@dobj.save.should == true
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
@doc = WithCallBacks.new
end
describe "validate" do
it "should run before_validate before validating" do
@doc.run_before_validate.should be_nil
@doc.should be_valid
@doc.run_before_validate.should be_true
end
it "should run after_validate after validating" do
@doc.run_after_validate.should be_nil
@doc.should be_valid
@doc.run_after_validate.should be_true
end
end
describe "save" do
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 run the grouped callbacks before saving" do
@doc.run_one.should be_nil
@doc.run_two.should be_nil
@doc.run_three.should be_nil
@doc.save.should be_true
@doc.run_one.should be_true
@doc.run_two.should be_true
@doc.run_three.should be_true
end
it "should not run conditional callbacks" do
@doc.run_it = false
@doc.save.should be_true
@doc.conditional_one.should be_nil
@doc.conditional_two.should be_nil
end
it "should run conditional callbacks" do
@doc.run_it = true
@doc.save.should be_true
@doc.conditional_one.should be_true
@doc.conditional_two.should be_true
end
end
describe "create" do
it "should run the before save filter when creating" do
@doc.run_before_save.should be_nil
@doc.create.should_not be_nil
@doc.run_before_save.should be_true
end
it "should run the before create filter" do
@doc.run_before_create.should be_nil
@doc.create.should_not be_nil
@doc.create
@doc.run_before_create.should be_true
end
it "should run the after create filter" do
@doc.run_after_create.should be_nil
@doc.create.should_not be_nil
@doc.create
@doc.run_after_create.should be_true
end
end
describe "update" do
before(:each) do
@doc.save
end
it "should run the before update filter when updating an existing document" do
@doc.run_before_update.should be_nil
@doc.update
@doc.run_before_update.should be_true
end
it "should run the after update filter when updating an existing document" do
@doc.run_after_update.should be_nil
@doc.update
@doc.run_after_update.should be_true
end
it "should run the before update filter when saving an existing document" do
@doc.run_before_update.should be_nil
@doc.save
@doc.run_before_update.should be_true
end
end
end
describe "getter and setter methods" do
it "should try to call the arg= method before setting :arg in the hash" do
@doc = WithGetterAndSetterMethods.new(:arg => "foo")
@doc['arg'].should be_nil
@doc[:arg].should be_nil
@doc.other_arg.should == "foo-foo"
end
end
describe "initialization" do
it "should call after_initialize method if available" do
@doc = WithAfterInitializeMethod.new
@doc['some_value'].should eql('value')
end
end
describe "recursive validation on an extended document" do
before :each do
reset_test_db!
@cat = Cat.new(:name => 'Sockington')
end
it "should not save if a nested casted model is invalid" do
@cat.favorite_toy = CatToy.new
@cat.should_not be_valid
@cat.save.should be_false
lambda{@cat.save!}.should raise_error
end
it "should save when nested casted model is valid" do
@cat.favorite_toy = CatToy.new(:name => 'Squeaky')
@cat.should be_valid
@cat.save.should be_true
lambda{@cat.save!}.should_not raise_error
end
it "should not save when nested collection contains an invalid casted model" do
@cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new]
@cat.should_not be_valid
@cat.save.should be_false
lambda{@cat.save!}.should raise_error
end
it "should save when nested collection contains valid casted models" do
@cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')]
@cat.should be_valid
@cat.save.should be_true
lambda{@cat.save!}.should_not raise_error
end
it "should not fail if the nested casted model doesn't have validation" do
Cat.property :trainer, :cast_as => 'Person'
Cat.validates_presence_of :name
cat = Cat.new(:name => 'Mr Bigglesworth')
cat.trainer = Person.new
cat.trainer.validatable?.should be_false
cat.should be_valid
cat.save.should be_true
end
end
describe "searching the contents of an extended document" do
before :each do
@db = reset_test_db!
names = ["Fuzzy", "Whiskers", "Mr Bigglesworth", "Sockington", "Smitty", "Sammy", "Samson", "Simon"]
names.each { |name| Cat.create(:name => name) }
search_function = { 'defaults' => {'store' => 'no', 'index' => 'analyzed_no_norms'},
'index' => "function(doc) { ret = new Document(); ret.add(doc['name'], {'field':'name'}); return ret; }" }
@db.save_doc({'_id' => '_design/search', 'fulltext' => {'cats' => search_function}})
end
it "should be able to paginate through a large set of search results" do
if couchdb_lucene_available?
names = []
Cat.paginated_each(:design_doc => "search", :view_name => "cats",
:q => 'name:S*', :search => true, :include_docs => true, :per_page => 3) do |cat|
cat.should_not be_nil
names << cat.name
end
names.size.should == 5
names.should include('Sockington')
names.should include('Smitty')
names.should include('Sammy')
names.should include('Samson')
names.should include('Simon')
end
end
end
end

View File

@ -13,7 +13,7 @@ begin
class PlainChild < PlainParent
end
class ExtendedParent < CouchRest::ExtendedDocument
class ExtendedParent < CouchRest::Model::Base
class_inheritable_accessor :foo
self.foo = :bar
end

View File

@ -0,0 +1,409 @@
# encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__)
require File.join(FIXTURE_PATH, 'base')
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'card')
describe "Model Persistence" do
before(:each) do
@obj = WithDefaultValues.new
end
describe "creating a new document from database" do
it "should instantialize" do
doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'couchrest-type' => 'Article', 'name' => 'my test'})
doc.class.should eql(Article)
end
it "should instantialize of same class if no couchrest-type included from DB" do
doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'name' => 'my test'})
doc.class.should eql(Article)
end
it "should instantialize document of different type" do
doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, 'couchrest-type' => 'WithTemplateAndUniqueID', 'name' => 'my test'})
doc.class.should eql(WithTemplateAndUniqueID)
end
end
describe "basic saving and retrieving" do
it "should work fine" do
@obj.name = "should be easily saved and retrieved"
@obj.save
saved_obj = WithDefaultValues.get(@obj.id)
saved_obj.should_not be_nil
end
it "should parse the Time attributes automatically" do
@obj.name = "should parse the Time attributes automatically"
@obj.set_by_proc.should be_an_instance_of(Time)
@obj.save
@obj.set_by_proc.should be_an_instance_of(Time)
saved_obj = WithDefaultValues.get(@obj.id)
saved_obj.set_by_proc.should be_an_instance_of(Time)
end
end
describe "creating a model" do
before(:each) do
@sobj = Basic.new
end
it "should accept true or false on save for validation" do
@sobj.should_receive(:valid?)
@sobj.save(true)
end
it "should accept hash with validation option" do
@sobj.should_receive(:valid?)
@sobj.save(:validate => true)
end
it "should not call validation when option is false" do
@sobj.should_not_receive(:valid?)
@sobj.save(false)
end
it "should not call validation when option :validate is false" do
@sobj.should_not_receive(:valid?)
@sobj.save(:validate => false)
end
it "should instantialize and save a document" do
article = Article.create(:title => 'my test')
article.title.should == 'my test'
article.should_not be_new
end
it "should trigger the create callbacks" do
doc = WithCallBacks.create(:name => 'my other test')
doc.run_before_create.should be_true
doc.run_after_create.should be_true
doc.run_before_save.should be_true
doc.run_after_save.should be_true
end
end
describe "saving a model" do
before(:all) do
@sobj = Basic.new
@sobj.save.should be_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
it "should accept true or false on save for validation" do
@sobj.should_receive(:valid?)
@sobj.save(true)
end
it "should accept hash with validation option" do
@sobj.should_receive(:valid?)
@sobj.save(:validate => true)
end
it "should not call validation when option is false" do
@sobj.should_not_receive(:valid?)
@sobj.save(false)
end
it "should not call validation when option :validate is false" do
@sobj.should_not_receive(:valid?)
@sobj.save(:validate => false)
end
describe "save!" do
before(:each) do
@sobj = Card.new(:first_name => "Marcos", :last_name => "Tapajós")
end
it "should return true if save the document" do
@sobj.save!.should be_true
end
it "should raise error if don't save the document" do
@sobj.first_name = nil
lambda { @sobj.save! }.should raise_error(CouchRest::Model::Errors::Validations)
end
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_new
@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 be_true
end
it "should not change the slug on update" do
@art.title = 'This is the title'
@art.save.should be_true
@art.title = 'new title'
@art.save.should be_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 be_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 be_true
@art.slug.should == 'this-is-the-title'
end
it "should set the id" do
@art.title = 'This is the title'
@art.save.should be_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 be_true
end
it "should save with the id" do
@templated['important-field'] = 'very-important'
@templated.save.should be_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 be_true
@templated['important-field'] = 'not-important'
@templated.save.should be_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 be_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 be_true
@templated.id.should == 'very-important'
end
end
describe "destroying an instance" do
before(:each) do
@dobj = Basic.new
@dobj.save.should be_true
end
it "should return true" do
result = @dobj.destroy
result.should be_true
end
it "should be resavable" do
@dobj.destroy
@dobj.rev.should be_nil
@dobj.id.should be_nil
@dobj.save.should be_true
end
it "should make it go away" do
@dobj.destroy
lambda{Basic.get!(@dobj.id)}.should raise_error
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
it "should load and instantiate with find" do
foundart = Article.find @art.id
foundart.title.should == "All About Getting"
end
it "should return nil if `get` is used and the document doesn't exist" do
foundart = Article.get 'matt aimonetti'
foundart.should be_nil
end
it "should raise an error if `get!` is used and the document doesn't exist" do
lambda{foundart = Article.get!('matt aimonetti')}.should raise_error
end
it "should raise an error if `find!` is used and the document doesn't exist" do
lambda{foundart = Article.find!('matt aimonetti')}.should raise_error
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 "callbacks" do
before(:each) do
@doc = WithCallBacks.new
end
describe "validation" do
it "should run before_validation before validating" do
@doc.run_before_validate.should be_nil
@doc.should be_valid
@doc.run_before_validate.should be_true
end
it "should run after_validation after validating" do
@doc.run_after_validate.should be_nil
@doc.should be_valid
@doc.run_after_validate.should be_true
end
end
describe "save" do
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 run the grouped callbacks before saving" do
@doc.run_one.should be_nil
@doc.run_two.should be_nil
@doc.run_three.should be_nil
@doc.save.should be_true
@doc.run_one.should be_true
@doc.run_two.should be_true
@doc.run_three.should be_true
end
it "should not run conditional callbacks" do
@doc.run_it = false
@doc.save.should be_true
@doc.conditional_one.should be_nil
@doc.conditional_two.should be_nil
end
it "should run conditional callbacks" do
@doc.run_it = true
@doc.save.should be_true
@doc.conditional_one.should be_true
@doc.conditional_two.should be_true
end
end
describe "create" do
it "should run the before save filter when creating" do
@doc.run_before_save.should be_nil
@doc.create.should_not be_nil
@doc.run_before_save.should be_true
end
it "should run the before create filter" do
@doc.run_before_create.should be_nil
@doc.create.should_not be_nil
@doc.create
@doc.run_before_create.should be_true
end
it "should run the after create filter" do
@doc.run_after_create.should be_nil
@doc.create.should_not be_nil
@doc.create
@doc.run_after_create.should be_true
end
end
describe "update" do
before(:each) do
@doc.save
end
it "should run the before update filter when updating an existing document" do
@doc.run_before_update.should be_nil
@doc.update
@doc.run_before_update.should be_true
end
it "should run the after update filter when updating an existing document" do
@doc.run_after_update.should be_nil
@doc.update
@doc.run_after_update.should be_true
end
it "should run the before update filter when saving an existing document" do
@doc.run_before_update.should be_nil
@doc.save
@doc.run_before_update.should be_true
end
end
end
end

View File

@ -1,16 +1,16 @@
# encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__)
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'invoice')
require File.join(FIXTURE_PATH, 'more', 'service')
require File.join(FIXTURE_PATH, 'more', 'event')
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'user')
require File.join(FIXTURE_PATH, 'more', 'course')
describe "ExtendedDocument properties" do
describe "Model properties" do
before(:each) do
reset_test_db!
@ -134,27 +134,27 @@ describe "ExtendedDocument properties" do
@card.first_name = nil
@card.should_not be_valid
@card.errors.should_not be_empty
@card.errors.on(:first_name).should == ["First name must not be blank"]
@card.errors[:first_name].should == ["can't be blank"]
end
it "should let you look up errors for a field by a string name" do
@card.first_name = nil
@card.should_not be_valid
@card.errors.on('first_name').should == ["First name must not be blank"]
@card.errors['first_name'].should == ["can't be blank"]
end
it "should validate the presence of 2 attributes" do
@invoice.clear
@invoice.should_not be_valid
@invoice.errors.should_not be_empty
@invoice.errors.on(:client_name).first.should == "Client name must not be blank"
@invoice.errors.on(:employee_name).should_not be_empty
@invoice.errors[:client_name].should == ["can't be blank"]
@invoice.errors[:employee_name].should_not be_empty
end
it "should let you set an error message" do
@invoice.location = nil
@invoice.valid?
@invoice.errors.on(:location).should == ["Hey stupid!, you forgot the location"]
@invoice.errors[:location].should == ["Hey stupid!, you forgot the location"]
end
it "should validate before saving" do
@ -165,37 +165,6 @@ describe "ExtendedDocument properties" do
end
end
describe "autovalidation" do
before(:each) do
@service = Service.new(:name => "Coumpound analysis", :price => 3_000)
end
it "should be valid" do
@service.should be_valid
end
it "should not respond to properties not setup" do
@service.respond_to?(:client_name).should be_false
end
describe "property :name, :length => 4...20" do
it "should autovalidate the presence when length is set" do
@service.name = nil
@service.should_not be_valid
@service.errors.should_not be_nil
@service.errors.on(:name).first.should == "Name must be between 4 and 19 characters long"
end
it "should autovalidate the correct length" do
@service.name = "a"
@service.should_not be_valid
@service.errors.should_not be_nil
@service.errors.on(:name).first.should == "Name must be between 4 and 19 characters long"
end
end
end
describe "casting" do
before(:each) do
@course = Course.new(:title => 'Relaxation')
@ -740,50 +709,43 @@ end
describe "Property Class" do
it "should provide name as string" do
property = CouchRest::Property.new(:test, String)
property = CouchRest::Model::Property.new(:test, String)
property.name.should eql('test')
property.to_s.should eql('test')
end
it "should provide class from type" do
property = CouchRest::Property.new(:test, String)
property = CouchRest::Model::Property.new(:test, String)
property.type_class.should eql(String)
end
it "should provide base class from type in array" do
property = CouchRest::Property.new(:test, [String])
property = CouchRest::Model::Property.new(:test, [String])
property.type_class.should eql(String)
end
it "should leave type as string if requested" do
property = CouchRest::Property.new(:test, 'String')
property.type.should eql('String')
property.type_class.should eql(String)
it "should raise error if type as string requested" do
lambda {
property = CouchRest::Model::Property.new(:test, 'String')
}.should raise_error
end
it "should leave type nil and return string by default" do
property = CouchRest::Property.new(:test, nil)
it "should leave type nil and return class as nil also" do
property = CouchRest::Model::Property.new(:test, nil)
property.type.should be_nil
# Type cast should never be used on non-casted property!
property.type_class.should eql(String)
property.type_class.should be_nil
end
it "should convert empty type array to [String]" do
property = CouchRest::Property.new(:test, [])
property.type_class.should eql(String)
end
it "should convert boolean text-type TrueClass" do
property = CouchRest::Property.new(:test, 'boolean')
property.type.should eql('boolean') # no change
property.type_class.should eql(TrueClass)
it "should convert empty type array to [Object]" do
property = CouchRest::Model::Property.new(:test, [])
property.type_class.should eql(Object)
end
it "should set init method option or leave as 'new'" do
# (bad example! Time already typecast)
property = CouchRest::Property.new(:test, Time)
property = CouchRest::Model::Property.new(:test, Time)
property.init_method.should eql('new')
property = CouchRest::Property.new(:test, Time, :init_method => 'parse')
property = CouchRest::Model::Property.new(:test, Time, :init_method => 'parse')
property.init_method.should eql('parse')
end
@ -791,32 +753,32 @@ describe "Property Class" do
describe "casting" do
it "should cast a value" do
property = CouchRest::Property.new(:test, Date)
property = CouchRest::Model::Property.new(:test, Date)
parent = mock("FooObject")
property.cast(parent, "2010-06-16").should eql(Date.new(2010, 6, 16))
property.cast_value(parent, "2010-06-16").should eql(Date.new(2010, 6, 16))
end
it "should cast an array of values" do
property = CouchRest::Property.new(:test, [Date])
property = CouchRest::Model::Property.new(:test, [Date])
parent = mock("FooObject")
property.cast(parent, ["2010-06-01", "2010-06-02"]).should eql([Date.new(2010, 6, 1), Date.new(2010, 6, 2)])
end
it "should set a CastedArray on array of Objects" do
property = CouchRest::Property.new(:test, [Object])
property = CouchRest::Model::Property.new(:test, [Object])
parent = mock("FooObject")
property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(::CouchRest::CastedArray)
property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray)
end
it "should not set a CastedArray on array of Strings" do
property = CouchRest::Property.new(:test, [String])
property = CouchRest::Model::Property.new(:test, [String])
parent = mock("FooObject")
property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should_not eql(::CouchRest::CastedArray)
property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should_not eql(CouchRest::Model::CastedArray)
end
it "should raise and error if value is array when type is not" do
property = CouchRest::Property.new(:test, Date)
property = CouchRest::Model::Property.new(:test, Date)
parent = mock("FooClass")
lambda {
cast = property.cast(parent, [Date.new(2010, 6, 1)])
@ -825,13 +787,13 @@ describe "Property Class" do
it "should set parent as casted_by object in CastedArray" do
property = CouchRest::Property.new(:test, [Object])
property = CouchRest::Model::Property.new(:test, [Object])
parent = mock("FooObject")
property.cast(parent, ["2010-06-01", "2010-06-02"]).casted_by.should eql(parent)
end
it "should set casted_by on new value" do
property = CouchRest::Property.new(:test, CatToy)
property = CouchRest::Model::Property.new(:test, CatToy)
parent = mock("CatObject")
cast = property.cast(parent, {:name => 'catnip'})
cast.casted_by.should eql(parent)

View File

@ -1,4 +1,5 @@
require File.expand_path("../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'course')
@ -9,6 +10,9 @@ Card.property :bg_color, :default => '#ccc'
class BusinessCard < Card
property :extension_code
property :job_title
validates_presence_of :extension_code
validates_presence_of :job_title
end
class DesignBusinessCard < BusinessCard
@ -20,7 +24,7 @@ class OnlineCourse < Course
view_by :url
end
class Animal < CouchRest::ExtendedDocument
class Animal < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name
view_by :name
@ -28,7 +32,7 @@ end
class Dog < Animal; end
describe "Subclassing an ExtendedDocument" do
describe "Subclassing a Model" do
before(:each) do
@card = BusinessCard.new
@ -42,22 +46,18 @@ describe "Subclassing an ExtendedDocument" do
@card.database.uri.should == Card.database.uri
end
it "should share the same autovalidation details" do
@card.auto_validation.should be_true
end
it "should have kept the validation details" do
@card.should_not be_valid
end
it "should have added the new validation details" do
validated_fields = @card.class.validators.contexts[:default].map{|v| v.field_name}
validated_fields.should include(:extension_code)
validated_fields = @card.class.validators.map{|v| v.attributes}.flatten
validated_fields.should include(:extension_code)
validated_fields.should include(:job_title)
end
it "should not add to the parent's validations" do
validated_fields = Card.validators.contexts[:default].map{|v| v.field_name}
validated_fields = Card.validators.map{|v| v.attributes}.flatten
validated_fields.should_not include(:extension_code)
validated_fields.should_not include(:job_title)
end

View File

@ -1,10 +1,12 @@
require File.expand_path("../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
describe "ExtendedDocument views" do
describe "Model views" do
class Unattached < CouchRest::ExtendedDocument
class Unattached < CouchRest::Model::Base
# Note: no use_database here
property :title
property :questions
@ -438,7 +440,7 @@ describe "ExtendedDocument views" do
it "should pass database parameter to pager" do
proxy = mock(:proxy)
proxy.stub!(:paginate)
::CouchRest::Mixins::Collection::CollectionProxy.should_receive(:new).with('database', anything(), anything(), anything(), anything()).and_return(proxy)
::CouchRest::Model::Collection::CollectionProxy.should_receive(:new).with('database', anything(), anything(), anything(), anything()).and_return(proxy)
Article.paginate(:design_doc => 'Article', :view_name => 'by_date', :database => 'database')
end
end

117
spec/fixtures/base.rb vendored Normal file
View File

@ -0,0 +1,117 @@
class WithDefaultValues < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :preset, Object, :default => {:right => 10, :top_align => false}
property :set_by_proc, Time, :default => Proc.new{Time.now}
property :tags, [String], :default => []
property :read_only_with_default, :default => 'generic', :read_only => true
property :default_false, TrueClass, :default => false
property :name
timestamps!
end
class WithSimplePropertyType < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name, String
property :preset, String, :default => 'none'
property :tags, [String]
timestamps!
end
class WithCallBacks < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name
property :run_before_validate
property :run_after_validate
property :run_before_save
property :run_after_save
property :run_before_create
property :run_after_create
property :run_before_update
property :run_after_update
before_validate do |object|
object.run_before_validate = true
end
after_validate do |object|
object.run_after_validate = true
end
before_save do |object|
object.run_before_save = true
end
after_save do |object|
object.run_after_save = true
end
before_create do |object|
object.run_before_create = true
end
after_create do |object|
object.run_after_create = true
end
before_update do |object|
object.run_before_update = true
end
after_update do |object|
object.run_after_update = true
end
property :run_one
property :run_two
property :run_three
before_save :run_one_method, :run_two_method do |object|
object.run_three = true
end
def run_one_method
self.run_one = true
end
def run_two_method
self.run_two = true
end
attr_accessor :run_it
property :conditional_one
property :conditional_two
before_save :conditional_one_method, :conditional_two_method, :if => proc { self.run_it }
def conditional_one_method
self.conditional_one = true
end
def conditional_two_method
self.conditional_two = true
end
end
class WithTemplateAndUniqueID < CouchRest::Model::Base
use_database TEST_SERVER.default_database
unique_id do |model|
model['important-field']
end
property :preset, :default => 'value'
property :has_no_default
end
class WithGetterAndSetterMethods < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :other_arg
def arg
other_arg
end
def arg=(value)
self.other_arg = "foo-#{value}"
end
end
class WithAfterInitializeMethod < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :some_value
def after_initialize
self.some_value ||= "value"
end
end

View File

@ -1,4 +1,4 @@
class Article < CouchRest::ExtendedDocument
class Article < CouchRest::Model::Base
use_database DB
unique_id :slug
@ -20,10 +20,10 @@ class Article < CouchRest::ExtendedDocument
return sum(values);
}"
property :date, :type => 'Date'
property :date, Date
property :slug, :read_only => true
property :title
property :tags, :type => ['String']
property :tags, [String]
timestamps!

View File

@ -1,9 +1,4 @@
class Card < CouchRest::ExtendedDocument
# Include the validation module to get access to the validation methods
include CouchRest::Validation
# set the auto_validation before defining the properties
auto_validate!
class Card < CouchRest::Model::Base
# Set the default database to use
use_database DB

View File

@ -1,16 +1,13 @@
class CatToy < Hash
include ::CouchRest::CastedModel
include ::CouchRest::Validation
include ::CouchRest::Model::CastedModel
property :name
validates_presence_of :name
end
class Cat < CouchRest::ExtendedDocument
include ::CouchRest::Validation
class Cat < CouchRest::Model::Base
# Set the default database to use
use_database DB

View File

@ -1,22 +1,22 @@
require File.join(FIXTURE_PATH, 'more', 'question')
require File.join(FIXTURE_PATH, 'more', 'person')
class Course < CouchRest::ExtendedDocument
class Course < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :title, :cast_as => 'String'
property :questions, :cast_as => ['Question']
property :professor, :cast_as => 'Person'
property :participants, :type => ['Object']
property :ends_at, :type => 'Time'
property :estimate, :type => 'Float'
property :hours, :type => 'Integer'
property :profit, :type => 'BigDecimal'
property :started_on, :type => 'Date'
property :updated_at, :type => 'DateTime'
property :active, :type => 'Boolean'
property :title, String
property :questions, [Question]
property :professor, Person
property :participants, [Object]
property :ends_at, Time
property :estimate, Float
property :hours, Integer
property :profit, BigDecimal
property :started_on, :type => Date
property :updated_at, :type => DateTime
property :active, :type => TrueClass
property :very_active, :type => TrueClass
property :klass, :type => 'Class'
property :klass, :type => Class
view_by :title
view_by :title, :active

View File

@ -1,8 +1,8 @@
class Event < CouchRest::ExtendedDocument
class Event < CouchRest::Model::Base
use_database DB
property :subject
property :occurs_at, :cast_as => 'Time', :init_method => 'parse'
property :end_date, :cast_as => 'Date', :init_method => 'parse'
property :occurs_at, Time, :init_method => 'parse'
property :end_date, Date, :init_method => 'parse'
end

View File

@ -1,7 +1,4 @@
class Invoice < CouchRest::ExtendedDocument
# Include the validation module to get access to the validation methods
include CouchRest::Validation
class Invoice < CouchRest::Model::Base
# Set the default database to use
use_database DB

View File

@ -1,6 +1,6 @@
class Person < Hash
include ::CouchRest::CastedModel
property :pet, :cast_as => 'Cat'
include ::CouchRest::Model::CastedModel
property :pet, Cat
property :name, [String]
def last_name

View File

@ -1,5 +1,5 @@
class Question < Hash
include ::CouchRest::CastedModel
include ::CouchRest::Model::CastedModel
property :q
property :a

View File

@ -1,12 +1,10 @@
class Service < CouchRest::ExtendedDocument
# Include the validation module to get access to the validation methods
include CouchRest::Validation
auto_validate!
class Service < CouchRest::Model::Base
# Set the default database to use
use_database DB
# Official Schema
property :name, :length => 4...20
property :price, :type => 'Integer'
property :name
property :price, Integer
end
validates_length_of :name, :minimum => 4, :maximum => 20
end

View File

@ -1,11 +1,11 @@
class User < CouchRest::ExtendedDocument
class User < CouchRest::Model::Base
# Set the default database to use
use_database DB
property :name, :accessible => true
property :admin # this will be automatically protected
end
class SpecialUser < CouchRest::ExtendedDocument
class SpecialUser < CouchRest::Model::Base
# Set the default database to use
use_database DB
property :name # this will not be protected

View File

@ -1,7 +1,7 @@
require "rubygems"
require "spec" # Satisfies Autotest and anyone else not using the Rake tasks
require File.join(File.dirname(__FILE__), '..','lib','couchrest_extended_document')
require File.join(File.dirname(__FILE__), '..','lib','couchrest_model')
# check the following file to see how to use the spec'd features.
unless defined?(FIXTURE_PATH)
@ -9,14 +9,13 @@ unless defined?(FIXTURE_PATH)
SCRATCH_PATH = File.join(File.dirname(__FILE__), '/tmp')
COUCHHOST = "http://127.0.0.1:5984"
TESTDB = 'couchrest-test'
REPLICATIONDB = 'couchrest-test-replication'
TESTDB = 'couchrest-model-test'
TEST_SERVER = CouchRest.new
TEST_SERVER.default_database = TESTDB
DB = TEST_SERVER.database(TESTDB)
end
class Basic < CouchRest::ExtendedDocument
class Basic < CouchRest::Model::Base
use_database TEST_SERVER.default_database
end