added autovalidation (auto_validate! in your ExtendedDocument) and extracted some extlib stuff so we will soon be able to remove the dependency.

This commit is contained in:
Matt Aimonetti 2009-02-05 17:06:12 -08:00
parent e9930c5a86
commit 890b60cae4
18 changed files with 497 additions and 52 deletions

View file

@ -1,18 +1,6 @@
module CouchRest
class Response < Hash
def initialize(keys = {})
keys.each do |k,v|
self[k.to_s] = v
end
end
def []= key, value
super(key.to_s, value)
end
def [] key
super(key.to_s)
end
end
require 'delegate'
module CouchRest
class Document < Response
include CouchRest::Mixins::Attachments

View file

@ -1,3 +1,5 @@
require File.join(File.dirname(__FILE__), '..', 'support', 'class')
# Extracted from ActiveSupport::Callbacks written by Yehuda Katz
# http://github.com/wycats/rails/raw/18b405f154868204a8f332888871041a7bad95e1/activesupport/lib/active_support/callbacks.rb

View file

@ -7,23 +7,27 @@ module CouchRest
end
module ClassMethods
# Stores the class properties
def properties
@@properties ||= []
end
# This is not a thread safe operation, if you have to set new properties at runtime
# make sure to use a mutex.
def property(name, options={})
unless properties.map{|p| p.name}.include?(name.to_s)
property = CouchRest::Property.new(name, options.delete(:type), options)
create_property_getter(property)
create_property_setter(property) unless property.read_only == true
properties << property
end
define_property(name, options) unless properties.map{|p| p.name}.include?(name.to_s)
end
protected
# This is not a thread safe operation, if you have to set new properties at runtime
# make sure to use a mutex.
def define_property(name, options={})
property = CouchRest::Property.new(name, options.delete(:type), options)
create_property_getter(property)
create_property_setter(property) unless property.read_only == true
properties << property
end
# defines the getter for the property
def create_property_getter(property)
meth = property.name

View file

@ -27,10 +27,14 @@ class Object
end
end
require 'pathname'
require File.join(File.dirname(__FILE__), '..', 'support', 'class')
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')
@ -50,6 +54,13 @@ module CouchRest
save_callback :before, :check_validations
end
EOS
base.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def self.define_property(name, options={})
super
auto_generate_validations(properties.last)
autovalidation_check = true
end
RUBY_EVAL
end
# Ensures the object is valid for the context provided, and otherwise
@ -136,7 +147,7 @@ module CouchRest
include CouchRest::Validation::ValidatesWithMethod
# include CouchRest::Validation::ValidatesWithBlock
# include CouchRest::Validation::ValidatesIsUnique
# include CouchRest::Validation::AutoValidate
include CouchRest::Validation::AutoValidate
# Return the set of contextual validators or create a new one
#

View file

@ -4,7 +4,6 @@ module CouchRest
def self.included(base)
base.extend(ClassMethods)
# extlib is required for the following code
base.send(:class_inheritable_accessor, :design_doc)
base.send(:class_inheritable_accessor, :design_doc_slug_cache)
base.send(:class_inheritable_accessor, :design_doc_fresh)

View file

@ -1,3 +1,6 @@
require File.join(File.dirname(__FILE__), 'support', 'class')
require File.join(File.dirname(__FILE__), 'support', 'blank')
# This file must be loaded after the JSON gem and any other library that beats up the Time class.
class Time
# This date format sorts lexicographically

View file

@ -1,3 +1,10 @@
begin
require 'extlib'
rescue
puts "CouchRest::ExtendedDocument still requires extlib (not for much longer). This is left out of the gemspec on purpose."
raise
end
require 'mime/types'
require File.join(File.dirname(__FILE__), "property")
require File.join(File.dirname(__FILE__), '..', 'mixins', 'extended_document_mixins')
@ -6,11 +13,11 @@ module CouchRest
# Same as CouchRest::Document but with properties and validations
class ExtendedDocument < Document
include CouchRest::Mixins::DocumentQueries
include CouchRest::Callbacks
include CouchRest::Mixins::DocumentProperties
include CouchRest::Mixins::DocumentQueries
include CouchRest::Mixins::Views
include CouchRest::Mixins::DesignDoc
include CouchRest::Callbacks
# Callbacks
define_callbacks :save
@ -98,6 +105,7 @@ module CouchRest
# Overridden to set the unique ID.
# Returns a boolean value
def save_without_callbacks(bulk = false)
raise ArgumentError, "a document requires database to be saved to" unless database
set_unique_id if new_document? && self.respond_to?(:set_unique_id)
result = database.save_doc(self, bulk)
result["ok"] == true

View file

