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.
This commit is contained in:
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,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