From c280b3a29bb17ac7c262a03beee1e64ee7cb0d01 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Sun, 20 Jun 2010 22:01:11 +0200 Subject: [PATCH] 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. --- README.md | 79 +- Rakefile | 17 +- history.txt | 19 +- lib/couchrest/extended_document.rb | 261 ------ lib/couchrest/mixins.rb | 13 - lib/couchrest/mixins/callbacks.rb | 532 ----------- lib/couchrest/model.rb | 10 + .../{mixins => model}/associations.rb | 18 +- .../{mixins => model}/attribute_protection.rb | 2 +- lib/couchrest/{mixins => model}/attributes.rb | 2 +- lib/couchrest/model/base.rb | 93 ++ lib/couchrest/model/callbacks.rb | 27 + lib/couchrest/{ => model}/casted_array.rb | 2 +- lib/couchrest/{ => model}/casted_model.rb | 21 +- .../{mixins => model}/class_proxy.rb | 20 +- lib/couchrest/{mixins => model}/collection.rb | 2 +- lib/couchrest/{mixins => model}/design_doc.rb | 5 +- .../{mixins => model}/document_queries.rb | 2 +- lib/couchrest/model/errors.rb | 23 + .../{mixins => model}/extended_attachments.rb | 2 +- lib/couchrest/model/persistence.rb | 141 +++ lib/couchrest/{mixins => model}/properties.rb | 29 +- lib/couchrest/{ => model}/property.rb | 30 +- .../{ => model}/support/couchrest.rb | 6 +- lib/couchrest/{ => model}/support/rails.rb | 12 +- lib/couchrest/{mixins => model}/typecast.rb | 6 +- lib/couchrest/model/validations.rb | 32 + .../model/validations/casted_model.rb | 14 + lib/couchrest/{mixins => model}/views.rb | 7 +- lib/couchrest/validation.rb | 244 ----- lib/couchrest/validation/auto_validate.rb | 156 ---- .../validation/contextual_validators.rb | 78 -- lib/couchrest/validation/validation_errors.rb | 125 --- .../validators/absent_field_validator.rb | 74 -- .../validators/confirmation_validator.rb | 107 --- .../validation/validators/format_validator.rb | 122 --- .../validation/validators/formats/email.rb | 66 -- .../validation/validators/formats/url.rb | 43 - .../validators/generic_validator.rb | 120 --- .../validation/validators/length_validator.rb | 139 --- .../validation/validators/method_validator.rb | 89 -- .../validators/numeric_validator.rb | 109 --- .../validators/required_field_validator.rb | 114 --- lib/couchrest_extended_document.rb | 22 - lib/couchrest_model.rb | 56 ++ spec/couchrest/assocations_spec.rb | 10 +- ..._attachment_spec.rb => attachment_spec.rb} | 14 +- spec/couchrest/attribute_protection_spec.rb | 285 +++--- spec/couchrest/base_spec.rb | 403 ++++++++ spec/couchrest/casted_model_spec.rb | 32 +- ...ed_extended_doc_spec.rb => casted_spec.rb} | 12 +- spec/couchrest/class_proxy_spec.rb | 2 +- spec/couchrest/extended_doc_spec.rb | 869 ------------------ ...oc_inherited_spec.rb => inherited_spec.rb} | 2 +- spec/couchrest/persistence_spec.rb | 409 +++++++++ spec/couchrest/property_spec.rb | 100 +- ..._doc_subclass_spec.rb => subclass_spec.rb} | 18 +- ...extended_doc_view_spec.rb => view_spec.rb} | 8 +- spec/fixtures/base.rb | 117 +++ spec/fixtures/more/article.rb | 6 +- spec/fixtures/more/card.rb | 7 +- spec/fixtures/more/cat.rb | 7 +- spec/fixtures/more/course.rb | 26 +- spec/fixtures/more/event.rb | 6 +- spec/fixtures/more/invoice.rb | 5 +- spec/fixtures/more/person.rb | 4 +- spec/fixtures/more/question.rb | 2 +- spec/fixtures/more/service.rb | 12 +- spec/fixtures/more/user.rb | 4 +- spec/spec_helper.rb | 7 +- 70 files changed, 1725 insertions(+), 3733 deletions(-) delete mode 100644 lib/couchrest/extended_document.rb delete mode 100644 lib/couchrest/mixins.rb delete mode 100644 lib/couchrest/mixins/callbacks.rb create mode 100644 lib/couchrest/model.rb rename lib/couchrest/{mixins => model}/associations.rb (90%) rename lib/couchrest/{mixins => model}/attribute_protection.rb (99%) rename lib/couchrest/{mixins => model}/attributes.rb (99%) create mode 100644 lib/couchrest/model/base.rb create mode 100644 lib/couchrest/model/callbacks.rb rename lib/couchrest/{ => model}/casted_array.rb (97%) rename lib/couchrest/{ => model}/casted_model.rb (72%) rename lib/couchrest/{mixins => model}/class_proxy.rb (89%) rename lib/couchrest/{mixins => model}/collection.rb (99%) rename lib/couchrest/{mixins => model}/design_doc.rb (98%) rename lib/couchrest/{mixins => model}/document_queries.rb (99%) create mode 100644 lib/couchrest/model/errors.rb rename lib/couchrest/{mixins => model}/extended_attachments.rb (99%) create mode 100644 lib/couchrest/model/persistence.rb rename lib/couchrest/{mixins => model}/properties.rb (79%) rename lib/couchrest/{ => model}/property.rb (74%) rename lib/couchrest/{ => model}/support/couchrest.rb (50%) rename lib/couchrest/{ => model}/support/rails.rb (65%) rename lib/couchrest/{mixins => model}/typecast.rb (98%) create mode 100644 lib/couchrest/model/validations.rb create mode 100644 lib/couchrest/model/validations/casted_model.rb rename lib/couchrest/{mixins => model}/views.rb (98%) delete mode 100644 lib/couchrest/validation.rb delete mode 100644 lib/couchrest/validation/auto_validate.rb delete mode 100644 lib/couchrest/validation/contextual_validators.rb delete mode 100644 lib/couchrest/validation/validation_errors.rb delete mode 100644 lib/couchrest/validation/validators/absent_field_validator.rb delete mode 100644 lib/couchrest/validation/validators/confirmation_validator.rb delete mode 100644 lib/couchrest/validation/validators/format_validator.rb delete mode 100644 lib/couchrest/validation/validators/formats/email.rb delete mode 100644 lib/couchrest/validation/validators/formats/url.rb delete mode 100644 lib/couchrest/validation/validators/generic_validator.rb delete mode 100644 lib/couchrest/validation/validators/length_validator.rb delete mode 100644 lib/couchrest/validation/validators/method_validator.rb delete mode 100644 lib/couchrest/validation/validators/numeric_validator.rb delete mode 100644 lib/couchrest/validation/validators/required_field_validator.rb delete mode 100644 lib/couchrest_extended_document.rb create mode 100644 lib/couchrest_model.rb rename spec/couchrest/{extended_doc_attachment_spec.rb => attachment_spec.rb} (95%) create mode 100644 spec/couchrest/base_spec.rb rename spec/couchrest/{casted_extended_doc_spec.rb => casted_spec.rb} (81%) delete mode 100644 spec/couchrest/extended_doc_spec.rb rename spec/couchrest/{extended_doc_inherited_spec.rb => inherited_spec.rb} (94%) create mode 100644 spec/couchrest/persistence_spec.rb rename spec/couchrest/{extended_doc_subclass_spec.rb => subclass_spec.rb} (85%) rename spec/couchrest/{extended_doc_view_spec.rb => view_spec.rb} (97%) create mode 100644 spec/fixtures/base.rb diff --git a/README.md b/README.md index 537e86d..cc98d59 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,25 @@ -# CouchRest::ExtendedDocument: CouchDB, not too close to the metal +# CouchRest Model: CouchDB, close to shiny metal with rounded edges -CouchRest::ExtendedDocument adds additional functionality to the standard CouchRest Document class such as +CouchRest Models adds additional functionality to the standard CouchRest Document class such as setting properties, callbacks, typecasting, and validations. -Note: CouchRest::ExtendedDocument only supports CouchDB 0.10.0 or newer. +Originally called ExtendedDocument, the new Model structure uses ActiveModel, part of Rails 3, +for validations and callbacks. + +If your project is still running Rails 2.3, you'll have to continue using ExtendedDocument as +it is not possible to load ActiveModel into programs that do not use ActiveSupport 3.0. + +CouchRest Model only supports CouchDB 0.10.0 or newer. ## Install - $ sudo gem install couchrest_extended_document + $ sudo gem install couchrest_model -## Usage +## General Usage -### General + require 'couchrest_model' - require 'couchrest_extended_document' - - class Cat < CouchRest::ExtendedDocument + class Cat < CouchRest::Model::Base property :name, String property :lives, Integer, :default => 9 @@ -43,9 +47,9 @@ Note: CouchRest::ExtendedDocument only supports CouchDB 0.10.0 or newer. @cat.random_text # Raises error! -### Properties +## Properties -Only attributes with a property definition will be stored be ExtendedDocument (as opposed +Only attributes with a property definition will be stored be CouchRest Model (as opposed to a normal CouchRest Document which will store everything). To help prevent confusion, a property should be considered as the definition of an attribute. An attribute must be associated with a property, but a property may not have any attributes associated if none have been set. @@ -56,7 +60,7 @@ will only create a getter and setter passing all attribute data directly to the provided responds to +to_json+, there will not be any problems saving it, but when loading the data back it will either be a string, number, array, or hash: - class Cat < CouchRest::ExtendedDocument + class Cat < CouchRest::Model::Base property :name property :birthday end @@ -82,7 +86,7 @@ Properties create getters and setters similar to the following: Properties can also have a type which will be used for casting data retrieved from CouchDB when the attribute is set: - class Cat < CouchRest::ExtendedDocument + class Cat < CouchRest::Model::Base property :name, String property :last_fed_at, Time end @@ -96,7 +100,7 @@ will be used for casting data retrieved from CouchDB when the attribute is set: Booleans or TrueClass will also create a getter with question mark at the end: - class Cat < CouchRest::ExtendedDocument + class Cat < CouchRest::Model::Base property :awake, TrueClass, :default => true end @@ -108,7 +112,7 @@ Defining a property as read-only will mean that its value is set only when read database and that it will not have a setter method. You can however update a read-only attribute using the +write_attribute+ method: - class Cat < CouchRest::ExtendedDocument + class Cat < CouchRest::Model::Base property :name, String property :lives, Integer, :default => 9, :readonly => true @@ -123,12 +127,12 @@ attribute using the +write_attribute+ method: @cat.lives # Now 8! -### Property Arrays +## Property Arrays -An attribute may also contain an array of data. ExtendedDocument handles this, along +An attribute may also contain an array of data. CouchRest Model handles this, along with casting, by defining the class of the child attributes inside an Array: - class Cat < CouchRest::ExtendedDocument + class Cat < CouchRest::Model::Base property :name, String property :nicknames, [String] end @@ -144,21 +148,21 @@ When anything other than a string is set as the class of a property, the array w into special wrapper called a CastedArray. If the child objects respond to the 'casted_by' method (such as those created with CastedModel, below) it will contain a reference to the parent. -### Casted Models +## Casted Models -ExtendedDocument allows you to take full advantage of CouchDB's ability to store complex +CouchRest Model allows you to take full advantage of CouchDB's ability to store complex documents and retrieve them using the CastedModel module. Simply include the module in a Hash (or other model that responds to the [] and []= methods) and set any properties you'd like to use. For example: class CatToy << Hash - include CouchRest::CastedModel + include CouchRest::Model::CastedModel property :name, String property :purchased, Date end - class Cat << CouchRest::ExtendedDocument + class Cat << CouchRest::Model::Base property :name, String property :toys, [CatToy] end @@ -174,10 +178,10 @@ Additionally, any hashes sent to the property will automatically be converted: Of course, to use your own classes they *must* be defined before the parent uses them otherwise Ruby will bring up a missing constant error. To avoid this, or if you have a really simple array of data -you'd like to model, the latest version of ExtendedDocument (> 1.0.0) supports creating +you'd like to model, the latest version of CouchRest Model (> 1.0.0) supports creating anonymous classes: - class Cat << CouchRest::ExtendedDocument + class Cat << CouchRest::Model::Base property :name, String property :toys do |toy| @@ -192,26 +196,34 @@ anonymous classes: Using this method of anonymous classes will *only* create arrays of objects. -### Notable Issues +## Notable Issues -ExtendedDocument uses active_support for some of its internals. Ensure you have a stable active support gem installed +CouchRest Model uses active_support for some of its internals. Ensure you have a stable active support gem installed or at least 3.0.0.beta4. JSON gem versions 1.4.X are kown to cause problems with stack overflows and general badness. Version 1.2.4 appears to work fine. -### Ruby on Rails +## Ruby on Rails -CouchRest::ExtendedDocument is compatible with rails and provides some ActiveRecord-like methods. -You might also be interested in the CouchRest companion rails project: -[http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) +CouchRest Model is compatible with rails and provides some ActiveRecord-like methods. -#### Rails 2.X +The CouchRest companion rails project +[http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) is great +for provided default connection details for your database. At the time of writting however it +does not provide explicit support for CouchRest Model. + +CouchRest Model and the original CouchRest ExtendedDocument do not share the same namespace, +as such you should not have any problems using them both at the same time. This might +help with migrations. + + +### Rails 3.0 In your environment.rb file require the gem as follows: Rails::Initializer.run do |config| .... - config.gem "couchrest_extended_document" + config.gem "couchrest_model" .... end @@ -223,12 +235,11 @@ CouchRest install, from the project root directory run `rake`, or `autotest` ## Docs -API: [http://rdoc.info/projects/couchrest/couchrest_extended_document](http://rdoc.info/projects/couchrest/couchrest_extended_document) +API: [http://rdoc.info/projects/couchrest/couchrest_model](http://rdoc.info/projects/couchrest/couchrest_model) Check the wiki for documentation and examples [http://wiki.github.com/couchrest/couchrest](http://wiki.github.com/couchrest/couchrest) - ## Contact Please post bugs, suggestions and patches to the bug tracker at [http://github.com/couchrest/couchrest/issues](http://github.com/couchrest/couchrest/issues). diff --git a/Rakefile b/Rakefile index c296968..abae8b9 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,8 @@ require 'rake' require "rake/rdoctask" -require File.join(File.expand_path(File.dirname(__FILE__)),'lib','couchrest_extended_document') + +$LOAD_PATH.unshift File.expand_path("../lib", __FILE__) +require 'couchrest_model' begin require 'spec/rake/spectask' @@ -15,19 +17,20 @@ end begin require 'jeweler' Jeweler::Tasks.new do |gemspec| - gemspec.name = "couchrest_extended_document" - gemspec.summary = "Extend CouchRest Document class with useful features." - gemspec.description = "CouchRest::ExtendedDocument provides aditional features to the standard CouchRest::Document class such as properties, view designs, callbacks, typecasting and validations." + gemspec.name = "couchrest_model" + gemspec.summary = "Extends the CouchRest Document for advanced modelling." + gemspec.description = "CouchRest Model provides aditional features to the standard CouchRest Document class such as properties, view designs, associations, callbacks, typecasting and validations." gemspec.email = "jchris@apache.org" - gemspec.homepage = "http://github.com/couchrest/couchrest_extended_document" - gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber"] + gemspec.homepage = "http://github.com/couchrest/couchrest_model" + gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber", "Sam Lown"] gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md ) gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt couchrest.gemspec) + Dir["{examples,lib,spec,utils}/**/*"] - Dir["spec/tmp"] gemspec.has_rdoc = true gemspec.add_dependency("couchrest", ">= 1.0.0.beta") gemspec.add_dependency("mime-types", ">= 1.15") gemspec.add_dependency("activesupport", ">= 2.3.5") - gemspec.version = CouchRest::ExtendedDocument::VERSION + gemspec.add_dependency("activemodel", ">= 3.0.0.beta4") + gemspec.version = CouchRest::Model::VERSION gemspec.date = "2008-11-22" gemspec.require_path = "lib" end diff --git a/history.txt b/history.txt index f385ebd..9001ce1 100644 --- a/history.txt +++ b/history.txt @@ -4,8 +4,22 @@ * Minor enhancements +== CouchRest Model 1.0.0.beta7 + +* Major enhancements + * Renamed ExtendedDocument to CouchRest::Model + * Added initial support for simple belongs_to associations + * Added support for basic collection_of association (unique to document databases!) + * Moved Validation to ActiveModel + * Moved Callbacks to ActiveModel + * Removed support for properties defined using a string for the type instead of a class + + == 1.0.0.beta6 +* Major enhancements + * Added support for anonymous CastedModels defined in Documents + * Minor enhancements * Added 'find_by_*' alias for finding first item in view with matching key. * Fixed issue with active_support in Rails3 and text in README for JSON. @@ -15,11 +29,6 @@ * Setting a property of type Array (or keyed hash) must be an array or an error will be raised. * Now possible to set Array attribute from hash where keys determine order. -* Major enhancements - * Added support for anonymous CastedModels defined in Documents - * Added initial support for simple belongs_to associations - * Added support for basic collection_of association (unique to document databases!) - == 1.0.0.beta5 * Minor enhancements diff --git a/lib/couchrest/extended_document.rb b/lib/couchrest/extended_document.rb deleted file mode 100644 index 84c86d1..0000000 --- a/lib/couchrest/extended_document.rb +++ /dev/null @@ -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 updated_at and created_at fields - # on the document whenever saving occurs. CouchRest uses a pretty - # decent time format by default. See Time#to_json - def 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 _id. - # Because CouchDB enforces a constraint that each id must be unique, - # this can be used to enforce eg: uniq usernames. Note that this id - # must be globally unique across all document types which share a - # database, so if you'd like to scope uniqueness to this class, you - # should use the class name as part of the unique id. - def 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 _id and _rev fields, preparing the - # document to be saved to a new _id. - 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 diff --git a/lib/couchrest/mixins.rb b/lib/couchrest/mixins.rb deleted file mode 100644 index 509e1a6..0000000 --- a/lib/couchrest/mixins.rb +++ /dev/null @@ -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') diff --git a/lib/couchrest/mixins/callbacks.rb b/lib/couchrest/mixins/callbacks.rb deleted file mode 100644 index 989fe25..0000000 --- a/lib/couchrest/mixins/callbacks.rb +++ /dev/null @@ -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 _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 _callback method also updates the _run__callbacks - # method, which is the public API to run the callbacks. - # - # Also creates a skip__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 diff --git a/lib/couchrest/model.rb b/lib/couchrest/model.rb new file mode 100644 index 0000000..77eb586 --- /dev/null +++ b/lib/couchrest/model.rb @@ -0,0 +1,10 @@ + +module CouchRest + + module Model + + VERSION = "1.0.0.beta7" + + end + +end diff --git a/lib/couchrest/mixins/associations.rb b/lib/couchrest/model/associations.rb similarity index 90% rename from lib/couchrest/mixins/associations.rb rename to lib/couchrest/model/associations.rb index 0dbc1b6..6762250 100644 --- a/lib/couchrest/mixins/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -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 diff --git a/lib/couchrest/mixins/attribute_protection.rb b/lib/couchrest/model/attribute_protection.rb similarity index 99% rename from lib/couchrest/mixins/attribute_protection.rb rename to lib/couchrest/model/attribute_protection.rb index b2efc53..e73ac50 100644 --- a/lib/couchrest/mixins/attribute_protection.rb +++ b/lib/couchrest/model/attribute_protection.rb @@ -1,5 +1,5 @@ module CouchRest - module Mixins + module Model module AttributeProtection # Attribute protection from mass assignment to CouchRest properties # diff --git a/lib/couchrest/mixins/attributes.rb b/lib/couchrest/model/attributes.rb similarity index 99% rename from lib/couchrest/mixins/attributes.rb rename to lib/couchrest/model/attributes.rb index 8c2753a..40ddbc9 100644 --- a/lib/couchrest/mixins/attributes.rb +++ b/lib/couchrest/model/attributes.rb @@ -1,5 +1,5 @@ module CouchRest - module Mixins + module Model module Attributes ## Support for handling attributes diff --git a/lib/couchrest/model/base.rb b/lib/couchrest/model/base.rb new file mode 100644 index 0000000..4c4856b --- /dev/null +++ b/lib/couchrest/model/base.rb @@ -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 diff --git a/lib/couchrest/model/callbacks.rb b/lib/couchrest/model/callbacks.rb new file mode 100644 index 0000000..f1d16a5 --- /dev/null +++ b/lib/couchrest/model/callbacks.rb @@ -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 diff --git a/lib/couchrest/casted_array.rb b/lib/couchrest/model/casted_array.rb similarity index 97% rename from lib/couchrest/casted_array.rb rename to lib/couchrest/model/casted_array.rb index 203c05d..a52f152 100644 --- a/lib/couchrest/casted_array.rb +++ b/lib/couchrest/model/casted_array.rb @@ -3,7 +3,7 @@ # elements of the array. # -module CouchRest +module CouchRest::Model class CastedArray < Array attr_accessor :casted_by attr_accessor :property diff --git a/lib/couchrest/casted_model.rb b/lib/couchrest/model/casted_model.rb similarity index 72% rename from lib/couchrest/casted_model.rb rename to lib/couchrest/model/casted_model.rb index cbe2638..6aa9bce 100644 --- a/lib/couchrest/casted_model.rb +++ b/lib/couchrest/model/casted_model.rb @@ -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 = {}) diff --git a/lib/couchrest/mixins/class_proxy.rb b/lib/couchrest/model/class_proxy.rb similarity index 89% rename from lib/couchrest/mixins/class_proxy.rb rename to lib/couchrest/model/class_proxy.rb index f6285d8..36200f9 100644 --- a/lib/couchrest/mixins/class_proxy.rb +++ b/lib/couchrest/model/class_proxy.rb @@ -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 diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/model/collection.rb similarity index 99% rename from lib/couchrest/mixins/collection.rb rename to lib/couchrest/model/collection.rb index b6bc02b..17910e7 100644 --- a/lib/couchrest/mixins/collection.rb +++ b/lib/couchrest/model/collection.rb @@ -1,5 +1,5 @@ module CouchRest - module Mixins + module Model module Collection def self.included(base) diff --git a/lib/couchrest/mixins/design_doc.rb b/lib/couchrest/model/design_doc.rb similarity index 98% rename from lib/couchrest/mixins/design_doc.rb rename to lib/couchrest/model/design_doc.rb index 47f3d5a..8cdc8a7 100644 --- a/lib/couchrest/mixins/design_doc.rb +++ b/lib/couchrest/model/design_doc.rb @@ -1,7 +1,6 @@ -require 'digest/md5' - +# encoding: utf-8 module CouchRest - module Mixins + module Model module DesignDoc def self.included(base) diff --git a/lib/couchrest/mixins/document_queries.rb b/lib/couchrest/model/document_queries.rb similarity index 99% rename from lib/couchrest/mixins/document_queries.rb rename to lib/couchrest/model/document_queries.rb index e40ccb6..eb3e7da 100644 --- a/lib/couchrest/mixins/document_queries.rb +++ b/lib/couchrest/model/document_queries.rb @@ -1,5 +1,5 @@ module CouchRest - module Mixins + module Model module DocumentQueries def self.included(base) diff --git a/lib/couchrest/model/errors.rb b/lib/couchrest/model/errors.rb new file mode 100644 index 0000000..7f14fca --- /dev/null +++ b/lib/couchrest/model/errors.rb @@ -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: + # + # Validations.new(person.errors) + class Validations < CouchRestModelError + attr_reader :document + def initialize(document) + @document = document + super("Validation Failed: #{@document.errors.full_messages.join(", ")}") + end + end + end + end +end diff --git a/lib/couchrest/mixins/extended_attachments.rb b/lib/couchrest/model/extended_attachments.rb similarity index 99% rename from lib/couchrest/mixins/extended_attachments.rb rename to lib/couchrest/model/extended_attachments.rb index 0f928bd..8d1e6ed 100644 --- a/lib/couchrest/mixins/extended_attachments.rb +++ b/lib/couchrest/model/extended_attachments.rb @@ -1,5 +1,5 @@ module CouchRest - module Mixins + module Model module ExtendedAttachments # Add a file attachment to the current document. Expects diff --git a/lib/couchrest/model/persistence.rb b/lib/couchrest/model/persistence.rb new file mode 100644 index 0000000..dea3e45 --- /dev/null +++ b/lib/couchrest/model/persistence.rb @@ -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 _id and _rev fields, preparing the + # document to be saved to a new _id 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 _id. + # + # Because CouchDB enforces a constraint that each id must be unique, + # this can be used to enforce eg: uniq usernames. Note that this id + # must be globally unique across all document types which share a + # database, so if you'd like to scope uniqueness to this class, you + # should use the class name as part of the unique id. + def unique_id method = nil, &block + if method + define_method :set_unique_id do + self['_id'] ||= self.send(method) + end + elsif block + define_method :set_unique_id do + uniqid = block.call(self) + raise ArgumentError, "unique_id block must not return nil" if uniqid.nil? + self['_id'] ||= uniqid + end + end + end + + # Raise an error if validation failed. + def fail_validate!(document) + raise Errors::Validations.new(document) + end + end + + + end + end +end diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/model/properties.rb similarity index 79% rename from lib/couchrest/mixins/properties.rb rename to lib/couchrest/model/properties.rb index 44fd1cf..ebc784d 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/model/properties.rb @@ -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 updated_at and created_at fields + # on the document whenever saving occurs. CouchRest uses a pretty + # decent time format by default. See Time#to_json + def timestamps! + 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 diff --git a/lib/couchrest/property.rb b/lib/couchrest/model/property.rb similarity index 74% rename from lib/couchrest/property.rb rename to lib/couchrest/model/property.rb index 1804901..fd2e104 100644 --- a/lib/couchrest/property.rb +++ b/lib/couchrest/model/property.rb @@ -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 diff --git a/lib/couchrest/support/couchrest.rb b/lib/couchrest/model/support/couchrest.rb similarity index 50% rename from lib/couchrest/support/couchrest.rb rename to lib/couchrest/model/support/couchrest.rb index e795556..6fde806 100644 --- a/lib/couchrest/support/couchrest.rb +++ b/lib/couchrest/model/support/couchrest.rb @@ -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 diff --git a/lib/couchrest/support/rails.rb b/lib/couchrest/model/support/rails.rb similarity index 65% rename from lib/couchrest/support/rails.rb rename to lib/couchrest/model/support/rails.rb index 997c2c6..f14d2da 100644 --- a/lib/couchrest/support/rails.rb +++ b/lib/couchrest/model/support/rails.rb @@ -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 diff --git a/lib/couchrest/mixins/typecast.rb b/lib/couchrest/model/typecast.rb similarity index 98% rename from lib/couchrest/mixins/typecast.rb rename to lib/couchrest/model/typecast.rb index da7c6db..c4aa934 100644 --- a/lib/couchrest/mixins/typecast.rb +++ b/lib/couchrest/model/typecast.rb @@ -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) diff --git a/lib/couchrest/model/validations.rb b/lib/couchrest/model/validations.rb new file mode 100644 index 0000000..20c95c2 --- /dev/null +++ b/lib/couchrest/model/validations.rb @@ -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 diff --git a/lib/couchrest/model/validations/casted_model.rb b/lib/couchrest/model/validations/casted_model.rb new file mode 100644 index 0000000..b4e89e6 --- /dev/null +++ b/lib/couchrest/model/validations/casted_model.rb @@ -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 diff --git a/lib/couchrest/mixins/views.rb b/lib/couchrest/model/views.rb similarity index 98% rename from lib/couchrest/mixins/views.rb rename to lib/couchrest/model/views.rb index f4b55a8..8e2e0d7 100644 --- a/lib/couchrest/mixins/views.rb +++ b/lib/couchrest/model/views.rb @@ -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 diff --git a/lib/couchrest/validation.rb b/lib/couchrest/validation.rb deleted file mode 100644 index b0b7604..0000000 --- a/lib/couchrest/validation.rb +++ /dev/null @@ -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_? 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 diff --git a/lib/couchrest/validation/auto_validate.rb b/lib/couchrest/validation/auto_validate.rb deleted file mode 100644 index 06e8070..0000000 --- a/lib/couchrest/validation/auto_validate.rb +++ /dev/null @@ -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 diff --git a/lib/couchrest/validation/contextual_validators.rb b/lib/couchrest/validation/contextual_validators.rb deleted file mode 100644 index 9f2304c..0000000 --- a/lib/couchrest/validation/contextual_validators.rb +++ /dev/null @@ -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 a hash of validators - def contexts - @contexts ||= {} - end - - # Return an array of validators for a named context - # - # @return 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 named_context the context we are validating against - # @param target the resource that we are validating - # @return 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 diff --git a/lib/couchrest/validation/validation_errors.rb b/lib/couchrest/validation/validation_errors.rb deleted file mode 100644 index a1ce372..0000000 --- a/lib/couchrest/validation/validation_errors.rb +++ /dev/null @@ -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 field_name the name of the field that caused the error - # @param 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 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 diff --git a/lib/couchrest/validation/validators/absent_field_validator.rb b/lib/couchrest/validation/validators/absent_field_validator.rb deleted file mode 100644 index e2b7f55..0000000 --- a/lib/couchrest/validation/validators/absent_field_validator.rb +++ /dev/null @@ -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 diff --git a/lib/couchrest/validation/validators/confirmation_validator.rb b/lib/couchrest/validation/validators/confirmation_validator.rb deleted file mode 100644 index 2a5c587..0000000 --- a/lib/couchrest/validation/validators/confirmation_validator.rb +++ /dev/null @@ -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 true/false (default is true) - # @option :confirm 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 diff --git a/lib/couchrest/validation/validators/format_validator.rb b/lib/couchrest/validation/validators/format_validator.rb deleted file mode 100644 index f15c7eb..0000000 --- a/lib/couchrest/validation/validators/format_validator.rb +++ /dev/null @@ -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 true/false (default is true) - # @option :as the pre-defined format, Proc or Regexp to validate against - # @option :with 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 diff --git a/lib/couchrest/validation/validators/formats/email.rb b/lib/couchrest/validation/validators/formats/email.rb deleted file mode 100644 index 22006a7..0000000 --- a/lib/couchrest/validation/validators/formats/email.rb +++ /dev/null @@ -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 diff --git a/lib/couchrest/validation/validators/formats/url.rb b/lib/couchrest/validation/validators/formats/url.rb deleted file mode 100644 index 675f0c4..0000000 --- a/lib/couchrest/validation/validators/formats/url.rb +++ /dev/null @@ -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 \ No newline at end of file diff --git a/lib/couchrest/validation/validators/generic_validator.rb b/lib/couchrest/validation/validators/generic_validator.rb deleted file mode 100644 index f353cf6..0000000 --- a/lib/couchrest/validation/validators/generic_validator.rb +++ /dev/null @@ -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 The property specified for validation - # - # @option :if The name of a method or a Proc to call to - # determine if the validation should occur. - # @option :unless 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 target the resource that has the error - # @param message the message to add - # @param 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 target the resource that the validator must be called - # against - # @return 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 target the resource that we check against - # @return 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 diff --git a/lib/couchrest/validation/validators/length_validator.rb b/lib/couchrest/validation/validators/length_validator.rb deleted file mode 100644 index 04c3ce0..0000000 --- a/lib/couchrest/validation/validators/length_validator.rb +++ /dev/null @@ -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 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 given a Range, ensures that the attributes length is - # include?'ed in the Range - # @option :within 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 diff --git a/lib/couchrest/validation/validators/method_validator.rb b/lib/couchrest/validation/validators/method_validator.rb deleted file mode 100644 index d393fc9..0000000 --- a/lib/couchrest/validation/validators/method_validator.rb +++ /dev/null @@ -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::, Error Message::] - # - # @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 diff --git a/lib/couchrest/validation/validators/numeric_validator.rb b/lib/couchrest/validation/validators/numeric_validator.rb deleted file mode 100644 index a27e274..0000000 --- a/lib/couchrest/validation/validators/numeric_validator.rb +++ /dev/null @@ -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 diff --git a/lib/couchrest/validation/validators/required_field_validator.rb b/lib/couchrest/validation/validators/required_field_validator.rb deleted file mode 100644 index 0c64ccf..0000000 --- a/lib/couchrest/validation/validators/required_field_validator.rb +++ /dev/null @@ -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 diff --git a/lib/couchrest_extended_document.rb b/lib/couchrest_extended_document.rb deleted file mode 100644 index 2ac68cc..0000000 --- a/lib/couchrest_extended_document.rb +++ /dev/null @@ -1,22 +0,0 @@ - -# require File.join(File.dirname(__FILE__), "couchrest", "extended_document") - -gem 'couchrest' - -require 'couchrest' - -require 'active_support/core_ext' -require 'active_support/json' -require 'mime/types' -require "enumerator" - -# Monkey patches applied to couchrest -require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'couchrest') - -# Base libraries -require File.join(File.dirname(__FILE__), 'couchrest', 'extended_document') -require File.join(File.dirname(__FILE__), 'couchrest', 'casted_model') - -# Add rails support *after* everything has loaded -require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'rails') if defined?(Rails) - diff --git a/lib/couchrest_model.rb b/lib/couchrest_model.rb new file mode 100644 index 0000000..0bd2083 --- /dev/null +++ b/lib/couchrest_model.rb @@ -0,0 +1,56 @@ + +# require File.join(File.dirname(__FILE__), "couchrest", "extended_document") + +gem 'couchrest' + +require 'couchrest' + +require 'active_support/core_ext' +require 'active_support/json' +require 'active_model' +require "active_model/callbacks" +require "active_model/conversion" +require "active_model/deprecated_error_methods" +require "active_model/errors" +require "active_model/naming" +require "active_model/serialization" +require "active_model/translation" +require "active_model/validator" +require "active_model/validations" +require 'mime/types' +require "enumerator" +require "time" +require 'digest/md5' + +require 'bigdecimal' # used in typecast +require 'bigdecimal/util' # used in typecast + +require 'couchrest/model' +require 'couchrest/model/errors' +require "couchrest/model/persistence" +require "couchrest/model/typecast" +require "couchrest/model/property" +require "couchrest/model/casted_array" +require "couchrest/model/properties" +require "couchrest/model/validations" +require "couchrest/model/callbacks" +require "couchrest/model/document_queries" +require "couchrest/model/views" +require "couchrest/model/design_doc" +require "couchrest/model/extended_attachments" +require "couchrest/model/class_proxy" +require "couchrest/model/collection" +require "couchrest/model/attribute_protection" +require "couchrest/model/attributes" +require "couchrest/model/associations" + +# Monkey patches applied to couchrest +require "couchrest/model/support/couchrest" + +# Base libraries +require "couchrest/model/casted_model" +require "couchrest/model/base" + +# Add rails support *after* everything has loaded +require "couchrest/model/support/rails" if defined?(Rails) + diff --git a/spec/couchrest/assocations_spec.rb b/spec/couchrest/assocations_spec.rb index 853757a..e83e24a 100644 --- a/spec/couchrest/assocations_spec.rb +++ b/spec/couchrest/assocations_spec.rb @@ -1,21 +1,21 @@ # encoding: utf-8 require File.expand_path('../../spec_helper', __FILE__) -class Client < CouchRest::ExtendedDocument +class Client < CouchRest::Model::Base use_database DB property :name property :tax_code end -class SaleEntry < CouchRest::ExtendedDocument +class SaleEntry < CouchRest::Model::Base use_database DB property :description property :price end -class SaleInvoice < CouchRest::ExtendedDocument +class SaleInvoice < CouchRest::Model::Base use_database DB belongs_to :client @@ -64,7 +64,7 @@ describe "Assocations" do it "should raise error if class name does not exist" do lambda { - class TestBadAssoc < CouchRest::ExtendedDocument + class TestBadAssoc < CouchRest::Model::Base belongs_to :test_bad_item end }.should raise_error @@ -99,7 +99,7 @@ describe "Assocations" do it "should create an associated property and collection proxy" do @invoice.respond_to?('entry_ids') @invoice.respond_to?('entry_ids=') - @invoice.entries.class.should eql(::CouchRest::CollectionProxy) + @invoice.entries.class.should eql(::CouchRest::CollectionOfProxy) end it "should allow replacement of objects" do diff --git a/spec/couchrest/extended_doc_attachment_spec.rb b/spec/couchrest/attachment_spec.rb similarity index 95% rename from spec/couchrest/extended_doc_attachment_spec.rb rename to spec/couchrest/attachment_spec.rb index 30f94dd..75331e1 100644 --- a/spec/couchrest/extended_doc_attachment_spec.rb +++ b/spec/couchrest/attachment_spec.rb @@ -1,12 +1,12 @@ require File.expand_path('../../spec_helper', __FILE__) -describe "ExtendedDocument attachments" do +describe "Model attachments" do describe "#has_attachment?" do before(:each) do reset_test_db! @obj = Basic.new - @obj.save.should == true + @obj.save.should be_true @file = File.open(FIXTURE_PATH + '/attachments/test.html') @attachment_name = 'my_attachment' @obj.create_attachment(:file => @file, :name => @attachment_name) @@ -35,7 +35,7 @@ describe "ExtendedDocument attachments" do describe "creating an attachment" do before(:each) do @obj = Basic.new - @obj.save.should == true + @obj.save.should be_true @file_ext = File.open(FIXTURE_PATH + '/attachments/test.html') @file_no_ext = File.open(FIXTURE_PATH + '/attachments/README') @attachment_name = 'my_attachment' @@ -44,14 +44,14 @@ describe "ExtendedDocument attachments" do it "should create an attachment from file with an extension" do @obj.create_attachment(:file => @file_ext, :name => @attachment_name) - @obj.save.should == true + @obj.save.should be_true reloaded_obj = Basic.get(@obj.id) reloaded_obj['_attachments'][@attachment_name].should_not be_nil end it "should create an attachment from file without an extension" do @obj.create_attachment(:file => @file_no_ext, :name => @attachment_name) - @obj.save.should == true + @obj.save.should be_true reloaded_obj = Basic.get(@obj.id) reloaded_obj['_attachments'][@attachment_name].should_not be_nil end @@ -89,7 +89,7 @@ describe "ExtendedDocument attachments" do @file = File.open(FIXTURE_PATH + '/attachments/test.html') @attachment_name = 'my_attachment' @obj.create_attachment(:file => @file, :name => @attachment_name) - @obj.save.should == true + @obj.save.should be_true @file.rewind @content_type = 'media/mp3' end @@ -129,7 +129,7 @@ describe "ExtendedDocument attachments" do @file = File.open(FIXTURE_PATH + '/attachments/test.html') @attachment_name = 'my_attachment' @obj.create_attachment(:file => @file, :name => @attachment_name) - @obj.save.should == true + @obj.save.should be_true end it 'should return nil if attachment does not exist' do diff --git a/spec/couchrest/attribute_protection_spec.rb b/spec/couchrest/attribute_protection_spec.rb index d1a23b4..c75ff57 100644 --- a/spec/couchrest/attribute_protection_spec.rb +++ b/spec/couchrest/attribute_protection_spec.rb @@ -1,150 +1,153 @@ require File.expand_path("../../spec_helper", __FILE__) -describe "ExtendedDocument", "no declarations" do - class NoProtection < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - property :name - property :phone - end +describe "Model Attributes" do - it "should not protect anything through new" do - user = NoProtection.new(:name => "will", :phone => "555-5555") + describe "no declarations" do + class NoProtection < CouchRest::Model::Base + use_database TEST_SERVER.default_database + property :name + property :phone + end - user.name.should == "will" - user.phone.should == "555-5555" - end + it "should not protect anything through new" do + user = NoProtection.new(:name => "will", :phone => "555-5555") - it "should not protect anything through attributes=" do - user = NoProtection.new - user.attributes = {:name => "will", :phone => "555-5555"} + user.name.should == "will" + user.phone.should == "555-5555" + end - user.name.should == "will" - user.phone.should == "555-5555" - end - - it "should recreate from the database properly" do - user = NoProtection.new - user.name = "will" - user.phone = "555-5555" - user.save! + it "should not protect anything through attributes=" do + user = NoProtection.new + user.attributes = {:name => "will", :phone => "555-5555"} + + user.name.should == "will" + user.phone.should == "555-5555" + end - user = NoProtection.get(user.id) - user.name.should == "will" - user.phone.should == "555-5555" - end -end - -describe "ExtendedDocument", "accessible flag" do - class WithAccessible < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - property :name, :accessible => true - property :admin, :default => false - end - - it "should recognize accessible properties" do - props = WithAccessible.accessible_properties.map { |prop| prop.name} - props.should include("name") - props.should_not include("admin") - end - - it "should protect non-accessible properties set through new" do - user = WithAccessible.new(:name => "will", :admin => true) - - user.name.should == "will" - user.admin.should == false - end - - it "should protect non-accessible properties set through attributes=" do - user = WithAccessible.new - user.attributes = {:name => "will", :admin => true} - - user.name.should == "will" - user.admin.should == false - end -end - -describe "ExtendedDocument", "protected flag" do - class WithProtected < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - property :name - property :admin, :default => false, :protected => true - end - - it "should recognize protected properties" do - props = WithProtected.protected_properties.map { |prop| prop.name} - props.should_not include("name") - props.should include("admin") - end - - it "should protect non-accessible properties set through new" do - user = WithProtected.new(:name => "will", :admin => true) - - user.name.should == "will" - user.admin.should == false - end - - it "should protect non-accessible properties set through attributes=" do - user = WithProtected.new - user.attributes = {:name => "will", :admin => true} - - user.name.should == "will" - user.admin.should == false - end -end - -describe "ExtendedDocument", "protected flag" do - class WithBoth < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - property :name, :accessible => true - property :admin, :default => false, :protected => true - end - - it "should raise an error when both are set" do - lambda { WithBoth.new }.should raise_error - end -end - -describe "ExtendedDocument", "from database" do - class WithProtected < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - property :name - property :admin, :default => false, :protected => true - view_by :name - end - - before(:each) do - @user = WithProtected.new - @user.name = "will" - @user.admin = true - @user.save! - end - - def verify_attrs(user) - user.name.should == "will" - user.admin.should == true - end - - it "ExtendedDocument#get should not strip protected attributes" do - reloaded = WithProtected.get( @user.id ) - verify_attrs reloaded - end - - it "ExtendedDocument#get! should not strip protected attributes" do - reloaded = WithProtected.get!( @user.id ) - verify_attrs reloaded - end - - it "ExtendedDocument#all should not strip protected attributes" do - # all creates a CollectionProxy - docs = WithProtected.all(:key => @user.id) - docs.size.should == 1 - reloaded = docs.first - verify_attrs reloaded - end - - it "views should not strip protected attributes" do - docs = WithProtected.by_name(:startkey => "will", :endkey => "will") - reloaded = docs.first - verify_attrs reloaded + it "should recreate from the database properly" do + user = NoProtection.new + user.name = "will" + user.phone = "555-5555" + user.save! + + user = NoProtection.get(user.id) + user.name.should == "will" + user.phone.should == "555-5555" + end + end + + describe "Model Base", "accessible flag" do + class WithAccessible < CouchRest::Model::Base + use_database TEST_SERVER.default_database + property :name, :accessible => true + property :admin, :default => false + end + + it "should recognize accessible properties" do + props = WithAccessible.accessible_properties.map { |prop| prop.name} + props.should include("name") + props.should_not include("admin") + end + + it "should protect non-accessible properties set through new" do + user = WithAccessible.new(:name => "will", :admin => true) + + user.name.should == "will" + user.admin.should == false + end + + it "should protect non-accessible properties set through attributes=" do + user = WithAccessible.new + user.attributes = {:name => "will", :admin => true} + + user.name.should == "will" + user.admin.should == false + end + end + + describe "Model Base", "protected flag" do + class WithProtected < CouchRest::Model::Base + use_database TEST_SERVER.default_database + property :name + property :admin, :default => false, :protected => true + end + + it "should recognize protected properties" do + props = WithProtected.protected_properties.map { |prop| prop.name} + props.should_not include("name") + props.should include("admin") + end + + it "should protect non-accessible properties set through new" do + user = WithProtected.new(:name => "will", :admin => true) + + user.name.should == "will" + user.admin.should == false + end + + it "should protect non-accessible properties set through attributes=" do + user = WithProtected.new + user.attributes = {:name => "will", :admin => true} + + user.name.should == "will" + user.admin.should == false + end + end + + describe "protected flag" do + class WithBoth < CouchRest::Model::Base + use_database TEST_SERVER.default_database + property :name, :accessible => true + property :admin, :default => false, :protected => true + end + + it "should raise an error when both are set" do + lambda { WithBoth.new }.should raise_error + end + end + + describe "from database" do + class WithProtected < CouchRest::Model::Base + use_database TEST_SERVER.default_database + property :name + property :admin, :default => false, :protected => true + view_by :name + end + + before(:each) do + @user = WithProtected.new + @user.name = "will" + @user.admin = true + @user.save! + end + + def verify_attrs(user) + user.name.should == "will" + user.admin.should == true + end + + it "Base#get should not strip protected attributes" do + reloaded = WithProtected.get( @user.id ) + verify_attrs reloaded + end + + it "Base#get! should not strip protected attributes" do + reloaded = WithProtected.get!( @user.id ) + verify_attrs reloaded + end + + it "Base#all should not strip protected attributes" do + # all creates a CollectionProxy + docs = WithProtected.all(:key => @user.id) + docs.size.should == 1 + reloaded = docs.first + verify_attrs reloaded + end + + it "views should not strip protected attributes" do + docs = WithProtected.by_name(:startkey => "will", :endkey => "will") + reloaded = docs.first + verify_attrs reloaded + end end end diff --git a/spec/couchrest/base_spec.rb b/spec/couchrest/base_spec.rb new file mode 100644 index 0000000..ecee222 --- /dev/null +++ b/spec/couchrest/base_spec.rb @@ -0,0 +1,403 @@ +# encoding: utf-8 + +require File.expand_path("../../spec_helper", __FILE__) +require File.join(FIXTURE_PATH, 'more', 'cat') +require File.join(FIXTURE_PATH, 'more', 'article') +require File.join(FIXTURE_PATH, 'more', 'course') +require File.join(FIXTURE_PATH, 'more', 'card') +require File.join(FIXTURE_PATH, 'base') + +describe "Model Base" do + + before(:each) do + @obj = WithDefaultValues.new + end + + describe "instance database connection" do + it "should use the default database" do + @obj.database.name.should == 'couchrest-model-test' + end + + it "should override the default db" do + @obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test') + @obj.database.name.should == 'couchrest-extendedmodel-test' + @obj.database.delete! + end + end + + describe "a new model" do + it "should be a new document" do + @obj = Basic.new + @obj.rev.should be_nil + @obj.should be_new + @obj.should be_new_document + @obj.should be_new_record + end + + it "should not failed on a nil value in argument" do + @obj = Basic.new(nil) + @obj.should == { 'couchrest-type' => 'Basic' } + end + end + + + describe "update attributes without saving" do + before(:each) do + a = Article.get "big-bad-danger" rescue nil + a.destroy if a + @art = Article.new(:title => "big bad danger") + @art.save + end + it "should work for attribute= methods" do + @art['title'].should == "big bad danger" + @art.update_attributes_without_saving('date' => Time.now, :title => "super danger") + @art['title'].should == "super danger" + end + it "should silently ignore _id" do + @art.update_attributes_without_saving('_id' => 'foobar') + @art['_id'].should_not == 'foobar' + end + it "should silently ignore _rev" do + @art.update_attributes_without_saving('_rev' => 'foobar') + @art['_rev'].should_not == 'foobar' + end + it "should silently ignore created_at" do + @art.update_attributes_without_saving('created_at' => 'foobar') + @art['created_at'].should_not == 'foobar' + end + it "should silently ignore updated_at" do + @art.update_attributes_without_saving('updated_at' => 'foobar') + @art['updated_at'].should_not == 'foobar' + end + it "should also work using attributes= alias" do + @art.respond_to?(:attributes=).should be_true + @art.attributes = {'date' => Time.now, :title => "something else"} + @art['title'].should == "something else" + end + + it "should not flip out if an attribute= method is missing and ignore it" do + lambda { + @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") + }.should_not raise_error + @art.slug.should == "big-bad-danger" + end + + #it "should not change other attributes if there is an error" do + # lambda { + # @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") + # }.should raise_error + # @art['title'].should == "big bad danger" + #end + end + + describe "update attributes" do + before(:each) do + a = Article.get "big-bad-danger" rescue nil + a.destroy if a + @art = Article.new(:title => "big bad danger") + @art.save + end + it "should save" do + @art['title'].should == "big bad danger" + @art.update_attributes('date' => Time.now, :title => "super danger") + loaded = Article.get(@art.id) + loaded['title'].should == "super danger" + end + end + + describe "with default" do + it "should have the default value set at initalization" do + @obj.preset.should == {:right => 10, :top_align => false} + end + + it "should have the default false value explicitly assigned" do + @obj.default_false.should == false + end + + it "should automatically call a proc default at initialization" do + @obj.set_by_proc.should be_an_instance_of(Time) + @obj.set_by_proc.should == @obj.set_by_proc + @obj.set_by_proc.should < Time.now + end + + it "should let you overwrite the default values" do + obj = WithDefaultValues.new(:preset => 'test') + obj.preset = 'test' + end + + it "should work with a default empty array" do + obj = WithDefaultValues.new(:tags => ['spec']) + obj.tags.should == ['spec'] + end + + it "should set default value of read-only property" do + obj = WithDefaultValues.new + obj.read_only_with_default.should == 'generic' + end + end + + describe "simplified way of setting property types" do + it "should set defaults" do + obj = WithSimplePropertyType.new + obj.preset.should eql('none') + end + + it "should handle arrays" do + obj = WithSimplePropertyType.new(:tags => ['spec']) + obj.tags.should == ['spec'] + end + end + + describe "a doc with template values (CR::Model spec)" do + before(:all) do + WithTemplateAndUniqueID.all.map{|o| o.destroy(true)} + WithTemplateAndUniqueID.database.bulk_delete + @tmpl = WithTemplateAndUniqueID.new + @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'important-field' => '1') + end + it "should have fields set when new" do + @tmpl.preset.should == 'value' + end + it "shouldn't override explicitly set values" do + @tmpl2.preset.should == 'not_value' + end + it "shouldn't override existing documents" do + @tmpl2.save + tmpl2_reloaded = WithTemplateAndUniqueID.get(@tmpl2.id) + @tmpl2.preset.should == 'not_value' + tmpl2_reloaded.preset.should == 'not_value' + end + end + + + describe "finding all instances of a model" do + before(:all) do + WithTemplateAndUniqueID.req_design_doc_refresh + WithTemplateAndUniqueID.all.map{|o| o.destroy(true)} + WithTemplateAndUniqueID.database.bulk_delete + WithTemplateAndUniqueID.new('important-field' => '1').save + WithTemplateAndUniqueID.new('important-field' => '2').save + WithTemplateAndUniqueID.new('important-field' => '3').save + WithTemplateAndUniqueID.new('important-field' => '4').save + end + it "should make the design doc" do + WithTemplateAndUniqueID.all + d = WithTemplateAndUniqueID.design_doc + d['views']['all']['map'].should include('WithTemplateAndUniqueID') + end + it "should find all" do + rs = WithTemplateAndUniqueID.all + rs.length.should == 4 + end + end + + describe "counting all instances of a model" do + before(:each) do + @db = reset_test_db! + WithTemplateAndUniqueID.req_design_doc_refresh + end + + it ".count should return 0 if there are no docuemtns" do + WithTemplateAndUniqueID.count.should == 0 + end + + it ".count should return the number of documents" do + WithTemplateAndUniqueID.new('important-field' => '1').save + WithTemplateAndUniqueID.new('important-field' => '2').save + WithTemplateAndUniqueID.new('important-field' => '3').save + + WithTemplateAndUniqueID.count.should == 3 + end + end + + describe "finding the first instance of a model" do + before(:each) do + @db = reset_test_db! + # WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically + WithTemplateAndUniqueID.new('important-field' => '1').save + WithTemplateAndUniqueID.new('important-field' => '2').save + WithTemplateAndUniqueID.new('important-field' => '3').save + WithTemplateAndUniqueID.new('important-field' => '4').save + end + it "should make the design doc" do + WithTemplateAndUniqueID.all + d = WithTemplateAndUniqueID.design_doc + d['views']['all']['map'].should include('WithTemplateAndUniqueID') + end + it "should find first" do + rs = WithTemplateAndUniqueID.first + rs['important-field'].should == "1" + end + it "should return nil if no instances are found" do + WithTemplateAndUniqueID.all.each {|obj| obj.destroy } + WithTemplateAndUniqueID.first.should be_nil + end + end + + describe "lazily refreshing the design document" do + before(:all) do + @db = reset_test_db! + WithTemplateAndUniqueID.new('important-field' => '1').save + end + it "should not save the design doc twice" do + WithTemplateAndUniqueID.all + WithTemplateAndUniqueID.req_design_doc_refresh + WithTemplateAndUniqueID.refresh_design_doc + rev = WithTemplateAndUniqueID.design_doc['_rev'] + WithTemplateAndUniqueID.req_design_doc_refresh + WithTemplateAndUniqueID.refresh_design_doc + WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev) + end + end + + describe "getting a model with a subobject field" do + before(:all) do + course_doc = { + "title" => "Metaphysics 410", + "professor" => { + "name" => ["Mark", "Hinchliff"] + }, + "ends_at" => "2008/12/19 13:00:00 +0800" + } + r = Course.database.save_doc course_doc + @course = Course.get r['id'] + end + it "should load the course" do + @course["professor"]["name"][1].should == "Hinchliff" + end + it "should instantiate the professor as a person" do + @course['professor'].last_name.should == "Hinchliff" + end + it "should instantiate the ends_at as a Time" do + @course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800") + end + end + + describe "timestamping" do + before(:each) do + oldart = Article.get "saving-this" rescue nil + oldart.destroy if oldart + @art = Article.new(:title => "Saving this") + @art.save + end + + it "should define the updated_at and created_at getters and set the values" do + @obj.save + obj = WithDefaultValues.get(@obj.id) + obj.should be_an_instance_of(WithDefaultValues) + obj.created_at.should be_an_instance_of(Time) + obj.updated_at.should be_an_instance_of(Time) + obj.created_at.to_s.should == @obj.updated_at.to_s + end + + it "should not change created_at on update" do + 2.times do + lambda do + @art.save + end.should_not change(@art, :created_at) + end + end + + it "should set the time on create" do + (Time.now - @art.created_at).should < 2 + foundart = Article.get @art.id + foundart.created_at.should == foundart.updated_at + end + it "should set the time on update" do + @art.save + @art.created_at.should < @art.updated_at + end + end + + describe "getter and setter methods" do + it "should try to call the arg= method before setting :arg in the hash" do + @doc = WithGetterAndSetterMethods.new(:arg => "foo") + @doc['arg'].should be_nil + @doc[:arg].should be_nil + @doc.other_arg.should == "foo-foo" + end + end + + describe "initialization" do + it "should call after_initialize method if available" do + @doc = WithAfterInitializeMethod.new + @doc['some_value'].should eql('value') + end + end + + describe "recursive validation on a model" do + before :each do + reset_test_db! + @cat = Cat.new(:name => 'Sockington') + end + + it "should not save if a nested casted model is invalid" do + @cat.favorite_toy = CatToy.new + @cat.should_not be_valid + @cat.save.should be_false + lambda{@cat.save!}.should raise_error + end + + it "should save when nested casted model is valid" do + @cat.favorite_toy = CatToy.new(:name => 'Squeaky') + @cat.should be_valid + @cat.save.should be_true + lambda{@cat.save!}.should_not raise_error + end + + it "should not save when nested collection contains an invalid casted model" do + @cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new] + @cat.should_not be_valid + @cat.save.should be_false + lambda{@cat.save!}.should raise_error + end + + it "should save when nested collection contains valid casted models" do + @cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')] + @cat.should be_valid + @cat.save.should be_true + lambda{@cat.save!}.should_not raise_error + end + + it "should not fail if the nested casted model doesn't have validation" do + Cat.property :trainer, Person + Cat.validates_presence_of :name + cat = Cat.new(:name => 'Mr Bigglesworth') + cat.trainer = Person.new + cat.should be_valid + cat.save.should be_true + end + end + + describe "searching the contents of an extended document" do + before :each do + @db = reset_test_db! + + names = ["Fuzzy", "Whiskers", "Mr Bigglesworth", "Sockington", "Smitty", "Sammy", "Samson", "Simon"] + names.each { |name| Cat.create(:name => name) } + + search_function = { 'defaults' => {'store' => 'no', 'index' => 'analyzed_no_norms'}, + 'index' => "function(doc) { ret = new Document(); ret.add(doc['name'], {'field':'name'}); return ret; }" } + @db.save_doc({'_id' => '_design/search', 'fulltext' => {'cats' => search_function}}) + end + + it "should be able to paginate through a large set of search results" do + if couchdb_lucene_available? + names = [] + Cat.paginated_each(:design_doc => "search", :view_name => "cats", + :q => 'name:S*', :search => true, :include_docs => true, :per_page => 3) do |cat| + cat.should_not be_nil + names << cat.name + end + + names.size.should == 5 + names.should include('Sockington') + names.should include('Smitty') + names.should include('Sammy') + names.should include('Samson') + names.should include('Simon') + end + end + end + +end diff --git a/spec/couchrest/casted_model_spec.rb b/spec/couchrest/casted_model_spec.rb index debd3fe..32ef3e0 100644 --- a/spec/couchrest/casted_model_spec.rb +++ b/spec/couchrest/casted_model_spec.rb @@ -1,39 +1,33 @@ # encoding: utf-8 require File.expand_path('../../spec_helper', __FILE__) +require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'card') -require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'question') require File.join(FIXTURE_PATH, 'more', 'course') class WithCastedModelMixin < Hash - include CouchRest::CastedModel + include CouchRest::Model::CastedModel property :name property :no_value - property :details, :type => 'Object', :default => {} - property :casted_attribute, :cast_as => 'WithCastedModelMixin' + property :details, Object, :default => {} + property :casted_attribute, WithCastedModelMixin end -class DummyModel < CouchRest::ExtendedDocument +class DummyModel < CouchRest::Model::Base use_database TEST_SERVER.default_database raise "Default DB not set" if TEST_SERVER.default_database.nil? property :casted_attribute, WithCastedModelMixin - property :keywords, ["String"] + property :keywords, [String] property :sub_models do |child| child.property :title end end -class CastedCallbackDoc < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - raise "Default DB not set" if TEST_SERVER.default_database.nil? - property :callback_model, :cast_as => 'WithCastedCallBackModel' -end class WithCastedCallBackModel < Hash - include CouchRest::CastedModel - include CouchRest::Validation + include CouchRest::Model::CastedModel property :name property :run_before_validate property :run_after_validate @@ -46,7 +40,13 @@ class WithCastedCallBackModel < Hash end end -describe CouchRest::CastedModel do +class CastedCallbackDoc < CouchRest::Model::Base + use_database TEST_SERVER.default_database + raise "Default DB not set" if TEST_SERVER.default_database.nil? + property :callback_model, WithCastedCallBackModel +end + +describe CouchRest::Model::CastedModel do describe "A non hash class including CastedModel" do it "should fail raising and include error" do @@ -227,7 +227,7 @@ describe CouchRest::CastedModel do @cat.toys.push(toy) @cat.save.should be_true @cat = Cat.get @cat.id - @cat.toys.class.should == CouchRest::CastedArray + @cat.toys.class.should == CouchRest::Model::CastedArray @cat.toys.first.class.should == CatToy @cat.toys.first.should === toy end @@ -240,7 +240,7 @@ describe CouchRest::CastedModel do end it "should not fail if the casted model doesn't have validation" do - Cat.property :masters, :cast_as => ['Person'], :default => [] + Cat.property :masters, [Person], :default => [] Cat.validates_presence_of :name cat = Cat.new(:name => 'kitty') cat.should be_valid diff --git a/spec/couchrest/casted_extended_doc_spec.rb b/spec/couchrest/casted_spec.rb similarity index 81% rename from spec/couchrest/casted_extended_doc_spec.rb rename to spec/couchrest/casted_spec.rb index dbfacd4..44e4231 100644 --- a/spec/couchrest/casted_extended_doc_spec.rb +++ b/spec/couchrest/casted_spec.rb @@ -3,7 +3,7 @@ require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'card') -class Driver < CouchRest::ExtendedDocument +class Driver < CouchRest::Model::Base use_database TEST_SERVER.default_database # You have to add a casted_by accessor if you want to reach a casted extended doc parent attr_accessor :casted_by @@ -11,12 +11,11 @@ class Driver < CouchRest::ExtendedDocument property :name end -class Car < CouchRest::ExtendedDocument +class Car < CouchRest::Model::Base use_database TEST_SERVER.default_database property :name - property :driver, :cast_as => 'Driver' - property :backseat_driver, :cast_as => Driver + property :driver, Driver end describe "casting an extended document" do @@ -24,17 +23,14 @@ describe "casting an extended document" do before(:each) do @driver = Driver.new(:name => 'Matt') @car = Car.new(:name => 'Renault 306', :driver => @driver) - @car2 = Car.new(:name => 'Renault 306', :backseat_driver => @driver.dup) end it "should retain all properties of the casted attribute" do @car.driver.should == @driver - @car2.backseat_driver.should == @driver end it "should let the casted document know who casted it" do @car.driver.casted_by.should == @car - @car2.backseat_driver.casted_by.should == @car2 end end @@ -61,7 +57,7 @@ describe "assigning a value to casted attribute after initializing an object" do end -describe "casting an extended document from parsed JSON" do +describe "casting a model from parsed JSON" do before(:each) do @driver = Driver.new(:name => 'Matt') diff --git a/spec/couchrest/class_proxy_spec.rb b/spec/couchrest/class_proxy_spec.rb index dc28c03..cba0e38 100644 --- a/spec/couchrest/class_proxy_spec.rb +++ b/spec/couchrest/class_proxy_spec.rb @@ -1,6 +1,6 @@ require File.expand_path("../../spec_helper", __FILE__) -class UnattachedDoc < CouchRest::ExtendedDocument +class UnattachedDoc < CouchRest::Model::Base # Note: no use_database here property :title property :questions diff --git a/spec/couchrest/extended_doc_spec.rb b/spec/couchrest/extended_doc_spec.rb deleted file mode 100644 index d602100..0000000 --- a/spec/couchrest/extended_doc_spec.rb +++ /dev/null @@ -1,869 +0,0 @@ -# encoding: utf-8 - -require File.expand_path("../../spec_helper", __FILE__) -require File.join(FIXTURE_PATH, 'more', 'article') -require File.join(FIXTURE_PATH, 'more', 'course') -require File.join(FIXTURE_PATH, 'more', 'card') -require File.join(FIXTURE_PATH, 'more', 'cat') - -describe "ExtendedDocument" do - - class WithDefaultValues < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - property :preset, :type => 'Object', :default => {:right => 10, :top_align => false} - property :set_by_proc, :default => Proc.new{Time.now}, :cast_as => 'Time' - property :tags, :type => ['String'], :default => [] - property :read_only_with_default, :default => 'generic', :read_only => true - property :default_false, :type => 'Boolean', :default => false - property :name - timestamps! - end - - class WithSimplePropertyType < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - property :name, String - property :preset, String, :default => 'none' - property :tags, [String] - timestamps! - end - - class WithCallBacks < CouchRest::ExtendedDocument - include ::CouchRest::Validation - use_database TEST_SERVER.default_database - property :name - property :run_before_validate - property :run_after_validate - property :run_before_save - property :run_after_save - property :run_before_create - property :run_after_create - property :run_before_update - property :run_after_update - - before_validate do |object| - object.run_before_validate = true - end - after_validate do |object| - object.run_after_validate = true - end - before_save do |object| - object.run_before_save = true - end - after_save do |object| - object.run_after_save = true - end - before_create do |object| - object.run_before_create = true - end - after_create do |object| - object.run_after_create = true - end - before_update do |object| - object.run_before_update = true - end - after_update do |object| - object.run_after_update = true - end - - property :run_one - property :run_two - property :run_three - - before_save :run_one_method, :run_two_method do |object| - object.run_three = true - end - def run_one_method - self.run_one = true - end - def run_two_method - self.run_two = true - end - - attr_accessor :run_it - property :conditional_one - property :conditional_two - - before_save :conditional_one_method, :conditional_two_method, :if => proc { self.run_it } - def conditional_one_method - self.conditional_one = true - end - def conditional_two_method - self.conditional_two = true - end - end - - class WithTemplateAndUniqueID < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - unique_id do |model| - model['important-field'] - end - property :preset, :default => 'value' - property :has_no_default - end - - class WithGetterAndSetterMethods < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - - property :other_arg - def arg - other_arg - end - - def arg=(value) - self.other_arg = "foo-#{value}" - end - end - - class WithAfterInitializeMethod < CouchRest::ExtendedDocument - use_database TEST_SERVER.default_database - - property :some_value - - def after_initialize - self.some_value ||= "value" - end - - end - - - before(:each) do - @obj = WithDefaultValues.new - end - - describe "instance database connection" do - it "should use the default database" do - @obj.database.name.should == 'couchrest-test' - end - - it "should override the default db" do - @obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test') - @obj.database.name.should == 'couchrest-extendedmodel-test' - @obj.database.delete! - end - end - - describe "a new model" do - it "should be a new document" do - @obj = Basic.new - @obj.rev.should be_nil - @obj.should be_new - @obj.should be_new_document - @obj.should be_new_record - end - - it "should not failed on a nil value in argument" do - @obj = Basic.new(nil) - @obj.should == { 'couchrest-type' => 'Basic' } - end - end - - describe "creating a new document" do - it "should instantialize and save a document" do - article = Article.create(:title => 'my test') - article.title.should == 'my test' - article.should_not be_new - end - - it "should trigger the create callbacks" do - doc = WithCallBacks.create(:name => 'my other test') - doc.run_before_create.should be_true - doc.run_after_create.should be_true - doc.run_before_save.should be_true - doc.run_after_save.should be_true - end - end - - describe "creating a new document from database" do - - it "should instantialize" do - doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'couchrest-type' => 'Article', 'name' => 'my test'}) - doc.class.should eql(Article) - end - - it "should instantialize of same class if no couchrest-type included from DB" do - doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'name' => 'my test'}) - doc.class.should eql(Article) - end - - it "should instantialize document of different type" do - doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, 'couchrest-type' => 'WithCallBacks', 'name' => 'my test'}) - doc.class.should eql(WithCallBacks) - end - - end - - describe "update attributes without saving" do - before(:each) do - a = Article.get "big-bad-danger" rescue nil - a.destroy if a - @art = Article.new(:title => "big bad danger") - @art.save - end - it "should work for attribute= methods" do - @art['title'].should == "big bad danger" - @art.update_attributes_without_saving('date' => Time.now, :title => "super danger") - @art['title'].should == "super danger" - end - it "should silently ignore _id" do - @art.update_attributes_without_saving('_id' => 'foobar') - @art['_id'].should_not == 'foobar' - end - it "should silently ignore _rev" do - @art.update_attributes_without_saving('_rev' => 'foobar') - @art['_rev'].should_not == 'foobar' - end - it "should silently ignore created_at" do - @art.update_attributes_without_saving('created_at' => 'foobar') - @art['created_at'].should_not == 'foobar' - end - it "should silently ignore updated_at" do - @art.update_attributes_without_saving('updated_at' => 'foobar') - @art['updated_at'].should_not == 'foobar' - end - it "should also work using attributes= alias" do - @art.respond_to?(:attributes=).should be_true - @art.attributes = {'date' => Time.now, :title => "something else"} - @art['title'].should == "something else" - end - - it "should not flip out if an attribute= method is missing and ignore it" do - lambda { - @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") - }.should_not raise_error - @art.slug.should == "big-bad-danger" - end - - #it "should not change other attributes if there is an error" do - # lambda { - # @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger") - # }.should raise_error - # @art['title'].should == "big bad danger" - #end - end - - describe "update attributes" do - before(:each) do - a = Article.get "big-bad-danger" rescue nil - a.destroy if a - @art = Article.new(:title => "big bad danger") - @art.save - end - it "should save" do - @art['title'].should == "big bad danger" - @art.update_attributes('date' => Time.now, :title => "super danger") - loaded = Article.get(@art.id) - loaded['title'].should == "super danger" - end - end - - describe "with default" do - it "should have the default value set at initalization" do - @obj.preset.should == {:right => 10, :top_align => false} - end - - it "should have the default false value explicitly assigned" do - @obj.default_false.should == false - end - - it "should automatically call a proc default at initialization" do - @obj.set_by_proc.should be_an_instance_of(Time) - @obj.set_by_proc.should == @obj.set_by_proc - @obj.set_by_proc.should < Time.now - end - - it "should let you overwrite the default values" do - obj = WithDefaultValues.new(:preset => 'test') - obj.preset = 'test' - end - - it "should work with a default empty array" do - obj = WithDefaultValues.new(:tags => ['spec']) - obj.tags.should == ['spec'] - end - - it "should set default value of read-only property" do - obj = WithDefaultValues.new - obj.read_only_with_default.should == 'generic' - end - end - - describe "simplified way of setting property types" do - it "should set defaults" do - obj = WithSimplePropertyType.new - obj.preset.should eql('none') - end - - it "should handle arrays" do - obj = WithSimplePropertyType.new(:tags => ['spec']) - obj.tags.should == ['spec'] - end - end - - describe "a doc with template values (CR::Model spec)" do - before(:all) do - WithTemplateAndUniqueID.all.map{|o| o.destroy(true)} - WithTemplateAndUniqueID.database.bulk_delete - @tmpl = WithTemplateAndUniqueID.new - @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'important-field' => '1') - end - it "should have fields set when new" do - @tmpl.preset.should == 'value' - end - it "shouldn't override explicitly set values" do - @tmpl2.preset.should == 'not_value' - end - it "shouldn't override existing documents" do - @tmpl2.save - tmpl2_reloaded = WithTemplateAndUniqueID.get(@tmpl2.id) - @tmpl2.preset.should == 'not_value' - tmpl2_reloaded.preset.should == 'not_value' - end - end - - describe "getting a model" do - before(:all) do - @art = Article.new(:title => 'All About Getting') - @art.save - end - it "should load and instantiate it" do - foundart = Article.get @art.id - foundart.title.should == "All About Getting" - end - it "should load and instantiate with find" do - foundart = Article.find @art.id - foundart.title.should == "All About Getting" - end - it "should return nil if `get` is used and the document doesn't exist" do - foundart = Article.get 'matt aimonetti' - foundart.should be_nil - end - it "should raise an error if `get!` is used and the document doesn't exist" do - lambda{foundart = Article.get!('matt aimonetti')}.should raise_error - end - it "should raise an error if `find!` is used and the document doesn't exist" do - lambda{foundart = Article.find!('matt aimonetti')}.should raise_error - end - end - - describe "getting a model with a subobjects array" do - before(:all) do - course_doc = { - "title" => "Metaphysics 200", - "questions" => [ - { - "q" => "Carve the ___ of reality at the ___.", - "a" => ["beast","joints"] - },{ - "q" => "Who layed the smack down on Leibniz's Law?", - "a" => "Willard Van Orman Quine" - } - ] - } - r = Course.database.save_doc course_doc - @course = Course.get r['id'] - end - it "should load the course" do - @course.title.should == "Metaphysics 200" - end - it "should instantiate them as such" do - @course["questions"][0].a[0].should == "beast" - end - end - - describe "finding all instances of a model" do - before(:all) do - WithTemplateAndUniqueID.req_design_doc_refresh - WithTemplateAndUniqueID.all.map{|o| o.destroy(true)} - WithTemplateAndUniqueID.database.bulk_delete - WithTemplateAndUniqueID.new('important-field' => '1').save - WithTemplateAndUniqueID.new('important-field' => '2').save - WithTemplateAndUniqueID.new('important-field' => '3').save - WithTemplateAndUniqueID.new('important-field' => '4').save - end - it "should make the design doc" do - WithTemplateAndUniqueID.all - d = WithTemplateAndUniqueID.design_doc - d['views']['all']['map'].should include('WithTemplateAndUniqueID') - end - it "should find all" do - rs = WithTemplateAndUniqueID.all - rs.length.should == 4 - end - end - - describe "counting all instances of a model" do - before(:each) do - @db = reset_test_db! - WithTemplateAndUniqueID.req_design_doc_refresh - end - - it ".count should return 0 if there are no docuemtns" do - WithTemplateAndUniqueID.count.should == 0 - end - - it ".count should return the number of documents" do - WithTemplateAndUniqueID.new('important-field' => '1').save - WithTemplateAndUniqueID.new('important-field' => '2').save - WithTemplateAndUniqueID.new('important-field' => '3').save - - WithTemplateAndUniqueID.count.should == 3 - end - end - - describe "finding the first instance of a model" do - before(:each) do - @db = reset_test_db! - # WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically - WithTemplateAndUniqueID.new('important-field' => '1').save - WithTemplateAndUniqueID.new('important-field' => '2').save - WithTemplateAndUniqueID.new('important-field' => '3').save - WithTemplateAndUniqueID.new('important-field' => '4').save - end - it "should make the design doc" do - WithTemplateAndUniqueID.all - d = WithTemplateAndUniqueID.design_doc - d['views']['all']['map'].should include('WithTemplateAndUniqueID') - end - it "should find first" do - rs = WithTemplateAndUniqueID.first - rs['important-field'].should == "1" - end - it "should return nil if no instances are found" do - WithTemplateAndUniqueID.all.each {|obj| obj.destroy } - WithTemplateAndUniqueID.first.should be_nil - end - end - - describe "lazily refreshing the design document" do - before(:all) do - @db = reset_test_db! - WithTemplateAndUniqueID.new('important-field' => '1').save - end - it "should not save the design doc twice" do - WithTemplateAndUniqueID.all - WithTemplateAndUniqueID.req_design_doc_refresh - WithTemplateAndUniqueID.refresh_design_doc - rev = WithTemplateAndUniqueID.design_doc['_rev'] - WithTemplateAndUniqueID.req_design_doc_refresh - WithTemplateAndUniqueID.refresh_design_doc - WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev) - end - end - - describe "getting a model with a subobject field" do - before(:all) do - course_doc = { - "title" => "Metaphysics 410", - "professor" => { - "name" => ["Mark", "Hinchliff"] - }, - "ends_at" => "2008/12/19 13:00:00 +0800" - } - r = Course.database.save_doc course_doc - @course = Course.get r['id'] - end - it "should load the course" do - @course["professor"]["name"][1].should == "Hinchliff" - end - it "should instantiate the professor as a person" do - @course['professor'].last_name.should == "Hinchliff" - end - it "should instantiate the ends_at as a Time" do - @course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800") - end - end - - describe "timestamping" do - before(:each) do - oldart = Article.get "saving-this" rescue nil - oldart.destroy if oldart - @art = Article.new(:title => "Saving this") - @art.save - end - - it "should define the updated_at and created_at getters and set the values" do - @obj.save - obj = WithDefaultValues.get(@obj.id) - obj.should be_an_instance_of(WithDefaultValues) - obj.created_at.should be_an_instance_of(Time) - obj.updated_at.should be_an_instance_of(Time) - obj.created_at.to_s.should == @obj.updated_at.to_s - end - - it "should not change created_at on update" do - 2.times do - lambda do - @art.save - end.should_not change(@art, :created_at) - end - end - - it "should set the time on create" do - (Time.now - @art.created_at).should < 2 - foundart = Article.get @art.id - foundart.created_at.should == foundart.updated_at - end - it "should set the time on update" do - @art.save - @art.created_at.should < @art.updated_at - end - end - - describe "basic saving and retrieving" do - it "should work fine" do - @obj.name = "should be easily saved and retrieved" - @obj.save - saved_obj = WithDefaultValues.get(@obj.id) - saved_obj.should_not be_nil - end - - it "should parse the Time attributes automatically" do - @obj.name = "should parse the Time attributes automatically" - @obj.set_by_proc.should be_an_instance_of(Time) - @obj.save - @obj.set_by_proc.should be_an_instance_of(Time) - saved_obj = WithDefaultValues.get(@obj.id) - saved_obj.set_by_proc.should be_an_instance_of(Time) - end - end - - describe "saving a model" do - before(:all) do - @sobj = Basic.new - @sobj.save.should == true - end - - it "should save the doc" do - doc = Basic.get(@sobj.id) - doc['_id'].should == @sobj.id - end - - it "should be set for resaving" do - rev = @obj.rev - @sobj['another-key'] = "some value" - @sobj.save - @sobj.rev.should_not == rev - end - - it "should set the id" do - @sobj.id.should be_an_instance_of(String) - end - - it "should set the type" do - @sobj['couchrest-type'].should == 'Basic' - end - - describe "save!" do - - before(:each) do - @sobj = Card.new(:first_name => "Marcos", :last_name => "Tapajós") - end - - it "should return true if save the document" do - @sobj.save!.should == true - end - - it "should raise error if don't save the document" do - @sobj.first_name = nil - lambda { @sobj.save!.should == true }.should raise_error(RuntimeError) - end - - end - - - end - - describe "saving a model with a unique_id configured" do - before(:each) do - @art = Article.new - @old = Article.database.get('this-is-the-title') rescue nil - Article.database.delete_doc(@old) if @old - end - - it "should be a new document" do - @art.should be_new - @art.title.should be_nil - end - - it "should require the title" do - lambda{@art.save}.should raise_error - @art.title = 'This is the title' - @art.save.should == true - end - - it "should not change the slug on update" do - @art.title = 'This is the title' - @art.save.should == true - @art.title = 'new title' - @art.save.should == true - @art.slug.should == 'this-is-the-title' - end - - it "should raise an error when the slug is taken" do - @art.title = 'This is the title' - @art.save.should == true - @art2 = Article.new(:title => 'This is the title!') - lambda{@art2.save}.should raise_error - end - - it "should set the slug" do - @art.title = 'This is the title' - @art.save.should == true - @art.slug.should == 'this-is-the-title' - end - - it "should set the id" do - @art.title = 'This is the title' - @art.save.should == true - @art.id.should == 'this-is-the-title' - end - end - - describe "saving a model with a unique_id lambda" do - before(:each) do - @templated = WithTemplateAndUniqueID.new - @old = WithTemplateAndUniqueID.get('very-important') rescue nil - @old.destroy if @old - end - - it "should require the field" do - lambda{@templated.save}.should raise_error - @templated['important-field'] = 'very-important' - @templated.save.should == true - end - - it "should save with the id" do - @templated['important-field'] = 'very-important' - @templated.save.should == true - t = WithTemplateAndUniqueID.get('very-important') - t.should == @templated - end - - it "should not change the id on update" do - @templated['important-field'] = 'very-important' - @templated.save.should == true - @templated['important-field'] = 'not-important' - @templated.save.should == true - t = WithTemplateAndUniqueID.get('very-important') - t.should == @templated - end - - it "should raise an error when the id is taken" do - @templated['important-field'] = 'very-important' - @templated.save.should == true - lambda{WithTemplateAndUniqueID.new('important-field' => 'very-important').save}.should raise_error - end - - it "should set the id" do - @templated['important-field'] = 'very-important' - @templated.save.should == true - @templated.id.should == 'very-important' - end - end - - describe "destroying an instance" do - before(:each) do - @dobj = Basic.new - @dobj.save.should == true - end - it "should return true" do - result = @dobj.destroy - result.should == true - end - it "should be resavable" do - @dobj.destroy - @dobj.rev.should be_nil - @dobj.id.should be_nil - @dobj.save.should == true - end - it "should make it go away" do - @dobj.destroy - lambda{Basic.get!(@dobj.id)}.should raise_error - end - end - - - describe "callbacks" do - - before(:each) do - @doc = WithCallBacks.new - end - - - describe "validate" do - it "should run before_validate before validating" do - @doc.run_before_validate.should be_nil - @doc.should be_valid - @doc.run_before_validate.should be_true - end - it "should run after_validate after validating" do - @doc.run_after_validate.should be_nil - @doc.should be_valid - @doc.run_after_validate.should be_true - end - end - - describe "save" do - it "should run the after filter after saving" do - @doc.run_after_save.should be_nil - @doc.save.should be_true - @doc.run_after_save.should be_true - end - it "should run the grouped callbacks before saving" do - @doc.run_one.should be_nil - @doc.run_two.should be_nil - @doc.run_three.should be_nil - @doc.save.should be_true - @doc.run_one.should be_true - @doc.run_two.should be_true - @doc.run_three.should be_true - end - it "should not run conditional callbacks" do - @doc.run_it = false - @doc.save.should be_true - @doc.conditional_one.should be_nil - @doc.conditional_two.should be_nil - end - it "should run conditional callbacks" do - @doc.run_it = true - @doc.save.should be_true - @doc.conditional_one.should be_true - @doc.conditional_two.should be_true - end - end - describe "create" do - it "should run the before save filter when creating" do - @doc.run_before_save.should be_nil - @doc.create.should_not be_nil - @doc.run_before_save.should be_true - end - it "should run the before create filter" do - @doc.run_before_create.should be_nil - @doc.create.should_not be_nil - @doc.create - @doc.run_before_create.should be_true - end - it "should run the after create filter" do - @doc.run_after_create.should be_nil - @doc.create.should_not be_nil - @doc.create - @doc.run_after_create.should be_true - end - end - describe "update" do - - before(:each) do - @doc.save - end - it "should run the before update filter when updating an existing document" do - @doc.run_before_update.should be_nil - @doc.update - @doc.run_before_update.should be_true - end - it "should run the after update filter when updating an existing document" do - @doc.run_after_update.should be_nil - @doc.update - @doc.run_after_update.should be_true - end - it "should run the before update filter when saving an existing document" do - @doc.run_before_update.should be_nil - @doc.save - @doc.run_before_update.should be_true - end - - end - end - - describe "getter and setter methods" do - it "should try to call the arg= method before setting :arg in the hash" do - @doc = WithGetterAndSetterMethods.new(:arg => "foo") - @doc['arg'].should be_nil - @doc[:arg].should be_nil - @doc.other_arg.should == "foo-foo" - end - end - - describe "initialization" do - it "should call after_initialize method if available" do - @doc = WithAfterInitializeMethod.new - @doc['some_value'].should eql('value') - end - end - - describe "recursive validation on an extended document" do - before :each do - reset_test_db! - @cat = Cat.new(:name => 'Sockington') - end - - it "should not save if a nested casted model is invalid" do - @cat.favorite_toy = CatToy.new - @cat.should_not be_valid - @cat.save.should be_false - lambda{@cat.save!}.should raise_error - end - - it "should save when nested casted model is valid" do - @cat.favorite_toy = CatToy.new(:name => 'Squeaky') - @cat.should be_valid - @cat.save.should be_true - lambda{@cat.save!}.should_not raise_error - end - - it "should not save when nested collection contains an invalid casted model" do - @cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new] - @cat.should_not be_valid - @cat.save.should be_false - lambda{@cat.save!}.should raise_error - end - - it "should save when nested collection contains valid casted models" do - @cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')] - @cat.should be_valid - @cat.save.should be_true - lambda{@cat.save!}.should_not raise_error - end - - it "should not fail if the nested casted model doesn't have validation" do - Cat.property :trainer, :cast_as => 'Person' - Cat.validates_presence_of :name - cat = Cat.new(:name => 'Mr Bigglesworth') - cat.trainer = Person.new - cat.trainer.validatable?.should be_false - cat.should be_valid - cat.save.should be_true - end - end - - describe "searching the contents of an extended document" do - before :each do - @db = reset_test_db! - - names = ["Fuzzy", "Whiskers", "Mr Bigglesworth", "Sockington", "Smitty", "Sammy", "Samson", "Simon"] - names.each { |name| Cat.create(:name => name) } - - search_function = { 'defaults' => {'store' => 'no', 'index' => 'analyzed_no_norms'}, - 'index' => "function(doc) { ret = new Document(); ret.add(doc['name'], {'field':'name'}); return ret; }" } - @db.save_doc({'_id' => '_design/search', 'fulltext' => {'cats' => search_function}}) - end - - it "should be able to paginate through a large set of search results" do - if couchdb_lucene_available? - names = [] - Cat.paginated_each(:design_doc => "search", :view_name => "cats", - :q => 'name:S*', :search => true, :include_docs => true, :per_page => 3) do |cat| - cat.should_not be_nil - names << cat.name - end - - names.size.should == 5 - names.should include('Sockington') - names.should include('Smitty') - names.should include('Sammy') - names.should include('Samson') - names.should include('Simon') - end - end - end - -end diff --git a/spec/couchrest/extended_doc_inherited_spec.rb b/spec/couchrest/inherited_spec.rb similarity index 94% rename from spec/couchrest/extended_doc_inherited_spec.rb rename to spec/couchrest/inherited_spec.rb index 32a0362..a661d01 100644 --- a/spec/couchrest/extended_doc_inherited_spec.rb +++ b/spec/couchrest/inherited_spec.rb @@ -13,7 +13,7 @@ begin class PlainChild < PlainParent end - class ExtendedParent < CouchRest::ExtendedDocument + class ExtendedParent < CouchRest::Model::Base class_inheritable_accessor :foo self.foo = :bar end diff --git a/spec/couchrest/persistence_spec.rb b/spec/couchrest/persistence_spec.rb new file mode 100644 index 0000000..05e9b39 --- /dev/null +++ b/spec/couchrest/persistence_spec.rb @@ -0,0 +1,409 @@ +# encoding: utf-8 +require File.expand_path('../../spec_helper', __FILE__) +require File.join(FIXTURE_PATH, 'base') +require File.join(FIXTURE_PATH, 'more', 'cat') +require File.join(FIXTURE_PATH, 'more', 'article') +require File.join(FIXTURE_PATH, 'more', 'course') +require File.join(FIXTURE_PATH, 'more', 'card') + +describe "Model Persistence" do + + before(:each) do + @obj = WithDefaultValues.new + end + + describe "creating a new document from database" do + + it "should instantialize" do + doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'couchrest-type' => 'Article', 'name' => 'my test'}) + doc.class.should eql(Article) + end + + it "should instantialize of same class if no couchrest-type included from DB" do + doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'name' => 'my test'}) + doc.class.should eql(Article) + end + + it "should instantialize document of different type" do + doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, 'couchrest-type' => 'WithTemplateAndUniqueID', 'name' => 'my test'}) + doc.class.should eql(WithTemplateAndUniqueID) + end + + end + + describe "basic saving and retrieving" do + it "should work fine" do + @obj.name = "should be easily saved and retrieved" + @obj.save + saved_obj = WithDefaultValues.get(@obj.id) + saved_obj.should_not be_nil + end + + it "should parse the Time attributes automatically" do + @obj.name = "should parse the Time attributes automatically" + @obj.set_by_proc.should be_an_instance_of(Time) + @obj.save + @obj.set_by_proc.should be_an_instance_of(Time) + saved_obj = WithDefaultValues.get(@obj.id) + saved_obj.set_by_proc.should be_an_instance_of(Time) + end + end + + describe "creating a model" do + + before(:each) do + @sobj = Basic.new + end + + it "should accept true or false on save for validation" do + @sobj.should_receive(:valid?) + @sobj.save(true) + end + + it "should accept hash with validation option" do + @sobj.should_receive(:valid?) + @sobj.save(:validate => true) + end + + it "should not call validation when option is false" do + @sobj.should_not_receive(:valid?) + @sobj.save(false) + end + + it "should not call validation when option :validate is false" do + @sobj.should_not_receive(:valid?) + @sobj.save(:validate => false) + end + + it "should instantialize and save a document" do + article = Article.create(:title => 'my test') + article.title.should == 'my test' + article.should_not be_new + end + + it "should trigger the create callbacks" do + doc = WithCallBacks.create(:name => 'my other test') + doc.run_before_create.should be_true + doc.run_after_create.should be_true + doc.run_before_save.should be_true + doc.run_after_save.should be_true + end + + end + + describe "saving a model" do + before(:all) do + @sobj = Basic.new + @sobj.save.should be_true + end + + it "should save the doc" do + doc = Basic.get(@sobj.id) + doc['_id'].should == @sobj.id + end + + it "should be set for resaving" do + rev = @obj.rev + @sobj['another-key'] = "some value" + @sobj.save + @sobj.rev.should_not == rev + end + + it "should set the id" do + @sobj.id.should be_an_instance_of(String) + end + + it "should set the type" do + @sobj['couchrest-type'].should == 'Basic' + end + + it "should accept true or false on save for validation" do + @sobj.should_receive(:valid?) + @sobj.save(true) + end + + it "should accept hash with validation option" do + @sobj.should_receive(:valid?) + @sobj.save(:validate => true) + end + + it "should not call validation when option is false" do + @sobj.should_not_receive(:valid?) + @sobj.save(false) + end + + it "should not call validation when option :validate is false" do + @sobj.should_not_receive(:valid?) + @sobj.save(:validate => false) + end + + describe "save!" do + + before(:each) do + @sobj = Card.new(:first_name => "Marcos", :last_name => "Tapajós") + end + + it "should return true if save the document" do + @sobj.save!.should be_true + end + + it "should raise error if don't save the document" do + @sobj.first_name = nil + lambda { @sobj.save! }.should raise_error(CouchRest::Model::Errors::Validations) + end + + end + end + + describe "saving a model with a unique_id configured" do + before(:each) do + @art = Article.new + @old = Article.database.get('this-is-the-title') rescue nil + Article.database.delete_doc(@old) if @old + end + + it "should be a new document" do + @art.should be_new + @art.title.should be_nil + end + + it "should require the title" do + lambda{@art.save}.should raise_error + @art.title = 'This is the title' + @art.save.should be_true + end + + it "should not change the slug on update" do + @art.title = 'This is the title' + @art.save.should be_true + @art.title = 'new title' + @art.save.should be_true + @art.slug.should == 'this-is-the-title' + end + + it "should raise an error when the slug is taken" do + @art.title = 'This is the title' + @art.save.should be_true + @art2 = Article.new(:title => 'This is the title!') + lambda{@art2.save}.should raise_error + end + + it "should set the slug" do + @art.title = 'This is the title' + @art.save.should be_true + @art.slug.should == 'this-is-the-title' + end + + it "should set the id" do + @art.title = 'This is the title' + @art.save.should be_true + @art.id.should == 'this-is-the-title' + end + end + + describe "saving a model with a unique_id lambda" do + before(:each) do + @templated = WithTemplateAndUniqueID.new + @old = WithTemplateAndUniqueID.get('very-important') rescue nil + @old.destroy if @old + end + + it "should require the field" do + lambda{@templated.save}.should raise_error + @templated['important-field'] = 'very-important' + @templated.save.should be_true + end + + it "should save with the id" do + @templated['important-field'] = 'very-important' + @templated.save.should be_true + t = WithTemplateAndUniqueID.get('very-important') + t.should == @templated + end + + it "should not change the id on update" do + @templated['important-field'] = 'very-important' + @templated.save.should be_true + @templated['important-field'] = 'not-important' + @templated.save.should be_true + t = WithTemplateAndUniqueID.get('very-important') + t.should == @templated + end + + it "should raise an error when the id is taken" do + @templated['important-field'] = 'very-important' + @templated.save.should be_true + lambda{WithTemplateAndUniqueID.new('important-field' => 'very-important').save}.should raise_error + end + + it "should set the id" do + @templated['important-field'] = 'very-important' + @templated.save.should be_true + @templated.id.should == 'very-important' + end + end + + describe "destroying an instance" do + before(:each) do + @dobj = Basic.new + @dobj.save.should be_true + end + it "should return true" do + result = @dobj.destroy + result.should be_true + end + it "should be resavable" do + @dobj.destroy + @dobj.rev.should be_nil + @dobj.id.should be_nil + @dobj.save.should be_true + end + it "should make it go away" do + @dobj.destroy + lambda{Basic.get!(@dobj.id)}.should raise_error + end + end + + + describe "getting a model" do + before(:all) do + @art = Article.new(:title => 'All About Getting') + @art.save + end + it "should load and instantiate it" do + foundart = Article.get @art.id + foundart.title.should == "All About Getting" + end + it "should load and instantiate with find" do + foundart = Article.find @art.id + foundart.title.should == "All About Getting" + end + it "should return nil if `get` is used and the document doesn't exist" do + foundart = Article.get 'matt aimonetti' + foundart.should be_nil + end + it "should raise an error if `get!` is used and the document doesn't exist" do + lambda{foundart = Article.get!('matt aimonetti')}.should raise_error + end + it "should raise an error if `find!` is used and the document doesn't exist" do + lambda{foundart = Article.find!('matt aimonetti')}.should raise_error + end + end + + describe "getting a model with a subobjects array" do + before(:all) do + course_doc = { + "title" => "Metaphysics 200", + "questions" => [ + { + "q" => "Carve the ___ of reality at the ___.", + "a" => ["beast","joints"] + },{ + "q" => "Who layed the smack down on Leibniz's Law?", + "a" => "Willard Van Orman Quine" + } + ] + } + r = Course.database.save_doc course_doc + @course = Course.get r['id'] + end + it "should load the course" do + @course.title.should == "Metaphysics 200" + end + it "should instantiate them as such" do + @course["questions"][0].a[0].should == "beast" + end + end + + describe "callbacks" do + + before(:each) do + @doc = WithCallBacks.new + end + + describe "validation" do + it "should run before_validation before validating" do + @doc.run_before_validate.should be_nil + @doc.should be_valid + @doc.run_before_validate.should be_true + end + it "should run after_validation after validating" do + @doc.run_after_validate.should be_nil + @doc.should be_valid + @doc.run_after_validate.should be_true + end + end + + describe "save" do + it "should run the after filter after saving" do + @doc.run_after_save.should be_nil + @doc.save.should be_true + @doc.run_after_save.should be_true + end + it "should run the grouped callbacks before saving" do + @doc.run_one.should be_nil + @doc.run_two.should be_nil + @doc.run_three.should be_nil + @doc.save.should be_true + @doc.run_one.should be_true + @doc.run_two.should be_true + @doc.run_three.should be_true + end + it "should not run conditional callbacks" do + @doc.run_it = false + @doc.save.should be_true + @doc.conditional_one.should be_nil + @doc.conditional_two.should be_nil + end + it "should run conditional callbacks" do + @doc.run_it = true + @doc.save.should be_true + @doc.conditional_one.should be_true + @doc.conditional_two.should be_true + end + end + describe "create" do + it "should run the before save filter when creating" do + @doc.run_before_save.should be_nil + @doc.create.should_not be_nil + @doc.run_before_save.should be_true + end + it "should run the before create filter" do + @doc.run_before_create.should be_nil + @doc.create.should_not be_nil + @doc.create + @doc.run_before_create.should be_true + end + it "should run the after create filter" do + @doc.run_after_create.should be_nil + @doc.create.should_not be_nil + @doc.create + @doc.run_after_create.should be_true + end + end + describe "update" do + + before(:each) do + @doc.save + end + it "should run the before update filter when updating an existing document" do + @doc.run_before_update.should be_nil + @doc.update + @doc.run_before_update.should be_true + end + it "should run the after update filter when updating an existing document" do + @doc.run_after_update.should be_nil + @doc.update + @doc.run_after_update.should be_true + end + it "should run the before update filter when saving an existing document" do + @doc.run_before_update.should be_nil + @doc.save + @doc.run_before_update.should be_true + end + + end + end + + +end diff --git a/spec/couchrest/property_spec.rb b/spec/couchrest/property_spec.rb index 0ebb669..7e6f252 100644 --- a/spec/couchrest/property_spec.rb +++ b/spec/couchrest/property_spec.rb @@ -1,16 +1,16 @@ # encoding: utf-8 require File.expand_path('../../spec_helper', __FILE__) +require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'invoice') require File.join(FIXTURE_PATH, 'more', 'service') require File.join(FIXTURE_PATH, 'more', 'event') -require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'user') require File.join(FIXTURE_PATH, 'more', 'course') -describe "ExtendedDocument properties" do +describe "Model properties" do before(:each) do reset_test_db! @@ -134,27 +134,27 @@ describe "ExtendedDocument properties" do @card.first_name = nil @card.should_not be_valid @card.errors.should_not be_empty - @card.errors.on(:first_name).should == ["First name must not be blank"] + @card.errors[:first_name].should == ["can't be blank"] end it "should let you look up errors for a field by a string name" do @card.first_name = nil @card.should_not be_valid - @card.errors.on('first_name').should == ["First name must not be blank"] + @card.errors['first_name'].should == ["can't be blank"] end it "should validate the presence of 2 attributes" do @invoice.clear @invoice.should_not be_valid @invoice.errors.should_not be_empty - @invoice.errors.on(:client_name).first.should == "Client name must not be blank" - @invoice.errors.on(:employee_name).should_not be_empty + @invoice.errors[:client_name].should == ["can't be blank"] + @invoice.errors[:employee_name].should_not be_empty end it "should let you set an error message" do @invoice.location = nil @invoice.valid? - @invoice.errors.on(:location).should == ["Hey stupid!, you forgot the location"] + @invoice.errors[:location].should == ["Hey stupid!, you forgot the location"] end it "should validate before saving" do @@ -165,37 +165,6 @@ describe "ExtendedDocument properties" do end end - describe "autovalidation" do - before(:each) do - @service = Service.new(:name => "Coumpound analysis", :price => 3_000) - end - - it "should be valid" do - @service.should be_valid - end - - it "should not respond to properties not setup" do - @service.respond_to?(:client_name).should be_false - end - - describe "property :name, :length => 4...20" do - - it "should autovalidate the presence when length is set" do - @service.name = nil - @service.should_not be_valid - @service.errors.should_not be_nil - @service.errors.on(:name).first.should == "Name must be between 4 and 19 characters long" - end - - it "should autovalidate the correct length" do - @service.name = "a" - @service.should_not be_valid - @service.errors.should_not be_nil - @service.errors.on(:name).first.should == "Name must be between 4 and 19 characters long" - end - end - end - describe "casting" do before(:each) do @course = Course.new(:title => 'Relaxation') @@ -740,50 +709,43 @@ end describe "Property Class" do it "should provide name as string" do - property = CouchRest::Property.new(:test, String) + property = CouchRest::Model::Property.new(:test, String) property.name.should eql('test') property.to_s.should eql('test') end it "should provide class from type" do - property = CouchRest::Property.new(:test, String) + property = CouchRest::Model::Property.new(:test, String) property.type_class.should eql(String) end it "should provide base class from type in array" do - property = CouchRest::Property.new(:test, [String]) + property = CouchRest::Model::Property.new(:test, [String]) property.type_class.should eql(String) end - it "should leave type as string if requested" do - property = CouchRest::Property.new(:test, 'String') - property.type.should eql('String') - property.type_class.should eql(String) + it "should raise error if type as string requested" do + lambda { + property = CouchRest::Model::Property.new(:test, 'String') + }.should raise_error end - it "should leave type nil and return string by default" do - property = CouchRest::Property.new(:test, nil) + it "should leave type nil and return class as nil also" do + property = CouchRest::Model::Property.new(:test, nil) property.type.should be_nil - # Type cast should never be used on non-casted property! - property.type_class.should eql(String) + property.type_class.should be_nil end - it "should convert empty type array to [String]" do - property = CouchRest::Property.new(:test, []) - property.type_class.should eql(String) - end - - it "should convert boolean text-type TrueClass" do - property = CouchRest::Property.new(:test, 'boolean') - property.type.should eql('boolean') # no change - property.type_class.should eql(TrueClass) + it "should convert empty type array to [Object]" do + property = CouchRest::Model::Property.new(:test, []) + property.type_class.should eql(Object) end it "should set init method option or leave as 'new'" do # (bad example! Time already typecast) - property = CouchRest::Property.new(:test, Time) + property = CouchRest::Model::Property.new(:test, Time) property.init_method.should eql('new') - property = CouchRest::Property.new(:test, Time, :init_method => 'parse') + property = CouchRest::Model::Property.new(:test, Time, :init_method => 'parse') property.init_method.should eql('parse') end @@ -791,32 +753,32 @@ describe "Property Class" do describe "casting" do it "should cast a value" do - property = CouchRest::Property.new(:test, Date) + property = CouchRest::Model::Property.new(:test, Date) parent = mock("FooObject") property.cast(parent, "2010-06-16").should eql(Date.new(2010, 6, 16)) property.cast_value(parent, "2010-06-16").should eql(Date.new(2010, 6, 16)) end it "should cast an array of values" do - property = CouchRest::Property.new(:test, [Date]) + property = CouchRest::Model::Property.new(:test, [Date]) parent = mock("FooObject") property.cast(parent, ["2010-06-01", "2010-06-02"]).should eql([Date.new(2010, 6, 1), Date.new(2010, 6, 2)]) end it "should set a CastedArray on array of Objects" do - property = CouchRest::Property.new(:test, [Object]) + property = CouchRest::Model::Property.new(:test, [Object]) parent = mock("FooObject") - property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(::CouchRest::CastedArray) + property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray) end it "should not set a CastedArray on array of Strings" do - property = CouchRest::Property.new(:test, [String]) + property = CouchRest::Model::Property.new(:test, [String]) parent = mock("FooObject") - property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should_not eql(::CouchRest::CastedArray) + property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should_not eql(CouchRest::Model::CastedArray) end it "should raise and error if value is array when type is not" do - property = CouchRest::Property.new(:test, Date) + property = CouchRest::Model::Property.new(:test, Date) parent = mock("FooClass") lambda { cast = property.cast(parent, [Date.new(2010, 6, 1)]) @@ -825,13 +787,13 @@ describe "Property Class" do it "should set parent as casted_by object in CastedArray" do - property = CouchRest::Property.new(:test, [Object]) + property = CouchRest::Model::Property.new(:test, [Object]) parent = mock("FooObject") property.cast(parent, ["2010-06-01", "2010-06-02"]).casted_by.should eql(parent) end it "should set casted_by on new value" do - property = CouchRest::Property.new(:test, CatToy) + property = CouchRest::Model::Property.new(:test, CatToy) parent = mock("CatObject") cast = property.cast(parent, {:name => 'catnip'}) cast.casted_by.should eql(parent) diff --git a/spec/couchrest/extended_doc_subclass_spec.rb b/spec/couchrest/subclass_spec.rb similarity index 85% rename from spec/couchrest/extended_doc_subclass_spec.rb rename to spec/couchrest/subclass_spec.rb index 21e6e79..8344b90 100644 --- a/spec/couchrest/extended_doc_subclass_spec.rb +++ b/spec/couchrest/subclass_spec.rb @@ -1,4 +1,5 @@ require File.expand_path("../../spec_helper", __FILE__) +require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'course') @@ -9,6 +10,9 @@ Card.property :bg_color, :default => '#ccc' class BusinessCard < Card property :extension_code property :job_title + + validates_presence_of :extension_code + validates_presence_of :job_title end class DesignBusinessCard < BusinessCard @@ -20,7 +24,7 @@ class OnlineCourse < Course view_by :url end -class Animal < CouchRest::ExtendedDocument +class Animal < CouchRest::Model::Base use_database TEST_SERVER.default_database property :name view_by :name @@ -28,7 +32,7 @@ end class Dog < Animal; end -describe "Subclassing an ExtendedDocument" do +describe "Subclassing a Model" do before(:each) do @card = BusinessCard.new @@ -42,22 +46,18 @@ describe "Subclassing an ExtendedDocument" do @card.database.uri.should == Card.database.uri end - it "should share the same autovalidation details" do - @card.auto_validation.should be_true - end - it "should have kept the validation details" do @card.should_not be_valid end it "should have added the new validation details" do - validated_fields = @card.class.validators.contexts[:default].map{|v| v.field_name} - validated_fields.should include(:extension_code) + validated_fields = @card.class.validators.map{|v| v.attributes}.flatten + validated_fields.should include(:extension_code) validated_fields.should include(:job_title) end it "should not add to the parent's validations" do - validated_fields = Card.validators.contexts[:default].map{|v| v.field_name} + validated_fields = Card.validators.map{|v| v.attributes}.flatten validated_fields.should_not include(:extension_code) validated_fields.should_not include(:job_title) end diff --git a/spec/couchrest/extended_doc_view_spec.rb b/spec/couchrest/view_spec.rb similarity index 97% rename from spec/couchrest/extended_doc_view_spec.rb rename to spec/couchrest/view_spec.rb index 53115f6..4329caf 100644 --- a/spec/couchrest/extended_doc_view_spec.rb +++ b/spec/couchrest/view_spec.rb @@ -1,10 +1,12 @@ require File.expand_path("../../spec_helper", __FILE__) +require File.join(FIXTURE_PATH, 'more', 'cat') +require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'article') require File.join(FIXTURE_PATH, 'more', 'course') -describe "ExtendedDocument views" do +describe "Model views" do - class Unattached < CouchRest::ExtendedDocument + class Unattached < CouchRest::Model::Base # Note: no use_database here property :title property :questions @@ -438,7 +440,7 @@ describe "ExtendedDocument views" do it "should pass database parameter to pager" do proxy = mock(:proxy) proxy.stub!(:paginate) - ::CouchRest::Mixins::Collection::CollectionProxy.should_receive(:new).with('database', anything(), anything(), anything(), anything()).and_return(proxy) + ::CouchRest::Model::Collection::CollectionProxy.should_receive(:new).with('database', anything(), anything(), anything(), anything()).and_return(proxy) Article.paginate(:design_doc => 'Article', :view_name => 'by_date', :database => 'database') end end diff --git a/spec/fixtures/base.rb b/spec/fixtures/base.rb new file mode 100644 index 0000000..66c32a5 --- /dev/null +++ b/spec/fixtures/base.rb @@ -0,0 +1,117 @@ +class WithDefaultValues < CouchRest::Model::Base + use_database TEST_SERVER.default_database + property :preset, Object, :default => {:right => 10, :top_align => false} + property :set_by_proc, Time, :default => Proc.new{Time.now} + property :tags, [String], :default => [] + property :read_only_with_default, :default => 'generic', :read_only => true + property :default_false, TrueClass, :default => false + property :name + timestamps! +end + +class WithSimplePropertyType < CouchRest::Model::Base + use_database TEST_SERVER.default_database + property :name, String + property :preset, String, :default => 'none' + property :tags, [String] + timestamps! +end + +class WithCallBacks < CouchRest::Model::Base + use_database TEST_SERVER.default_database + property :name + property :run_before_validate + property :run_after_validate + property :run_before_save + property :run_after_save + property :run_before_create + property :run_after_create + property :run_before_update + property :run_after_update + + before_validate do |object| + object.run_before_validate = true + end + after_validate do |object| + object.run_after_validate = true + end + before_save do |object| + object.run_before_save = true + end + after_save do |object| + object.run_after_save = true + end + before_create do |object| + object.run_before_create = true + end + after_create do |object| + object.run_after_create = true + end + before_update do |object| + object.run_before_update = true + end + after_update do |object| + object.run_after_update = true + end + + property :run_one + property :run_two + property :run_three + + before_save :run_one_method, :run_two_method do |object| + object.run_three = true + end + def run_one_method + self.run_one = true + end + def run_two_method + self.run_two = true + end + + attr_accessor :run_it + property :conditional_one + property :conditional_two + + before_save :conditional_one_method, :conditional_two_method, :if => proc { self.run_it } + def conditional_one_method + self.conditional_one = true + end + def conditional_two_method + self.conditional_two = true + end +end + +class WithTemplateAndUniqueID < CouchRest::Model::Base + use_database TEST_SERVER.default_database + unique_id do |model| + model['important-field'] + end + property :preset, :default => 'value' + property :has_no_default +end + +class WithGetterAndSetterMethods < CouchRest::Model::Base + use_database TEST_SERVER.default_database + + property :other_arg + def arg + other_arg + end + + def arg=(value) + self.other_arg = "foo-#{value}" + end +end + +class WithAfterInitializeMethod < CouchRest::Model::Base + use_database TEST_SERVER.default_database + + property :some_value + + def after_initialize + self.some_value ||= "value" + end + +end + + diff --git a/spec/fixtures/more/article.rb b/spec/fixtures/more/article.rb index 0185d3b..3a7a405 100644 --- a/spec/fixtures/more/article.rb +++ b/spec/fixtures/more/article.rb @@ -1,4 +1,4 @@ -class Article < CouchRest::ExtendedDocument +class Article < CouchRest::Model::Base use_database DB unique_id :slug @@ -20,10 +20,10 @@ class Article < CouchRest::ExtendedDocument return sum(values); }" - property :date, :type => 'Date' + property :date, Date property :slug, :read_only => true property :title - property :tags, :type => ['String'] + property :tags, [String] timestamps! diff --git a/spec/fixtures/more/card.rb b/spec/fixtures/more/card.rb index d7b7f27..7080ce9 100644 --- a/spec/fixtures/more/card.rb +++ b/spec/fixtures/more/card.rb @@ -1,9 +1,4 @@ -class Card < CouchRest::ExtendedDocument - # Include the validation module to get access to the validation methods - include CouchRest::Validation - # set the auto_validation before defining the properties - auto_validate! - +class Card < CouchRest::Model::Base # Set the default database to use use_database DB diff --git a/spec/fixtures/more/cat.rb b/spec/fixtures/more/cat.rb index 7ab5397..903eafd 100644 --- a/spec/fixtures/more/cat.rb +++ b/spec/fixtures/more/cat.rb @@ -1,16 +1,13 @@ class CatToy < Hash - include ::CouchRest::CastedModel - include ::CouchRest::Validation + include ::CouchRest::Model::CastedModel property :name validates_presence_of :name end -class Cat < CouchRest::ExtendedDocument - include ::CouchRest::Validation - +class Cat < CouchRest::Model::Base # Set the default database to use use_database DB diff --git a/spec/fixtures/more/course.rb b/spec/fixtures/more/course.rb index 8146065..23e52cd 100644 --- a/spec/fixtures/more/course.rb +++ b/spec/fixtures/more/course.rb @@ -1,22 +1,22 @@ require File.join(FIXTURE_PATH, 'more', 'question') require File.join(FIXTURE_PATH, 'more', 'person') -class Course < CouchRest::ExtendedDocument +class Course < CouchRest::Model::Base use_database TEST_SERVER.default_database - property :title, :cast_as => 'String' - property :questions, :cast_as => ['Question'] - property :professor, :cast_as => 'Person' - property :participants, :type => ['Object'] - property :ends_at, :type => 'Time' - property :estimate, :type => 'Float' - property :hours, :type => 'Integer' - property :profit, :type => 'BigDecimal' - property :started_on, :type => 'Date' - property :updated_at, :type => 'DateTime' - property :active, :type => 'Boolean' + property :title, String + property :questions, [Question] + property :professor, Person + property :participants, [Object] + property :ends_at, Time + property :estimate, Float + property :hours, Integer + property :profit, BigDecimal + property :started_on, :type => Date + property :updated_at, :type => DateTime + property :active, :type => TrueClass property :very_active, :type => TrueClass - property :klass, :type => 'Class' + property :klass, :type => Class view_by :title view_by :title, :active diff --git a/spec/fixtures/more/event.rb b/spec/fixtures/more/event.rb index 81a4cba..55f1708 100644 --- a/spec/fixtures/more/event.rb +++ b/spec/fixtures/more/event.rb @@ -1,8 +1,8 @@ -class Event < CouchRest::ExtendedDocument +class Event < CouchRest::Model::Base use_database DB property :subject - property :occurs_at, :cast_as => 'Time', :init_method => 'parse' - property :end_date, :cast_as => 'Date', :init_method => 'parse' + property :occurs_at, Time, :init_method => 'parse' + property :end_date, Date, :init_method => 'parse' end diff --git a/spec/fixtures/more/invoice.rb b/spec/fixtures/more/invoice.rb index 2f2e185..540c9ff 100644 --- a/spec/fixtures/more/invoice.rb +++ b/spec/fixtures/more/invoice.rb @@ -1,7 +1,4 @@ -class Invoice < CouchRest::ExtendedDocument - # Include the validation module to get access to the validation methods - include CouchRest::Validation - +class Invoice < CouchRest::Model::Base # Set the default database to use use_database DB diff --git a/spec/fixtures/more/person.rb b/spec/fixtures/more/person.rb index 264563c..076bbc0 100644 --- a/spec/fixtures/more/person.rb +++ b/spec/fixtures/more/person.rb @@ -1,6 +1,6 @@ class Person < Hash - include ::CouchRest::CastedModel - property :pet, :cast_as => 'Cat' + include ::CouchRest::Model::CastedModel + property :pet, Cat property :name, [String] def last_name diff --git a/spec/fixtures/more/question.rb b/spec/fixtures/more/question.rb index b3d8fbf..5efcd20 100644 --- a/spec/fixtures/more/question.rb +++ b/spec/fixtures/more/question.rb @@ -1,5 +1,5 @@ class Question < Hash - include ::CouchRest::CastedModel + include ::CouchRest::Model::CastedModel property :q property :a diff --git a/spec/fixtures/more/service.rb b/spec/fixtures/more/service.rb index 77b28ef..3e9bc1f 100644 --- a/spec/fixtures/more/service.rb +++ b/spec/fixtures/more/service.rb @@ -1,12 +1,10 @@ -class Service < CouchRest::ExtendedDocument - # Include the validation module to get access to the validation methods - include CouchRest::Validation - auto_validate! +class Service < CouchRest::Model::Base # Set the default database to use use_database DB # Official Schema - property :name, :length => 4...20 - property :price, :type => 'Integer' + property :name + property :price, Integer -end \ No newline at end of file + validates_length_of :name, :minimum => 4, :maximum => 20 +end diff --git a/spec/fixtures/more/user.rb b/spec/fixtures/more/user.rb index f9bbf97..8021f64 100644 --- a/spec/fixtures/more/user.rb +++ b/spec/fixtures/more/user.rb @@ -1,11 +1,11 @@ -class User < CouchRest::ExtendedDocument +class User < CouchRest::Model::Base # Set the default database to use use_database DB property :name, :accessible => true property :admin # this will be automatically protected end -class SpecialUser < CouchRest::ExtendedDocument +class SpecialUser < CouchRest::Model::Base # Set the default database to use use_database DB property :name # this will not be protected diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8a8192f..53d65c1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ require "rubygems" require "spec" # Satisfies Autotest and anyone else not using the Rake tasks -require File.join(File.dirname(__FILE__), '..','lib','couchrest_extended_document') +require File.join(File.dirname(__FILE__), '..','lib','couchrest_model') # check the following file to see how to use the spec'd features. unless defined?(FIXTURE_PATH) @@ -9,14 +9,13 @@ unless defined?(FIXTURE_PATH) SCRATCH_PATH = File.join(File.dirname(__FILE__), '/tmp') COUCHHOST = "http://127.0.0.1:5984" - TESTDB = 'couchrest-test' - REPLICATIONDB = 'couchrest-test-replication' + TESTDB = 'couchrest-model-test' TEST_SERVER = CouchRest.new TEST_SERVER.default_database = TESTDB DB = TEST_SERVER.database(TESTDB) end -class Basic < CouchRest::ExtendedDocument +class Basic < CouchRest::Model::Base use_database TEST_SERVER.default_database end