@ -1,13 +1,13 @@
module CouchRest
# Basic attribute support adding getter/setter + validation
# Basic attribute support for adding getter/setter + validation
class Property
attr_reader :name, :type, :validation_format, :required, :read_only, :alias
attr_reader :name, :type, :read_only, :alias, :options
# attribute to define
def initialize(name, type = String, options = {})
def initialize(name, type = nil, options = {})
@name = name.to_s
@type = type
@type = type || String
parse_options(options)
self
end
@ -16,10 +16,10 @@ module CouchRest
private
def parse_options(options)
return if options.empty?
@required = true if (options[:required] && (options[:required] == true))
@validation_format = options[:format] if options[:format]
@read_only = options[:read_only] if options[:read_only]
@alias = options[:alias] if options
@validation_format = options.delete(:format) if options[:format]
@read_only = options.delete(:read_only) if options[:read_only]
@alias = options.delete(:alias) if options[:alias]
@options = options
end
end

View file

@ -0,0 +1,42 @@
# blank? methods for several different class types
class Object
# Returns true if the object is nil or empty (if applicable)
def blank?
nil? || (respond_to?(:empty?) && empty?)
end
end # class Object
class Numeric
# Numerics can't be blank
def blank?
false
end
end # class Numeric
class NilClass
# Nils are always blank
def blank?
true
end
end # class NilClass
class TrueClass
# True is not blank.
def blank?
false
end
end # class TrueClass
class FalseClass
# False is always blank.
def blank?
true
end
end # class FalseClass
class String
# Strips out whitespace then tests if the string is empty.
def blank?
strip.empty?
end
end # class String

View file

@ -0,0 +1,175 @@
# Copyright (c) 2004-2008 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.
# Allows attributes to be shared within an inheritance hierarchy, but where
# each descendant gets a copy of their parents' attributes, instead of just a
# pointer to the same. This means that the child can add elements to, for
# example, an array without those additions being shared with either their
# parent, siblings, or children, which is unlike the regular class-level
# attributes that are shared across the entire hierarchy.
class Class
# Defines class-level and instance-level attribute reader.
#
# @param *syms<Array> Array of attributes to define reader for.
# @return <Array[#to_s]> List of attributes that were made into cattr_readers
#
# @api public
#
# @todo Is this inconsistent in that it does not allow you to prevent
# an instance_reader via :instance_reader => false
def cattr_reader(*syms)
syms.flatten.each do |sym|
next if sym.is_a?(Hash)
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
unless defined? @@#{sym}
@@#{sym} = nil
end
def self.#{sym}
@@#{sym}
end
def #{sym}
@@#{sym}
end
RUBY
end
end
# Defines class-level (and optionally instance-level) attribute writer.
#
# @param <Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define writer for.
# @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
# @return <Array[#to_s]> List of attributes that were made into cattr_writers
#
# @api public
def cattr_writer(*syms)
options = syms.last.is_a?(Hash) ? syms.pop : {}
syms.flatten.each do |sym|
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
unless defined? @@#{sym}
@@#{sym} = nil
end
def self.#{sym}=(obj)
@@#{sym} = obj
end
RUBY
unless options[:instance_writer] == false
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def #{sym}=(obj)
@@#{sym} = obj
end
RUBY
end
end
end
# Defines class-level (and optionally instance-level) attribute accessor.
#
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define accessor for.
# @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
# @return <Array[#to_s]> List of attributes that were made into accessors
#
# @api public
def cattr_accessor(*syms)
cattr_reader(*syms)
cattr_writer(*syms)
end
# Defines class-level inheritable attribute reader. Attributes are available to subclasses,
# each subclass has a copy of parent's attribute.
#
# @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
# @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
#
# @api public
#
# @todo Do we want to block instance_reader via :instance_reader => false
# @todo It would be preferable that we do something with a Hash passed in
# (error out or do the same as other methods above) instead of silently
# moving on). In particular, this makes the return value of this function
# less useful.
def class_inheritable_reader(*ivars)
instance_reader = ivars.pop[:reader] if ivars.last.is_a?(Hash)
ivars.each do |ivar|
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def self.#{ivar}
return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar})
ivar = superclass.#{ivar}
return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
@#{ivar} = ivar && !ivar.is_a?(Module) && !ivar.is_a?(Numeric) && !ivar.is_a?(TrueClass) && !ivar.is_a?(FalseClass) && !ivar.is_a?(Symbol) ? ivar.dup : ivar
end
RUBY
unless instance_reader == false
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{ivar}
self.class.#{ivar}
end
RUBY
end
end
end
# Defines class-level inheritable attribute writer. Attributes are available to subclasses,
# each subclass has a copy of parent's attribute.
#
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
# define inheritable writer for.
# @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
# @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
#
# @api public
#
# @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
# class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
def class_inheritable_writer(*ivars)
instance_writer = ivars.pop[:instance_writer] if ivars.last.is_a?(Hash)
ivars.each do |ivar|
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def self.#{ivar}=(obj)
@#{ivar} = obj
end
RUBY
unless instance_writer == false
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{ivar}=(obj) self.class.#{ivar} = obj end
RUBY
end
end
end
# Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
# each subclass has a copy of parent's attribute.
#
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
# define inheritable accessor for.
# @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
# @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
#
# @api public
def class_inheritable_accessor(*syms)
class_inheritable_reader(*syms)
class_inheritable_writer(*syms)
end
end

View file

@ -0,0 +1,167 @@
# Ported from dm-migrations
require File.join(File.dirname(__FILE__), '..', 'support', 'class')
module CouchRest
class Property
# flag letting us know if we already checked the autovalidation settings
attr_accessor :autovalidation_check
end
module Validation
module AutoValidate
# Turn off auto validation by default
def auto_validation
@@auto_validation ||= false
end
# 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_is_number
# validator to be created for the property. integer_only
# is set to true
#
# BigDecimal or 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.options
return unless property.autovalidation_check || auto_validation || (property.options && property.options.has_key?(:auto_validation) && property.options[:auto_validation])
# value is set by the storage system
opts = {}
opts[:context] = property.options[:validates] if property.options.has_key?(:validates)
# presence
unless opts[:allow_nil]
# validates_present property.name, opts
validates_present property.name, options_with_message(opts, property, :presence)
end
# length
if property.type == 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 property.name, opts
p "dude: #{options_with_message(opts, property, :length)}"
validates_length 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 Integer == property.type
opts[:integer_only] = true
# validates_is_number property.name, opts
validates_is_number property.name, options_with_message(opts, property, :is_number)
elsif Float == property.type
opts[:precision] = property.precision
opts[:scale] = property.scale
# validates_is_number property.name, opts
validates_is_number property.name, options_with_message(opts, property, :is_number)
end
# marked the property has checked
property.autovalidation_check = true
end
end # module AutoValidate
end # module Validation
end # module CouchRest

View file

@ -75,7 +75,7 @@ module CouchRest
# @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] ||= []) << message
(errors[field_name.to_sym] ||= []) << message
end
# Collect all errors into a single list.

View file

@ -66,7 +66,7 @@ module CouchRest
# Returns false for other property types.
# Returns false for non-properties.
def boolean_type?(property)
property ? property.primitive == TrueClass : false
property ? property.type == TrueClass : false
end
end # class RequiredFieldValidator

View file

@ -2,17 +2,17 @@ require File.dirname(__FILE__) + '/../../spec_helper'
describe CouchRest::Server do
before(:all) do
@couch = CouchRest::Server.new
end
after(:all) do
@couch.available_databases.each do |ref, db|
db.delete!
end
end
describe "available databases" do
before(:each) do
@couch = CouchRest::Server.new
end
after(:each) do
@couch.available_databases.each do |ref, db|
db.delete!
end
end
it "should let you add more databases" do
@couch.available_databases.should be_empty
@couch.define_available_database(:default, "cr-server-test-db")
@ -20,6 +20,7 @@ describe CouchRest::Server do
end
it "should verify that a database is available" do
@couch.define_available_database(:default, "cr-server-test-db")
@couch.available_database?(:default).should be_true
@couch.available_database?("cr-server-test-db").should be_true
@couch.available_database?(:matt).should be_false

View file

@ -1,8 +1,7 @@
require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
# check the following file to see how to use the spec'd features.
require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'invoice')
require File.join(FIXTURE_PATH, 'more', 'service')
describe "ExtendedDocument properties" do
@ -37,6 +36,8 @@ describe "ExtendedDocument properties" do
it "should be auto timestamped" do
@card.created_at.should be_nil
@card.updated_at.should be_nil
# :emo:hack for autospec
Card.use_database(TEST_SERVER.default_database) if @card.database.nil?
@card.save
@card.created_at.should_not be_nil
@card.updated_at.should_not be_nil
@ -49,7 +50,7 @@ describe "ExtendedDocument properties" do
end
it "should be able to be validated" do
@card.should be_valid
@card.valid?.should == true
end
it "should let you validate the presence of an attribute" do
@ -82,4 +83,31 @@ describe "ExtendedDocument properties" do
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
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 not be blank"
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
end

View file

@ -15,4 +15,6 @@ class Card < CouchRest::ExtendedDocument
# Validation
validates_present :first_name
auto_validate!
end

14
spec/fixtures/more/service.rb vendored Normal file
View file

@ -0,0 +1,14 @@
class Service < CouchRest::ExtendedDocument
# Include the validation module to get access to the validation methods
include CouchRest::Validation
# Set the default database to use
use_database TEST_SERVER.default_database
# Official Schema
property :name, :length => 4...20
property :price, :type => Integer
auto_validate!
end

View file

@ -1,7 +1,8 @@
require "rubygems"
require "spec" # Satisfies Autotest and anyone else not using the Rake tasks
require File.join(File.dirname(__FILE__), '/../lib/couchrest')
require File.join(File.dirname(__FILE__), '..','lib','couchrest')
# check the following file to see how to use the spec'd features.
unless defined?(FIXTURE_PATH)
FIXTURE_PATH = File.join(File.dirname(__FILE__), '/fixtures')