├── .gitignore ├── .rspec ├── .travis.yml ├── Dockerfile ├── Gemfile ├── Gemfile.activesupport-4.x ├── Gemfile.activesupport-5.x ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── THANKS.md ├── VERSION ├── benchmarks ├── Gemfile ├── connections.rb └── dirty.rb ├── couchrest_model.gemspec ├── docker-compose.yml ├── history.md ├── init.rb ├── lib ├── couchrest │ ├── model.rb │ ├── model │ │ ├── associations.rb │ │ ├── base.rb │ │ ├── callbacks.rb │ │ ├── casted_array.rb │ │ ├── casted_by.rb │ │ ├── configuration.rb │ │ ├── connection.rb │ │ ├── connection_config.rb │ │ ├── core_extensions │ │ │ ├── hash.rb │ │ │ └── time_parsing.rb │ │ ├── design.rb │ │ ├── designs.rb │ │ ├── designs │ │ │ ├── design_mapper.rb │ │ │ ├── migrations.rb │ │ │ └── view.rb │ │ ├── dirty.rb │ │ ├── document_queries.rb │ │ ├── embeddable.rb │ │ ├── errors.rb │ │ ├── extended_attachments.rb │ │ ├── persistence.rb │ │ ├── properties.rb │ │ ├── property.rb │ │ ├── property_protection.rb │ │ ├── proxyable.rb │ │ ├── server_pool.rb │ │ ├── support │ │ │ └── couchrest_database.rb │ │ ├── translation.rb │ │ ├── typecast.rb │ │ ├── utils │ │ │ └── migrate.rb │ │ ├── validations.rb │ │ └── validations │ │ │ ├── casted_model.rb │ │ │ ├── locale │ │ │ └── en.yml │ │ │ └── uniqueness.rb │ └── railtie.rb ├── couchrest_model.rb ├── rails │ └── generators │ │ ├── couchrest_model.rb │ │ └── couchrest_model │ │ ├── config │ │ ├── config_generator.rb │ │ └── templates │ │ │ └── couchdb.yml │ │ └── model │ │ ├── model_generator.rb │ │ └── templates │ │ └── model.rb └── tasks │ └── migrations.rake └── spec ├── .gitignore ├── fixtures ├── attachments │ ├── README │ ├── couchdb.png │ └── test.html ├── config │ └── couchdb.yml ├── models │ ├── article.rb │ ├── base.rb │ ├── card.rb │ ├── cat.rb │ ├── client.rb │ ├── concerns │ │ └── attachable.rb │ ├── course.rb │ ├── designs.rb │ ├── event.rb │ ├── invoice.rb │ ├── key_chain.rb │ ├── membership.rb │ ├── money.rb │ ├── person.rb │ ├── project.rb │ ├── question.rb │ ├── sale_entry.rb │ ├── sale_invoice.rb │ ├── service.rb │ └── user.rb └── views │ ├── lib.js │ └── test_view │ ├── lib.js │ ├── only-map.js │ ├── test-map.js │ └── test-reduce.js ├── functional └── validations_spec.rb ├── spec_helper.rb └── unit ├── active_model_lint_spec.rb ├── assocations_spec.rb ├── attachment_spec.rb ├── base_spec.rb ├── casted_array_spec.rb ├── casted_spec.rb ├── configuration_spec.rb ├── connection_config_spec.rb ├── connection_spec.rb ├── core_extensions └── time_parsing_spec.rb ├── design_spec.rb ├── designs ├── design_mapper_spec.rb ├── migrations_spec.rb └── view_spec.rb ├── designs_spec.rb ├── dirty_spec.rb ├── embeddable_spec.rb ├── inherited_spec.rb ├── persistence_spec.rb ├── properties_spec.rb ├── property_protection_spec.rb ├── property_spec.rb ├── proxyable_spec.rb ├── server_pool_spec.rb ├── subclass_spec.rb ├── support └── couchrest_database_spec.rb ├── translations_spec.rb ├── typecast_spec.rb ├── utils └── migrate_spec.rb └── validations_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | html/* 3 | pkg 4 | *.swp 5 | .rvmrc* 6 | .bundle 7 | couchdb.std* 8 | *.*~ 9 | *.lock 10 | spec.out 11 | *.gem 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format 3 | progress 4 | mtime 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: ruby 3 | jobs: 4 | include: 5 | - rvm: 2.7.0 6 | gemfile: Gemfile.activesupport-5.x 7 | - rvm: 2.6.0 8 | gemfile: Gemfile.activesupport-5.x 9 | - rvm: 2.6.0 10 | gemfile: Gemfile.activesupport-4.x 11 | - rvm: 2.5.0 12 | gemfile: Gemfile.activesupport-5.x 13 | - rvm: 2.5.0 14 | gemfile: Gemfile.activesupport-4.x 15 | - rvm: 2.4.10 16 | gemfile: Gemfile.activesupport-5.x 17 | - rvm: 2.4.10 18 | gemfile: Gemfile.activesupport-4.x 19 | - rvm: 2.3.0 20 | gemfile: Gemfile.activesupport-5.x 21 | - rvm: 2.3.0 22 | gemfile: Gemfile.activesupport-4.x 23 | - rvm: 2.2.4 24 | gemfile: Gemfile.activesupport-5.x 25 | - rvm: 2.2.4 26 | gemfile: Gemfile.activesupport-4.x 27 | - rvm: 2.1.10 28 | gemfile: Gemfile.activesupport-4.x 29 | - rvm: 2.0.0 30 | gemfile: Gemfile.activesupport-4.x 31 | services: couchdb 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.3 2 | 3 | ENV WORK_DIR /usr/lib/couchrest_model 4 | 5 | RUN mkdir -p $WORK_DIR 6 | WORKDIR $WORK_DIR 7 | 8 | COPY . $WORK_DIR 9 | 10 | RUN bundle install --jobs=3 --retry=3 11 | 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source "https://rubygems.org" 3 | gemspec 4 | 5 | gem "guard-rspec", "~> 4.7.0", group: :test 6 | 7 | # Enable for testing against local couchrest 8 | # gem "couchrest", path: "/Users/sam/workspace/couchrest" 9 | -------------------------------------------------------------------------------- /Gemfile.activesupport-4.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | 4 | gem 'activesupport', '~> 4.0' 5 | -------------------------------------------------------------------------------- /Gemfile.activesupport-5.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | 4 | gem 'activesupport', '~> 5.0' 5 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | # Note: The cmd option is now required due to the increasing number of ways 19 | # rspec may be run, below are examples of the most common uses. 20 | # * bundler: 'bundle exec rspec' 21 | # * bundler binstubs: 'bin/rspec' 22 | # * spring: 'bin/rsspec' (This will use spring if running and you have 23 | # installed the spring binstubs per the docs) 24 | # * zeus: 'zeus rspec' (requires the server to be started separetly) 25 | # * 'just' rspec: 'rspec' 26 | guard :rspec, cmd: 'rspec' do 27 | watch(%r{^spec/.+_spec\.rb$}) 28 | watch(%r{^lib/couchrest/model/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" } 29 | watch('spec/spec_helper.rb') { "spec" } 30 | end 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CouchRest Model 2 | 3 | [![Build Status](https://travis-ci.org/couchrest/couchrest_model.png)](https://travis-ci.org/couchrest/couchrest_model) 4 | 5 | CouchRest Model helps you define models that are stored as documents in your CouchDB database. 6 | 7 | It supports useful features such as setting properties with typecasting, callbacks, validations, associations, and helps 8 | with creating CouchDB views to access your data. 9 | 10 | CouchRest Model uses ActiveModel for a lot of the magic, so if you're using Rails, you'll need at least version 3.0. Releases since 2.0.0 are Rails 4.0 compatible, and we recommend Ruby 2.0+. 11 | 12 | ## Documentation 13 | 14 | Please visit the documentation project at [http://www.couchrest.info](http://www.couchrest.info). Your [contributions](https://github.com/couchrest/couchrest.github.com) would be greatly appreciated! 15 | 16 | General API: [http://rdoc.info/projects/couchrest/couchrest_model](http://rdoc.info/projects/couchrest/couchrest_model) 17 | 18 | See the [update history](https://github.com/couchrest/couchrest_model/blob/master/history.md) for an up to date list of all the changes we've been working on recently. 19 | 20 | ### Upgrading from an earlier version? 21 | 22 | *Pre 2.2:* As of August 2016, dirty tracking has been radically re-factored away from ActiveModel::Dirty, which only has support for basic attributes, into a solution that uses [Hashdiff](https://github.com/liufengyun/hashdiff), more details available in the [pull request](https://github.com/couchrest/couchrest_model/pull/211). The result is that some of ActiveModel's Dirty methods are no longer available, these are: `changes_applied`, `restore_attributes`, `previous_changes`, and `changed_attributes`. 23 | 24 | *Pre 2.0:* As of June 2012, couchrest model no longer supports the `view_by` and `view` calls from the model. Views are no only accessed via a design document. If you have older code and wish to upgrade, please ensure you move to the new syntax for using views. 25 | 26 | *Pre 1.1:* As of April 2011 and the release of version 1.1.0, the default model type key is 'type' instead of 'couchrest-type'. Simply updating your project will not work unless you migrate your data or set the configuration option in your initializers: 27 | 28 | ```ruby 29 | CouchRest::Model::Base.configure do |config| 30 | config.model_type_key = 'couchrest-type' 31 | end 32 | ``` 33 | 34 | ## Install 35 | 36 | ### Gem 37 | 38 | ```bash 39 | $ sudo gem install couchrest_model 40 | ``` 41 | 42 | ### Bundler 43 | 44 | If you're using bundler, define a line similar to the following in your project's Gemfile: 45 | 46 | ```ruby 47 | gem 'couchrest_model' 48 | ``` 49 | 50 | ### Configuration 51 | 52 | CouchRest Model is configured to work out the box with no configuration as long as your CouchDB instance is running on the default port (5984) on localhost. The default name of the database is either the name of your application as provided by the `Rails.application.class.to_s` call (with /application removed) or just 'couchrest' if none is available. 53 | 54 | The library will try to detect a configuration file at `config/couchdb.yml` from the Rails root or `Dir.pwd`. Here you can configuration your database connection in a Rails-like way: 55 | 56 | development: 57 | protocol: 'https' 58 | host: sample.cloudant.com 59 | port: 443 60 | prefix: project 61 | suffix: test 62 | username: test 63 | password: user 64 | 65 | Note that the name of the database is either just the prefix and suffix combined or the prefix plus any text you specify using `use_database` method in your models with the suffix on the end. 66 | 67 | The example config above for example would use a database called "project_test". Here's an example using the `use_database` call: 68 | 69 | ```ruby 70 | class Project < CouchRest::Model::Base 71 | use_database 'sample' 72 | end 73 | 74 | # The database object would be provided as: 75 | Project.database #=> "https://test:user@sample.cloudant.com:443/project_sample_test" 76 | ``` 77 | 78 | ### Using instead of ActiveRecord in Rails 79 | 80 | A common use case for a new project is to replace ActiveRecord with CouchRest Model, although they should work perfectly well together. If you no longer want to depend on ActiveRecord or any of its sub-dependencies such as sqlite, update your `config/application.rb` so the top looks something like: 81 | 82 | ```ruby 83 | # We don't need active record, so load everything but: 84 | # require 'rails/all' 85 | require 'action_controller/railtie' 86 | require 'action_mailer/railtie' 87 | require 'rails/test_unit/railtie' 88 | ``` 89 | 90 | You'll then need to make sure any references to `config.active_record` are removed from your environment files. 91 | 92 | or alternatively below command do the same work 93 | ```ruby 94 | rails new --skip-active-record 95 | ``` 96 | Now in the gem file just add [couchrest_model] and you are good to go. 97 | 98 | ## Generators 99 | 100 | ### Configuration 101 | 102 | ```bash 103 | $ rails generate couchrest_model:config 104 | ``` 105 | 106 | ### Model 107 | 108 | ```bash 109 | $ rails generate model person --orm=couchrest_model 110 | ``` 111 | 112 | ## General Usage 113 | 114 | ```ruby 115 | require 'couchrest_model' 116 | 117 | class Cat < CouchRest::Model::Base 118 | 119 | property :name, String 120 | property :lives, Integer, :default => 9 121 | 122 | property :nicknames, [String] 123 | 124 | timestamps! 125 | 126 | design do 127 | view :by_name 128 | end 129 | 130 | end 131 | 132 | @cat = Cat.new(:name => 'Felix', :nicknames => ['so cute', 'sweet kitty']) 133 | 134 | @cat.new? # true 135 | @cat.save 136 | 137 | @cat['name'] # "Felix" 138 | 139 | @cat.nicknames << 'getoffdamntable' 140 | 141 | @cat = Cat.new 142 | @cat.update_attributes(:name => 'Felix', :random_text => 'feline') 143 | @cat.new? # false 144 | @cat.random_text # Raises error! 145 | 146 | # Fetching by views, loading all results into memory 147 | cats = Cat.by_name.all 148 | cats.first.name # "Felix" 149 | 150 | # Streaming views, for efficient memory usage 151 | Cat.by_name.all do |cat| 152 | puts cat.name 153 | end 154 | ``` 155 | 156 | ## Development 157 | 158 | ### Preparations 159 | 160 | CouchRest Model now comes with a Gemfile to help with development. If you want to make changes to the code, download a copy then run: 161 | 162 | ```bash 163 | bundle install 164 | ``` 165 | 166 | That should set everything up for `rake spec` to be run correctly. Update the couchrest_model.gemspec if your alterations 167 | use different gems. 168 | 169 | ### Testing 170 | 171 | The most complete documentation is the spec/ directory. To validate your CouchRest install, from the project root directory run `bundle install` to ensure all the development dependencies are available and then `rspec spec` or `bundle exec rspec spec`. 172 | 173 | We will not accept pull requests to the project without sufficient tests. 174 | 175 | ## Contact 176 | 177 | Please post bugs, suggestions and patches to the bug tracker at [http://github.com/couchrest/couchrest_model/issues](http://github.com/couchrest/couchrest_model/issues). 178 | 179 | Follow us on Twitter: [http://twitter.com/couchrest](http://twitter.com/couchrest) 180 | 181 | Also, check [https://twitter.com/search?q=couchrest](https://twitter.com/search?q=couchrest) 182 | 183 | 184 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'bundler' 3 | Bundler::GemHelper.install_tasks 4 | 5 | require 'rspec/core/rake_task' 6 | 7 | desc 'Default: run unit tests.' 8 | task :default => :spec 9 | 10 | desc "Run all specs" 11 | RSpec::Core::RakeTask.new do |t| 12 | t.pattern = 'spec/**/*_spec.rb' 13 | t.rspec_opts = ["-c", "-f progress"] 14 | end 15 | -------------------------------------------------------------------------------- /THANKS.md: -------------------------------------------------------------------------------- 1 | CouchRest THANKS 2 | ===================== 3 | 4 | CouchRest was originally developed by J. Chris Anderson 5 | and a number of other contributors. Many people further contributed to 6 | CouchRest by reporting problems, suggesting various improvements or submitting 7 | changes. A list of these people is included below. 8 | 9 | * [Matt Aimonetti](http://merbist.com/about/) 10 | * [Greg Borenstein](http://ideasfordozens.com) 11 | * [Geoffrey Grosenbach](http://nubyonrails.com/) 12 | * [Jonathan S. Katz](http://github.com/jkatz) 13 | * [Matt Lyon](http://mattly.tumblr.com/) 14 | * Simon Rozet (simon /at/ rozet /dot/ name) 15 | * [Marcos Tapajós](http://tapajos.me) 16 | * [Sam Lown](http://github.com/samlown) 17 | * [Will Leinweber](http://github.com/will) 18 | 19 | Patches are welcome. The primary source for this software project is [on Github](http://github.com/couchrest/couchrest) 20 | 21 | A lot of people have active forks - thank you all - even the patches I don't end up using are helpful. 22 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.2.0.beta3 2 | -------------------------------------------------------------------------------- /benchmarks/Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://rubygems.org' 3 | 4 | #gem 'couchrest_model', '2.0.4' 5 | gem 'couchrest_model', path: '../' 6 | gem 'faker' 7 | 8 | -------------------------------------------------------------------------------- /benchmarks/connections.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Simple Benchmarking Script. 3 | # 4 | # Meant to test performance for different types on connections under a typical 5 | # scenario of requesting multiple objects from the database in series. 6 | # 7 | # To run, use `bundle install` then: 8 | # 9 | # bundle exec ruby connections.rb 10 | # 11 | 12 | require 'rubygems' 13 | require 'bundler/setup' 14 | 15 | require 'benchmark' 16 | require 'couchrest_model' 17 | require 'faker' 18 | 19 | class SampleModel < CouchRest::Model::Base 20 | use_database "benchmark" 21 | 22 | property :name, String 23 | property :date, Date 24 | 25 | timestamps! 26 | 27 | design do 28 | view :by_name 29 | end 30 | end 31 | 32 | 33 | Benchmark.bm do |x| 34 | x.report("Create: ") do 35 | 100.times do |i| 36 | m = SampleModel.new( 37 | name: Faker::Name.name, 38 | date: Faker::Date.between(1.year.ago, Date.today) 39 | ) 40 | m.save! 41 | end 42 | end 43 | 44 | # Make sure the view is fresh 45 | SampleModel.by_name.limit(1).rows 46 | 47 | x.report("Fetch inc/docs:") do 48 | SampleModel.by_name.all.each do |doc| 49 | doc.to_json 50 | end 51 | end 52 | 53 | x.report("Fetch: ") do 54 | SampleModel.by_name.rows.each do |row| 55 | row.doc.to_json # Causes each doc to be fetched 56 | end 57 | end 58 | 59 | if CouchRest::Model::VERSION >= '2.1.0' 60 | x.report("Fetch w/block: ") do 61 | SampleModel.by_name.rows do |row| 62 | row.doc.to_json # Causes each doc to be fetched 63 | end 64 | end 65 | end 66 | end 67 | 68 | SampleModel.database.delete! 69 | 70 | -------------------------------------------------------------------------------- /benchmarks/dirty.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'benchmark' 5 | 6 | $:.unshift(File.dirname(__FILE__) + '/../lib') 7 | require 'couchrest_model' 8 | 9 | class BenchmarkCasted < Hash 10 | include CouchRest::Model::CastedModel 11 | 12 | property :name 13 | end 14 | 15 | class BenchmarkModel < CouchRest::Model::Base 16 | use_database CouchRest.database!(ENV['BENCHMARK_DB'] || "http://localhost:5984/test") 17 | 18 | property :string, String 19 | property :number, Integer 20 | property :casted, BenchmarkCasted 21 | property :casted_list, [BenchmarkCasted] 22 | end 23 | 24 | # set dirty configuration, return previous configuration setting 25 | def set_dirty(value) 26 | orig = nil 27 | CouchRest::Model::Base.configure do |config| 28 | orig = config.use_dirty 29 | config.use_dirty = value 30 | end 31 | BenchmarkModel.instance_eval do 32 | self.use_dirty = value 33 | end 34 | orig 35 | end 36 | 37 | def supports_dirty? 38 | CouchRest::Model::Base.respond_to?(:use_dirty) 39 | end 40 | 41 | def run_benchmark 42 | n = 50000 # property operation count 43 | db_n = 1000 # database operation count 44 | b = BenchmarkModel.new 45 | 46 | Benchmark.bm(30) do |x| 47 | 48 | # property assigning 49 | 50 | x.report("assign string:") do 51 | n.times { b.string = "test" } 52 | end 53 | 54 | next if ENV["BENCHMARK_STRING"] 55 | 56 | x.report("assign integer:") do 57 | n.times { b.number = 1 } 58 | end 59 | 60 | x.report("assign casted hash:") do 61 | n.times { b.casted = { 'name' => 'test' } } 62 | end 63 | 64 | x.report("assign casted hash list:") do 65 | n.times { b.casted_list = [{ 'name' => 'test' }] } 66 | end 67 | 68 | # property reading 69 | 70 | x.report("read string") do 71 | n.times { b.string } 72 | end 73 | 74 | x.report("read integer") do 75 | n.times { b.number } 76 | end 77 | 78 | x.report("read casted hash") do 79 | n.times { b.casted } 80 | end 81 | 82 | x.report("read casted hash list") do 83 | n.times { b.casted_list } 84 | end 85 | 86 | if ENV['BENCHMARK_DB'] 87 | # db writing 88 | x.report("write changed record to db") do 89 | db_n.times { |i| b.string = "test#{i}"; b.save } 90 | end 91 | 92 | x.report("write unchanged record to db") do 93 | db_n.times { b.save } 94 | end 95 | 96 | # db reading 97 | x.report("read record from db") do 98 | db_n.times { BenchmarkModel.find(b.id) } 99 | end 100 | 101 | end 102 | 103 | end 104 | end 105 | 106 | begin 107 | if supports_dirty? 108 | if !ENV['BENCHMARK_DIRTY_OFF'] 109 | set_dirty(true) 110 | puts "with use_dirty true" 111 | run_benchmark 112 | end 113 | set_dirty(false) 114 | puts "\nwith use_dirty false" 115 | end 116 | 117 | run_benchmark 118 | end 119 | -------------------------------------------------------------------------------- /couchrest_model.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = %q{couchrest_model} 3 | s.version = `cat VERSION`.strip 4 | s.license = "Apache License 2.0" 5 | 6 | s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version= 7 | s.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber", "Sam Lown"] 8 | s.date = File.mtime('VERSION') 9 | s.description = %q{CouchRest Model provides aditional features to the standard CouchRest Document class such as properties, view designs, associations, callbacks, typecasting and validations.} 10 | s.email = %q{jchris@apache.org} 11 | s.extra_rdoc_files = [ 12 | "LICENSE", 13 | "README.md", 14 | "THANKS.md" 15 | ] 16 | s.homepage = %q{http://github.com/couchrest/couchrest_model} 17 | #s.rubygems_version = %q{1.3.7} 18 | s.summary = %q{Extends the CouchRest Document class for advanced modelling.} 19 | 20 | s.files = `git ls-files`.split("\n") 21 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 22 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 23 | s.require_paths = ["lib"] 24 | 25 | s.add_dependency("couchrest", "2.0.1") 26 | s.add_dependency("activemodel", ">= 4.0.2") 27 | s.add_dependency("tzinfo", ">= 0.3.22") 28 | s.add_dependency("hashdiff", '~> 1.0', '>= 1.0.1') 29 | s.add_development_dependency("rspec", "~> 3.5.0") 30 | s.add_development_dependency("rack-test", ">= 0.5.7") 31 | s.add_development_dependency("rake", ">= 0.8.0", "< 11.0") 32 | s.add_development_dependency("test-unit") 33 | s.add_development_dependency("minitest", "> 4.1") #, "< 5.0") # For Kaminari and activesupport, pending removal 34 | s.add_development_dependency("kaminari", ">= 0.14.1", "< 0.16.0") 35 | s.add_development_dependency("mime-types", "< 3.0") # Mime-types > 3.0 don't bundle properly on JRuby 36 | end 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | test: 4 | build: 5 | context: . 6 | entrypoint: ["bundle", "exec"] 7 | command: ["rspec"] 8 | environment: 9 | RAILS_ENV: test 10 | COUCH_HOST: "http://couch:5984/" 11 | depends_on: 12 | - couch 13 | volumes: 14 | - ./:/usr/lib/couchrest_model 15 | networks: 16 | - couchrest_model 17 | couch: 18 | image: couchdb:1.6 19 | ports: 20 | - "5984" 21 | networks: 22 | - couchrest_model 23 | - default 24 | networks: 25 | couchrest_model: 26 | driver: bridge 27 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__),'lib', 'couchrest', 'model') 2 | -------------------------------------------------------------------------------- /lib/couchrest/model.rb: -------------------------------------------------------------------------------- 1 | 2 | module CouchRest 3 | 4 | module Model 5 | 6 | VERSION = File.read(File.expand_path('../../../VERSION', __FILE__)).strip 7 | 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /lib/couchrest/model/associations.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | # Basic support for relationships between CouchRest::Model::Base 4 | module Associations 5 | extend ActiveSupport::Concern 6 | 7 | module ClassMethods 8 | 9 | # Define an association that this object belongs to. 10 | # 11 | # An attribute will be created matching the name of the attribute 12 | # with '_id' on the end, or the foreign key (:foreign_key) provided. 13 | # 14 | # Searching for the assocated object is performed using a string 15 | # (:proxy) to be evaulated in the context of the owner. Typically 16 | # this will be set to the class name (:class_name), or determined 17 | # automatically if the owner belongs to a proxy object. 18 | # 19 | # If the association owner is proxied by another model, than an attempt will 20 | # be made to automatically determine the correct place to request 21 | # the documents. Typically, this is a method with the pluralized name of the 22 | # association inside owner's owner, or proxy. 23 | # 24 | # For example, imagine a company acts as a proxy for invoices and clients. 25 | # If an invoice belongs to a client, the invoice will need to access the 26 | # list of clients via the proxy. So a request to search for the associated 27 | # client from an invoice would look like: 28 | # 29 | # self.company.clients 30 | # 31 | # If the name of the collection proxy is not the pluralized assocation name, 32 | # it can be set with the :proxy_name option. 33 | # 34 | def belongs_to(attrib, *options) 35 | opts = merge_belongs_to_association_options(attrib, options.first) 36 | 37 | property(opts[:foreign_key], String, opts) 38 | 39 | create_association_property_setter(attrib, opts) 40 | create_belongs_to_getter(attrib, opts) 41 | create_belongs_to_setter(attrib, opts) 42 | end 43 | 44 | # Provide access to a collection of objects where the associated 45 | # property contains a list of the collection item ids. 46 | # 47 | # The following: 48 | # 49 | # collection_of :groups 50 | # 51 | # creates a pseudo property called "groups" which allows access 52 | # to a CollectionOfProxy object. Adding, replacing or removing entries in this 53 | # proxy will cause the matching property array, in this case "group_ids", to 54 | # be kept in sync. 55 | # 56 | # Any manual changes made to the collection ids property (group_ids), unless replaced, will require 57 | # a reload of the CollectionOfProxy for the two sets of data to be in sync: 58 | # 59 | # group_ids = ['123'] 60 | # groups == [Group.get('123')] 61 | # group_ids << '321' 62 | # groups == [Group.get('123')] 63 | # groups(true) == [Group.get('123'), Group.get('321')] 64 | # 65 | # Of course, saving the parent record will store the collection ids as they are 66 | # found. 67 | # 68 | # The CollectionOfProxy supports the following array functions, anything else will cause 69 | # a mismatch between the collection objects and collection ids: 70 | # 71 | # groups << obj 72 | # groups.push obj 73 | # groups.unshift obj 74 | # groups[0] = obj 75 | # groups.pop == obj 76 | # groups.shift == obj 77 | # 78 | # Addtional options match those of the the belongs_to method. 79 | # 80 | # NOTE: This method is *not* recommended for large collections or collections that change 81 | # frequently! Use with prudence. 82 | # 83 | def collection_of(attrib, *options) 84 | opts = merge_belongs_to_association_options(attrib, options.first) 85 | opts[:foreign_key] = opts[:foreign_key].pluralize 86 | opts[:readonly] = true 87 | 88 | property(opts[:foreign_key], [String], opts) 89 | 90 | create_association_property_setter(attrib, opts) 91 | create_collection_of_getter(attrib, opts) 92 | create_collection_of_setter(attrib, opts) 93 | end 94 | 95 | 96 | private 97 | 98 | def merge_belongs_to_association_options(attrib, options = nil) 99 | opts = { 100 | :foreign_key => attrib.to_s.singularize + '_id', 101 | :class_name => attrib.to_s.singularize.camelcase, 102 | :proxy_name => attrib.to_s.pluralize, 103 | :allow_blank => false 104 | } 105 | opts.merge!(options) if options.is_a?(Hash) 106 | 107 | # Generate a string for the proxy method call 108 | # Assumes that the proxy_owner_method from "proxyable" is available. 109 | if opts[:proxy].to_s.empty? 110 | opts[:proxy] = if proxy_owner_method 111 | "self.#{proxy_owner_method}.#{opts[:proxy_name]}" 112 | else 113 | opts[:class_name] 114 | end 115 | end 116 | 117 | opts 118 | end 119 | 120 | ### Generic support methods 121 | 122 | def create_association_property_setter(attrib, options) 123 | # ensure CollectionOfProxy is nil, ready to be reloaded on request 124 | class_eval <<-EOS, __FILE__, __LINE__ + 1 125 | def #{options[:foreign_key]}=(value) 126 | @#{attrib} = nil 127 | write_attribute("#{options[:foreign_key]}", value) 128 | end 129 | EOS 130 | end 131 | 132 | ### belongs_to support methods 133 | 134 | def create_belongs_to_getter(attrib, options) 135 | class_eval <<-EOS, __FILE__, __LINE__ + 1 136 | def #{attrib} 137 | @#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : #{options[:proxy]}.get(self.#{options[:foreign_key]}) 138 | end 139 | EOS 140 | end 141 | 142 | def create_belongs_to_setter(attrib, options) 143 | class_eval <<-EOS, __FILE__, __LINE__ + 1 144 | def #{attrib}=(value) 145 | self.#{options[:foreign_key]} = value.nil? ? nil : value.id 146 | @#{attrib} = value 147 | end 148 | EOS 149 | end 150 | 151 | ### collection_of support methods 152 | 153 | def create_collection_of_getter(attrib, options) 154 | class_eval <<-EOS, __FILE__, __LINE__ + 1 155 | def #{attrib}(reload = false) 156 | return @#{attrib} unless @#{attrib}.nil? or reload 157 | ary = self.#{options[:foreign_key]}.collect{|i| #{options[:proxy]}.get(i)} 158 | @#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(ary, find_property('#{options[:foreign_key]}'), self) 159 | end 160 | EOS 161 | end 162 | 163 | def create_collection_of_setter(attrib, options) 164 | class_eval <<-EOS, __FILE__, __LINE__ + 1 165 | def #{attrib}=(value) 166 | @#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(value, find_property('#{options[:foreign_key]}'), self) 167 | end 168 | EOS 169 | end 170 | 171 | end 172 | 173 | end 174 | 175 | # Special proxy for a collection of items so that adding and removing 176 | # to the list automatically updates the associated property. 177 | class CollectionOfProxy < CastedArray 178 | 179 | def initialize(array, property, parent) 180 | (array ||= []).compact! 181 | super(array, property, parent) 182 | self.casted_by_attribute = [] # replace the original array! 183 | array.compact.each do |obj| 184 | check_obj(obj) 185 | casted_by_attribute << obj.id 186 | end 187 | end 188 | 189 | def << obj 190 | check_obj(obj) 191 | casted_by_attribute << obj.id 192 | super(obj) 193 | end 194 | 195 | def push(obj) 196 | check_obj(obj) 197 | casted_by_attribute.push obj.id 198 | super(obj) 199 | end 200 | 201 | def unshift(obj) 202 | check_obj(obj) 203 | casted_by_attribute.unshift obj.id 204 | super(obj) 205 | end 206 | 207 | def []= index, obj 208 | check_obj(obj) 209 | casted_by_attribute[index] = obj.id 210 | super(index, obj) 211 | end 212 | 213 | def pop 214 | casted_by_attribute.pop 215 | super 216 | end 217 | 218 | def shift 219 | casted_by_attribute.shift 220 | super 221 | end 222 | 223 | protected 224 | 225 | def casted_by_attribute=(value) 226 | casted_by.write_attribute(casted_by_property, value) 227 | end 228 | 229 | def casted_by_attribute 230 | casted_by.read_attribute(casted_by_property) 231 | end 232 | 233 | def check_obj(obj) 234 | raise "Object cannot be added to #{casted_by.class.to_s}##{casted_by_property.to_s} collection unless saved" if obj.new? 235 | end 236 | 237 | # Override CastedArray instantiation_and_cast method for a simpler 238 | # version that will not try to cast the model. 239 | def instantiate_and_cast(obj) 240 | obj.casted_by = casted_by if obj.respond_to?(:casted_by) 241 | obj.casted_by_property = casted_by_property if obj.respond_to?(:casted_by_property) 242 | obj 243 | end 244 | 245 | end 246 | 247 | end 248 | 249 | end 250 | -------------------------------------------------------------------------------- /lib/couchrest/model/base.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | class Base < CouchRest::Document 4 | 5 | include ActiveModel::Conversion 6 | 7 | extend Translation 8 | 9 | include Configuration 10 | include Connection 11 | include Persistence 12 | include DocumentQueries 13 | include ExtendedAttachments 14 | include Proxyable 15 | include PropertyProtection 16 | include Associations 17 | include Validations 18 | include Callbacks 19 | include Designs 20 | include CastedBy 21 | include Dirty 22 | 23 | 24 | def self.subclasses 25 | @subclasses ||= [] 26 | end 27 | 28 | def self.inherited(subklass) 29 | super 30 | subklass.send(:include, Properties) 31 | 32 | subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1 33 | def self.inherited(subklass) 34 | super 35 | subklass.properties = self.properties.dup 36 | # This is nasty: 37 | subklass._validators = self._validators.dup 38 | end 39 | EOS 40 | subclasses << subklass 41 | end 42 | 43 | # Instantiate a new CouchRest::Model::Base by preparing all properties 44 | # using the provided document hash. 45 | # 46 | # Options supported: 47 | # 48 | # * :write_all_attributes, true when data comes directly from database so we can set protected and read-only attributes. 49 | # * :database, provide an alternative database 50 | # 51 | # If a block is provided the new model will be passed into the 52 | # block so that it can be populated. 53 | def initialize(attributes = {}, options = {}, &block) 54 | super() 55 | 56 | # Always force the type of model 57 | self[self.model_type_key] = self.class.model_type_value 58 | 59 | # Some instances may require a different database 60 | self.database = options[:database] unless options[:database].nil? 61 | 62 | # Deal with the attributes 63 | write_attributes_for_initialization(attributes, options) 64 | 65 | yield self if block_given? 66 | 67 | after_initialize if respond_to?(:after_initialize) 68 | run_callbacks(:initialize) { self } 69 | end 70 | 71 | def self.build(attrs = {}, options = {}, &block) 72 | 73 | 74 | end 75 | 76 | alias :new_record? :new? 77 | alias :new_document? :new? 78 | 79 | # Compare this model with another by confirming to see 80 | # if the IDs and their databases match! 81 | # 82 | # Camparison of the database is required in case the 83 | # model has been proxied or loaded elsewhere. 84 | # 85 | # A Basic CouchRest document will only ever compare using 86 | # a Hash comparison on the attributes. 87 | def == other 88 | return false unless other.is_a?(Base) 89 | if id.nil? && other.id.nil? 90 | # no ids? assume comparing nested and revert to hash comparison 91 | to_hash == other.to_hash 92 | else 93 | database == other.database && id == other.id 94 | end 95 | end 96 | alias :eql? :== 97 | 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/couchrest/model/callbacks.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module CouchRest #:nodoc: 4 | module Model #:nodoc: 5 | 6 | module Callbacks 7 | extend ActiveSupport::Concern 8 | 9 | CALLBACKS = [ 10 | :before_validation, :after_validation, 11 | :after_initialize, 12 | :before_create, :around_create, :after_create, 13 | :before_destroy, :around_destroy, :after_destroy, 14 | :before_save, :around_save, :after_save, 15 | :before_update, :around_update, :after_update, 16 | ] 17 | 18 | included do 19 | extend ActiveModel::Callbacks 20 | include ActiveModel::Validations::Callbacks 21 | 22 | define_model_callbacks :initialize, :only => :after 23 | define_model_callbacks :create, :destroy, :save, :update 24 | end 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/couchrest/model/casted_array.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Wrapper around Array so that the casted_by attribute is set in all 3 | # elements of the array. 4 | # 5 | 6 | module CouchRest::Model 7 | class CastedArray < Array 8 | include CouchRest::Model::Configuration 9 | include CouchRest::Model::CastedBy 10 | include CouchRest::Model::Dirty 11 | 12 | def initialize(array, property, parent = nil) 13 | self.casted_by_property = property 14 | self.casted_by = parent unless parent.nil? 15 | super(array) 16 | end 17 | 18 | # Adding new entries 19 | 20 | def << obj 21 | super(instantiate_and_cast(obj)) 22 | end 23 | 24 | def push(obj) 25 | super(instantiate_and_cast(obj)) 26 | end 27 | 28 | def unshift(obj) 29 | super(instantiate_and_cast(obj)) 30 | end 31 | 32 | def []=(index, obj) 33 | super(index, instantiate_and_cast(obj)) 34 | end 35 | 36 | def insert(index, *args) 37 | values = args.map{|obj| instantiate_and_cast(obj)} 38 | super(index, *values) 39 | end 40 | 41 | def build(*args) 42 | obj = casted_by_property.build(*args) 43 | self.push(obj) 44 | obj 45 | end 46 | 47 | def as_couch_json 48 | map{ |v| (v.respond_to?(:as_couch_json) ? v.as_couch_json : v)} 49 | end 50 | 51 | # Overwrite the standard dirty tracking clearing. 52 | # We don't have any properties, but we do need to check 53 | # entries in our array. 54 | def clear_changes_information 55 | if use_dirty? 56 | each do |val| 57 | if val.respond_to?(:clear_changes_information) 58 | val.clear_changes_information 59 | end 60 | end 61 | @original_change_data = current_change_data 62 | else 63 | @original_change_data = nil 64 | end 65 | end 66 | 67 | protected 68 | 69 | def instantiate_and_cast(obj) 70 | property = casted_by_property 71 | if casted_by && property && obj.class != property.type 72 | property.cast_value(casted_by, obj) 73 | else 74 | obj.casted_by = casted_by if obj.respond_to?(:casted_by) 75 | obj.casted_by_property = casted_by_property if obj.respond_to?(:casted_by_property) 76 | obj 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/couchrest/model/casted_by.rb: -------------------------------------------------------------------------------- 1 | 2 | module CouchRest::Model 3 | module CastedBy 4 | extend ActiveSupport::Concern 5 | included do 6 | self.send(:attr_accessor, :casted_by) 7 | self.send(:attr_accessor, :casted_by_property) 8 | end 9 | 10 | # Gets a reference to the actual document in the DB 11 | # Calls up to the next document if there is one, 12 | # Otherwise we're at the top and we return self 13 | def base_doc 14 | return self if base_doc? 15 | casted_by ? casted_by.base_doc : nil 16 | end 17 | 18 | # Checks if we're the top document 19 | def base_doc? 20 | !casted_by 21 | end 22 | 23 | # Provide the property this casted model instance has been 24 | # used by. If it has not been set, search through the 25 | # casted_by objects properties to try and find it. 26 | #def casted_by_property 27 | # return nil unless casted_by 28 | # attrs = casted_by.attributes 29 | # @casted_by_property ||= casted_by.properties.detect{ |k| attrs[k.to_s] === self } 30 | #end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/couchrest/model/configuration.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | 3 | # CouchRest Model Configuration support, stolen from Carrierwave by jnicklas 4 | # http://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/uploader/configuration.rb 5 | 6 | module Model 7 | module Configuration 8 | extend ActiveSupport::Concern 9 | 10 | included do 11 | add_config :model_type_key 12 | add_config :mass_assign_any_attribute 13 | add_config :auto_update_design_doc 14 | add_config :environment 15 | add_config :connection 16 | add_config :connection_config_file 17 | add_config :time_fraction_digits 18 | add_config :disable_dirty_tracking 19 | 20 | configure do |config| 21 | config.model_type_key = 'type' # was 'couchrest-type' 22 | config.mass_assign_any_attribute = false 23 | config.auto_update_design_doc = true 24 | config.time_fraction_digits = 3 25 | config.disable_dirty_tracking = false 26 | 27 | config.environment = :development 28 | config.connection_config_file = File.join(Dir.pwd, 'config', 'couchdb.yml') 29 | config.connection = { 30 | :protocol => 'http', 31 | :host => 'localhost', 32 | :port => '5984', 33 | :prefix => 'couchrest', 34 | :suffix => nil, 35 | :join => '_', 36 | :username => nil, 37 | :password => nil, 38 | :persistent => true 39 | } 40 | end 41 | end 42 | 43 | module ClassMethods 44 | 45 | def add_config(name) 46 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 47 | def self.#{name}(value=nil) 48 | @#{name} = value if value 49 | return @#{name} if self.object_id == #{self.object_id} || defined?(@#{name}) 50 | name = superclass.#{name} 51 | return nil if name.nil? && !instance_variable_defined?("@#{name}") 52 | @#{name} = name && !name.is_a?(Module) && !name.is_a?(Symbol) && !name.is_a?(Numeric) && !name.is_a?(TrueClass) && !name.is_a?(FalseClass) ? name.dup : name 53 | end 54 | 55 | def self.#{name}=(value) 56 | @#{name} = value 57 | end 58 | 59 | def #{name} 60 | self.class.#{name} 61 | end 62 | RUBY 63 | end 64 | 65 | def configure 66 | yield self 67 | end 68 | end 69 | 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/couchrest/model/connection.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module Connection 4 | extend ActiveSupport::Concern 5 | 6 | def server 7 | self.class.server 8 | end 9 | 10 | module ClassMethods 11 | 12 | # Overwrite the CouchRest::Document.use_database method so that a database 13 | # name can be provided instead of a full connection. 14 | # We prepare the database immediatly, so ensure any connection details 15 | # are provided in advance. 16 | # Note that this will not work correctly with proxied models. 17 | def use_database(db) 18 | @database = prepare_database(db) 19 | end 20 | 21 | # Overwrite the default database method so that it always 22 | # provides something from the configuration. 23 | # It will try to inherit the database from an ancester 24 | # unless the use_database method has been used. 25 | def database 26 | @database ||= prepare_database(super) 27 | end 28 | 29 | def server 30 | @server ||= ServerPool.instance[prepare_server_uri] 31 | end 32 | 33 | def prepare_database(db = nil) 34 | if db.nil? || db.is_a?(String) || db.is_a?(Symbol) 35 | self.server.database!(prepare_database_name(db)) 36 | else 37 | db 38 | end 39 | end 40 | 41 | protected 42 | 43 | def prepare_database_name(base) 44 | conf = connection_configuration 45 | [conf[:prefix], base.to_s, conf[:suffix]].reject{|s| s.to_s.empty?}.join(conf[:join]) 46 | end 47 | 48 | def prepare_server_uri 49 | conf = connection_configuration 50 | userinfo = [conf[:username], conf[:password]].compact.join(':') 51 | userinfo += '@' unless userinfo.empty? 52 | "#{conf[:protocol]}://#{userinfo}#{conf[:host]}:#{conf[:port]}" 53 | end 54 | 55 | def connection_configuration 56 | @connection_configuration ||= 57 | self.connection.merge( 58 | (load_connection_config_file[environment.to_sym] || {}).symbolize_keys 59 | ) 60 | end 61 | 62 | def load_connection_config_file 63 | file = connection_config_file 64 | ConnectionConfig.instance[file] 65 | end 66 | 67 | end 68 | 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/couchrest/model/connection_config.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | 4 | # Thead safe caching of connection configuration files. 5 | class ConnectionConfig 6 | include Singleton 7 | 8 | def initialize 9 | @config_files = {} 10 | @mutex = Mutex.new 11 | end 12 | 13 | def [](file) 14 | @mutex.synchronize do 15 | @config_files[file] ||= load_config(file) 16 | end 17 | end 18 | 19 | private 20 | 21 | def load_config(file) 22 | if File.exists?(file) 23 | YAML::load(ERB.new(IO.read(file)).result).symbolize_keys 24 | else 25 | { } 26 | end 27 | end 28 | 29 | end 30 | 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/couchrest/model/core_extensions/hash.rb: -------------------------------------------------------------------------------- 1 | # This file contains various hacks for Rails compatibility. 2 | class Hash 3 | # Hack so that CouchRest::Document, which descends from Hash, 4 | # doesn't appear to Rails routing as a Hash of options 5 | def self.===(other) 6 | return false if self == Hash && other.is_a?(CouchRest::Document) 7 | super 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/couchrest/model/core_extensions/time_parsing.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module CoreExtensions 4 | 5 | module TimeParsing 6 | 7 | # Attemtps to parse a time string in ISO8601 format. 8 | # If no match is found, the standard time parse will be used. 9 | # 10 | # Times, unless provided with a time zone, are assumed to be in 11 | # UTC. 12 | # 13 | # Uses String#to_r on seconds portion to avoid rounding errors. Eg: 14 | # Time.parse_iso8601("2014-12-11T16:54:54.549Z").as_json 15 | # => "2014-12-11T16:54:54.548Z" 16 | # 17 | # See: https://bugs.ruby-lang.org/issues/7829 18 | # 19 | 20 | def parse_iso8601(string) 21 | if (string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2}(\.\d+)?)(Z| ?([\+|\s|\-])?(\d{2}):?(\d{2}))?/) 22 | # $1 = year 23 | # $2 = month 24 | # $3 = day 25 | # $4 = hours 26 | # $5 = minutes 27 | # $6 = seconds (with $7 for fraction) 28 | # $8 = UTC or Timezone 29 | # $9 = time zone direction 30 | # $10 = tz difference hours 31 | # $11 = tz difference minutes 32 | 33 | if $8 == 'Z' || $8.to_s.empty? 34 | utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_r) 35 | else 36 | new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_r, "#{$9 == '-' ? '-' : '+'}#{$10}:#{$11}") 37 | end 38 | else 39 | parse(string) 40 | end 41 | end 42 | 43 | end 44 | 45 | end 46 | end 47 | end 48 | 49 | Time.class_eval do 50 | extend CouchRest::Model::CoreExtensions::TimeParsing 51 | 52 | # Override the ActiveSupport's Time#as_json method to ensure that we *always* encode 53 | # using the iso8601 format and include fractional digits (3 by default). 54 | # 55 | # Including miliseconds in Time is very important for CouchDB to ensure that order 56 | # is preserved between models created in the same second. 57 | # 58 | # The number of fraction digits can be set by providing it in the options: 59 | # 60 | # time.as_json(:fraction_digits => 6) 61 | # 62 | # The CouchRest Model +time_fraction_digits+ configuration option is used for the 63 | # default fraction. Given the global nature of Time#as_json method, this configuration 64 | # option can only be set for the whole project. 65 | # 66 | # CouchRest::Model::Base.time_fraction_digits = 6 67 | # 68 | 69 | def as_json(options = {}) 70 | digits = options ? options[:fraction_digits] : nil 71 | fraction = digits || CouchRest::Model::Base.time_fraction_digits 72 | xmlschema(fraction) 73 | end 74 | 75 | end 76 | 77 | -------------------------------------------------------------------------------- /lib/couchrest/model/design.rb: -------------------------------------------------------------------------------- 1 | 2 | module CouchRest 3 | module Model 4 | 5 | class Design < ::CouchRest::Design 6 | include ::CouchRest::Model::Designs::Migrations 7 | 8 | # The model Class that this design belongs to and method name 9 | attr_accessor :model, :method_name 10 | 11 | # Can this design save itself to the database? 12 | # If false, the design will be loaded automatically before a view is executed. 13 | attr_accessor :auto_update 14 | 15 | 16 | # Instantiate a new design document for this model 17 | def initialize(model, prefix = nil) 18 | self.model = model 19 | self.method_name = self.class.method_name(prefix) 20 | @lock = Mutex.new 21 | suffix = prefix ? "_#{prefix}" : '' 22 | self["_id"] = "_design/#{model.to_s}#{suffix}" 23 | apply_defaults 24 | end 25 | 26 | def sync(db = nil) 27 | if auto_update 28 | db ||= database 29 | if cache_checksum(db) != checksum 30 | # Only allow one thread to update the design document at a time 31 | @lock.synchronize { sync!(db) } 32 | set_cache_checksum(db, checksum) 33 | end 34 | end 35 | self 36 | end 37 | 38 | def sync!(db = nil) 39 | db ||= database 40 | 41 | # Load up the last copy. We never blindly overwrite the remote copy 42 | # as it may contain views that are not used or known about by 43 | # our model. 44 | 45 | doc = load_from_database(db) 46 | 47 | if !doc || doc['couchrest-hash'] != checksum 48 | # We need to save something 49 | if doc 50 | # Different! Update. 51 | doc.merge!(to_hash) 52 | else 53 | # No previous doc, use a *copy* of our version. 54 | # Using a copy prevents reverse updates. 55 | doc = to_hash.dup 56 | end 57 | db.save_doc(doc) 58 | end 59 | 60 | self 61 | end 62 | 63 | def checksum 64 | sum = self['couchrest-hash'] 65 | if sum && (@_original_hash == to_hash) 66 | sum 67 | else 68 | checksum! 69 | end 70 | end 71 | 72 | def database 73 | model.database 74 | end 75 | 76 | # Override the default #uri method for one that accepts 77 | # the current database. 78 | # This is used by the caching code. 79 | def uri(db = database) 80 | "#{db.root}/#{self['_id']}" 81 | end 82 | 83 | 84 | ######## VIEW HANDLING ######## 85 | 86 | # Create a new view object. 87 | # This overrides the normal CouchRest Design view method 88 | def view(name, opts = {}) 89 | CouchRest::Model::Designs::View.new(self, model, opts, name) 90 | end 91 | 92 | # Helper method to provide a list of all the views 93 | def view_names 94 | self['views'].keys 95 | end 96 | 97 | def has_view?(name) 98 | view_names.include?(name.to_s) 99 | end 100 | 101 | # Add the specified view to the design doc the definition was made in 102 | # and create quick access methods in the model. 103 | def create_view(name, opts = {}) 104 | Designs::View.define_and_create(self, name, opts) 105 | end 106 | 107 | ######## FILTER HANDLING ######## 108 | 109 | def create_filter(name, function) 110 | filters = (self['filters'] ||= {}) 111 | filters[name.to_s] = function 112 | end 113 | 114 | ######## VIEW LIBS ######### 115 | 116 | def create_view_lib(name, function) 117 | filters = (self['views']['lib'] ||= {}) 118 | filters[name.to_s] = function 119 | end 120 | 121 | protected 122 | 123 | def load_from_database(db = database, id = nil) 124 | id ||= self['_id'] 125 | db.get(id) 126 | end 127 | 128 | # Calculate and update the checksum of the Design document. 129 | # Used for ensuring the latest version has been sent to the database. 130 | # 131 | # This will generate a flatterned, ordered array of all the elements of the 132 | # design document, convert to string then generate an MD5 Hash. This should 133 | # result in a consisitent Hash accross all platforms. 134 | # 135 | def checksum! 136 | # Get a deep copy of hash to compare with 137 | @_original_hash = Marshal.load(Marshal.dump(to_hash)) 138 | # create a copy of basic elements 139 | base = self.dup 140 | base.delete('_id') 141 | base.delete('_rev') 142 | base.delete('couchrest-hash') 143 | flatten = 144 | lambda {|r| 145 | (recurse = lambda {|v| 146 | if v.is_a?(Hash) || v.is_a?(CouchRest::Document) 147 | v.to_a.map{|p| recurse.call(p)}.flatten 148 | elsif v.is_a?(Array) 149 | v.flatten.map{|p| recurse.call(p)} 150 | else 151 | v.to_s 152 | end 153 | }).call(r) 154 | } 155 | self['couchrest-hash'] = Digest::MD5.hexdigest(flatten.call(base).sort.join('')) 156 | end 157 | 158 | def cache 159 | Thread.current[:couchrest_design_cache] ||= {} 160 | end 161 | def cache_checksum(db) 162 | cache[uri(db)] 163 | end 164 | def set_cache_checksum(db, checksum) 165 | cache[uri(db)] = checksum 166 | end 167 | 168 | def apply_defaults 169 | merge!( 170 | "language" => "javascript", 171 | "views" => { } 172 | ) 173 | end 174 | 175 | 176 | class << self 177 | 178 | def method_name(prefix = nil) 179 | (prefix ? "#{prefix}_" : '') + 'design_doc' 180 | end 181 | 182 | end 183 | 184 | end 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /lib/couchrest/model/designs.rb: -------------------------------------------------------------------------------- 1 | 2 | #### NOTE Work in progress! Not yet used! 3 | 4 | module CouchRest 5 | module Model 6 | 7 | # A design block in CouchRest Model groups together the functionality of CouchDB's 8 | # design documents in a simple block definition. 9 | # 10 | # class Person < CouchRest::Model::Base 11 | # property :name 12 | # timestamps! 13 | # 14 | # design do 15 | # view :by_name 16 | # end 17 | # end 18 | # 19 | module Designs 20 | extend ActiveSupport::Concern 21 | 22 | 23 | module ClassMethods 24 | 25 | # Define a Design Document associated with the current model. 26 | # 27 | # This class method supports several cool features that make it much 28 | # easier to define design documents. 29 | # 30 | # Adding a prefix allows you to associate multiple design documents with the same 31 | # model. This is useful if you'd like to split your designs into seperate 32 | # use cases; one for regular search functions and a second for stats for example. 33 | # 34 | # # Create a design doc with id _design/Cats 35 | # design do 36 | # view :by_name 37 | # end 38 | # 39 | # # Create a design doc with id _design/Cats_stats 40 | # design :stats do 41 | # view :by_age, :reduce => :stats 42 | # end 43 | # 44 | # 45 | def design(*args, &block) 46 | opts = prepare_design_options(*args) 47 | 48 | # Store ourselves a copy of this design spec incase any other model inherits. 49 | (@_design_blocks ||= [ ]) << {:args => args, :block => block} 50 | 51 | mapper = DesignMapper.new(self, opts[:prefix]) 52 | mapper.instance_eval(&block) if block_given? 53 | 54 | # Create an 'all' view if no prefix and one has not been defined already 55 | mapper.view(:all) if opts[:prefix].nil? and !mapper.design_doc.has_view?(:all) 56 | end 57 | 58 | def inherited(model) 59 | super 60 | @_design_blocks ||= nil 61 | 62 | # Go through our design blocks and re-implement them in the child. 63 | unless @_design_blocks.nil? 64 | @_design_blocks.each do |row| 65 | model.design(*row[:args], &row[:block]) 66 | end 67 | end 68 | end 69 | 70 | # Override the default page pagination value: 71 | # 72 | # class Person < CouchRest::Model::Base 73 | # paginates_per 10 74 | # end 75 | # 76 | def paginates_per(val) 77 | @_default_per_page = val 78 | end 79 | 80 | # The models number of documents to return 81 | # by default when performing pagination. 82 | # Returns 25 unless explicitly overridden via paginates_per 83 | def default_per_page 84 | @_default_per_page || 25 85 | end 86 | 87 | def design_docs 88 | @_design_docs ||= [] 89 | end 90 | 91 | private 92 | 93 | def prepare_design_options(*args) 94 | options = {} 95 | if !args.first.is_a?(Hash) 96 | options[:prefix] = args.shift 97 | end 98 | options.merge(args.last) unless args.empty? 99 | prepare_source_paths(options) 100 | options 101 | end 102 | 103 | def prepare_source_paths(options) 104 | 105 | end 106 | 107 | end 108 | 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/couchrest/model/designs/design_mapper.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module Designs 4 | 5 | # Support class that allows for a model's design 6 | # definition to be converted into an actual design document. 7 | # 8 | # The methods called in a DesignMapper instance will relay 9 | # the parameters to the appropriate method in the design document. 10 | # 11 | class DesignMapper 12 | 13 | # Basic mapper attributes 14 | attr_accessor :model, :method, :prefix 15 | 16 | # Temporary variable storing the design doc 17 | attr_accessor :design_doc 18 | 19 | def initialize(model, prefix = nil) 20 | self.model = model 21 | self.prefix = prefix 22 | self.method = Design.method_name(prefix) 23 | 24 | create_model_design_doc_reader 25 | self.design_doc = model.send(method) || assign_model_design_doc 26 | end 27 | 28 | def disable_auto_update 29 | design_doc.auto_update = false 30 | end 31 | 32 | def enable_auto_update 33 | design_doc.auto_update = true 34 | end 35 | 36 | # Add the specified view to the design doc the definition was made in 37 | # and create quick access methods in the model. 38 | def view(name, opts = {}) 39 | design_doc.create_view(name, opts) 40 | end 41 | 42 | # Really simple design function that allows a filter 43 | # to be added. Filters are simple functions used when listening 44 | # to the _changes feed. 45 | # 46 | # No methods are created here, the design is simply updated. 47 | # See the CouchDB API for more information on how to use this. 48 | def filter(name, function) 49 | design_doc.create_filter(name, function) 50 | end 51 | 52 | # Define a new view re-usable lib for shared functions. 53 | def view_lib(name, function) 54 | design_doc.create_view_lib(name, function) 55 | end 56 | 57 | # Convenience wrapper to access model's type key option. 58 | def model_type_key 59 | model.model_type_key 60 | end 61 | 62 | protected 63 | 64 | # Create accessor in model and assign a new design doc. 65 | # New design doc is returned ready to use. 66 | def create_model_design_doc_reader 67 | model.instance_eval "def #{method}; @#{method} ||= nil; @#{method}; end" 68 | end 69 | 70 | def assign_model_design_doc 71 | doc = Design.new(model, prefix) 72 | model.instance_variable_set("@#{method}", doc) 73 | model.design_docs << doc 74 | 75 | # Set defaults 76 | doc.auto_update = model.auto_update_design_doc 77 | 78 | doc 79 | end 80 | 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/couchrest/model/designs/migrations.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module Designs 4 | 5 | # Design Document Migrations Support 6 | # 7 | # A series of methods used inside design documents in order to perform migrations. 8 | # 9 | module Migrations 10 | 11 | # Migrate the design document preventing downtime on a production 12 | # system. Typically this will be used when auto updates are disabled. 13 | # 14 | # Steps taken are: 15 | # 16 | # 1. Compare the checksum with the current version 17 | # 2. If different, create a new design doc with timestamp 18 | # 3. Wait until the view returns a result 19 | # 4. Copy over the original design doc 20 | # 21 | # If a block is provided, it will be called with the result of the migration: 22 | # 23 | # * :no_change - Nothing performed as there are no changes. 24 | # * :created - Add a new design doc as non existed 25 | # * :migrated - Migrated the existing design doc. 26 | # 27 | # This can be used for progressivly printing the results of the migration. 28 | # 29 | # After completion, either a "cleanup" Proc object will be provided to finalize 30 | # the process and copy the document into place, or simply nil if no cleanup is 31 | # required. For example: 32 | # 33 | # print "Synchronising Cat model designs: " 34 | # callback = Cat.design_doc.migrate do |res| 35 | # puts res.to_s 36 | # end 37 | # if callback 38 | # puts "Cleaning up." 39 | # callback.call 40 | # end 41 | # 42 | def migrate(db = nil, &block) 43 | db ||= database 44 | doc = load_from_database(db) 45 | cleanup = nil 46 | id = self['_id'] 47 | 48 | if !doc 49 | # make sure the checksum has been calculated 50 | checksum! if !self['couchrest-hash'] 51 | 52 | # no need to migrate, just save it 53 | new_doc = to_hash.dup 54 | db.save_doc(new_doc) 55 | 56 | result = :created 57 | elsif doc['couchrest-hash'] != checksum 58 | id += "_migration" 59 | 60 | # Delete current migration if there is one 61 | old_migration = load_from_database(db, id) 62 | db.delete_doc(old_migration) if old_migration 63 | 64 | # Save new design doc 65 | new_doc = doc.merge(to_hash) 66 | new_doc['_id'] = id 67 | new_doc.delete('_rev') 68 | db.save_doc(new_doc) 69 | 70 | # Proc definition to copy the migration doc over the original 71 | cleanup = Proc.new do 72 | db.copy_doc(new_doc, doc) 73 | db.delete_doc(new_doc) 74 | self 75 | end 76 | 77 | result = :migrated 78 | else 79 | # Already up to date 80 | result = :no_change 81 | end 82 | 83 | wait_for_view_update_completion(db, new_doc) 84 | 85 | yield result if block_given? 86 | 87 | cleanup 88 | end 89 | 90 | # Perform a single migration and inmediatly request a cleanup operation: 91 | # 92 | # print "Synchronising Cat model designs: " 93 | # Cat.design_doc.migrate! do |res| 94 | # puts res.to_s 95 | # end 96 | # 97 | def migrate!(db = nil, &block) 98 | callback = migrate(db, &block) 99 | if callback.is_a?(Proc) 100 | callback.call 101 | else 102 | callback 103 | end 104 | end 105 | 106 | private 107 | 108 | def wait_for_view_update_completion(db, attrs) 109 | if attrs && !attrs['views'].empty? 110 | # Prepare a design doc we can use 111 | doc = CouchRest::Design.new(attrs) 112 | doc.database = db 113 | 114 | # Request view, to trigger a *background* view update 115 | doc.view(doc['views'].keys.first, :limit => 1, :stale => "update_after") 116 | 117 | # Poll the view update process 118 | while true 119 | sleep 1 120 | info = doc.info 121 | if !info || !info['view_index'] 122 | raise "Migration error, unable to load design doc info: #{db.root}/#{doc.id}" 123 | end 124 | break if !info['view_index']['updater_running'] 125 | end 126 | end 127 | end 128 | 129 | 130 | end 131 | 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/couchrest/model/dirty.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | 4 | # This applies to both Model::Base and Model::CastedModel 5 | module Dirty 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | # The original attributes data hash, used for comparing changes. 10 | self.send(:attr_reader, :original_change_data) 11 | end 12 | 13 | def use_dirty? 14 | # Use the configuration option. 15 | !disable_dirty_tracking 16 | end 17 | 18 | # Provide an array of changes according to the hashdiff gem of the raw 19 | # json hash data. 20 | # If dirty tracking is disabled, this will always return nil. 21 | def changes 22 | if original_change_data.nil? 23 | nil 24 | else 25 | Hashdiff.diff(original_change_data, current_change_data) 26 | end 27 | end 28 | 29 | # Has this model changed? If dirty tracking is disabled, this method 30 | # will always return true. 31 | def changed? 32 | diff = changes 33 | diff.nil? || !diff.empty? 34 | end 35 | 36 | def clear_changes_information 37 | if use_dirty? 38 | # Recursively clear all change information 39 | self.class.properties.each do |property| 40 | val = read_attribute(property) 41 | if val.respond_to?(:clear_changes_information) 42 | val.clear_changes_information 43 | end 44 | end 45 | @original_change_data = current_change_data 46 | else 47 | @original_change_data = nil 48 | end 49 | end 50 | 51 | protected 52 | 53 | def current_change_data 54 | as_couch_json.as_json 55 | end 56 | 57 | module ClassMethods 58 | 59 | def create_dirty_property_methods(property) 60 | create_dirty_property_change_method(property) 61 | create_dirty_property_changed_method(property) 62 | create_dirty_property_was_method(property) 63 | end 64 | 65 | # For #property_change. 66 | # Tries to be a bit more efficient by directly comparing the properties 67 | # current value with that stored in the original change data. This also 68 | # maintains compatibility with ActiveModel change results. 69 | def create_dirty_property_change_method(property) 70 | define_method("#{property.name}_change") do 71 | val = read_attribute(property.name) 72 | if val.respond_to?(:changes) 73 | val.changes 74 | else 75 | if original_change_data.nil? 76 | nil 77 | else 78 | orig = original_change_data[property.name] 79 | cur = val.as_json 80 | if orig != cur 81 | [orig, cur] 82 | else 83 | [] 84 | end 85 | end 86 | end 87 | end 88 | end 89 | 90 | # For #property_was value. 91 | # Uses the original raw value, if available. 92 | def create_dirty_property_was_method(property) 93 | define_method("#{property.name}_was") do 94 | if original_change_data.nil? 95 | nil 96 | else 97 | original_change_data[property.name] 98 | end 99 | end 100 | end 101 | 102 | # For #property_changed? 103 | def create_dirty_property_changed_method(property) 104 | define_method("#{property.name}_changed?") do 105 | changes = send("#{property.name}_change") 106 | changes.nil? || !changes.empty? 107 | end 108 | end 109 | 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/couchrest/model/document_queries.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module DocumentQueries 4 | extend ActiveSupport::Concern 5 | 6 | module ClassMethods 7 | 8 | # Wrapper for the master design documents all method to provide 9 | # a total count of entries. 10 | def count 11 | all.count 12 | end 13 | 14 | # Wrapper for the master design document's first method on all view. 15 | def first 16 | all.first 17 | end 18 | 19 | # Wrapper for the master design document's last method on all view. 20 | def last 21 | all.last 22 | end 23 | 24 | # Load a document from the database by id 25 | # No exceptions will be raised if the document isn't found 26 | # 27 | # ==== Returns 28 | # Object:: if the document was found 29 | # or 30 | # Nil:: 31 | # 32 | # === Parameters 33 | # id:: Document ID 34 | def get(id) 35 | get!(id) 36 | rescue CouchRest::Model::DocumentNotFound 37 | nil 38 | end 39 | alias :find :get 40 | 41 | # Load a document from the database by id 42 | # An exception will be raised if the document isn't found 43 | # 44 | # ==== Returns 45 | # Object:: if the document was found 46 | # or 47 | # Exception 48 | # 49 | # === Parameters 50 | # id:: Document ID 51 | def get!(id) 52 | fetch_and_build_from_database(id, database) 53 | end 54 | alias :find! :get! 55 | 56 | 57 | # Load the document and build from the provided database. 58 | # 59 | # ==== Returns 60 | # Object:: if the document was found 61 | # or 62 | # Exception 63 | # 64 | # === Parameters 65 | # id:: Document ID 66 | # db:: optional option to pass a custom database to use 67 | def fetch_and_build_from_database(id, db) 68 | raise CouchRest::Model::DocumentNotFound if id.blank? 69 | raise CouchRest::Model::DatabaseNotDefined if db.nil? 70 | doc = db.get(id) or raise CouchRest::Model::DocumentNotFound 71 | build_from_database(doc) 72 | end 73 | 74 | end 75 | 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/couchrest/model/embeddable.rb: -------------------------------------------------------------------------------- 1 | module CouchRest::Model 2 | module Embeddable 3 | extend ActiveSupport::Concern 4 | 5 | # Include Attributes early to ensure super() will work 6 | include CouchRest::Attributes 7 | 8 | included do 9 | include CouchRest::Model::Configuration 10 | include CouchRest::Model::Properties 11 | include CouchRest::Model::PropertyProtection 12 | include CouchRest::Model::Associations 13 | include CouchRest::Model::Validations 14 | include CouchRest::Model::Callbacks 15 | include CouchRest::Model::CastedBy 16 | include CouchRest::Model::Dirty 17 | include CouchRest::Model::Callbacks 18 | 19 | class_eval do 20 | # Override CastedBy's base_doc? 21 | def base_doc? 22 | false # Can never be base doc! 23 | end 24 | end 25 | end 26 | 27 | # Initialize a new Casted Model. Accepts the same 28 | # options as CouchRest::Model::Base for preparing and initializing 29 | # attributes. 30 | def initialize(attributes = {}, options = {}) 31 | super() 32 | write_attributes_for_initialization(attributes, options) 33 | run_callbacks(:initialize) { self } 34 | end 35 | 36 | # False if the casted model has already 37 | # been saved in the containing document 38 | def new? 39 | casted_by.nil? ? true : casted_by.new? 40 | end 41 | alias :new_record? :new? 42 | 43 | def persisted? 44 | !new? 45 | end 46 | 47 | # The to_param method is needed for rails to generate resourceful routes. 48 | # In your controller, remember that it's actually the id of the document. 49 | def id 50 | return nil if base_doc.nil? 51 | base_doc.id 52 | end 53 | alias :to_key :id 54 | alias :to_param :id 55 | 56 | end # End Embeddable 57 | 58 | # Provide backwards compatability with previous versions (pre 1.1.0) 59 | module CastedModel 60 | extend ActiveSupport::Concern 61 | included do 62 | include CouchRest::Model::Embeddable 63 | end 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/couchrest/model/errors.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module CouchRest 3 | module Model 4 | module Errors 5 | 6 | class CouchRestModelError < StandardError; end 7 | 8 | # Raised when a persisence method ending in ! fails validation. The message 9 | # will contain the full error messages from the +Document+ in question. 10 | # 11 | # Example: 12 | # 13 | # Validations.new(person.errors) 14 | class Validations < CouchRestModelError 15 | attr_reader :document 16 | def initialize(document) 17 | @document = document 18 | super("Validation Failed: #{@document.errors.full_messages.join(", ")}") 19 | end 20 | end 21 | end 22 | 23 | class DocumentNotFound < Errors::CouchRestModelError; end 24 | 25 | class DatabaseNotDefined < Errors::CouchRestModelError 26 | def initialize(msg = nil) 27 | msg ||= "Database must be defined in model or view!" 28 | super(msg) 29 | end 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/couchrest/model/extended_attachments.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module ExtendedAttachments 4 | extend ActiveSupport::Concern 5 | 6 | # Add a file attachment to the current document. Expects 7 | # :file and :name to be included in the arguments. 8 | def create_attachment(args={}) 9 | raise ArgumentError unless args[:file] && args[:name] 10 | return if has_attachment?(args[:name]) 11 | set_attachment_attr(args) 12 | rescue ArgumentError 13 | raise ArgumentError, 'You must specify :file and :name' 14 | end 15 | 16 | # return all attachments 17 | def attachments 18 | self['_attachments'] ||= {} 19 | end 20 | 21 | # reads the data from an attachment 22 | def read_attachment(attachment_name) 23 | database.fetch_attachment(self, attachment_name) 24 | end 25 | 26 | # modifies a file attachment on the current doc 27 | def update_attachment(args={}) 28 | raise ArgumentError unless args[:file] && args[:name] 29 | return unless has_attachment?(args[:name]) 30 | delete_attachment(args[:name]) 31 | set_attachment_attr(args) 32 | rescue ArgumentError 33 | raise ArgumentError, 'You must specify :file and :name' 34 | end 35 | 36 | # deletes a file attachment from the current doc 37 | def delete_attachment(attachment_name) 38 | return unless attachments 39 | if attachments.include?(attachment_name) 40 | attachments.delete attachment_name 41 | end 42 | end 43 | 44 | # returns true if attachment_name exists 45 | def has_attachment?(attachment_name) 46 | !!(attachments && attachments[attachment_name] && !attachments[attachment_name].empty?) 47 | end 48 | 49 | # returns URL to fetch the attachment from 50 | def attachment_url(attachment_name) 51 | return unless has_attachment?(attachment_name) 52 | "#{database.root}/#{self.id}/#{attachment_name}" 53 | end 54 | 55 | # returns URI to fetch the attachment from 56 | def attachment_uri(attachment_name) 57 | return unless has_attachment?(attachment_name) 58 | "#{database.uri}/#{self.id}/#{attachment_name}" 59 | end 60 | 61 | private 62 | 63 | def get_mime_type(path) 64 | return nil if path.nil? 65 | type = ::MIME::Types.type_for(path) 66 | type.empty? ? nil : type.first.content_type 67 | end 68 | 69 | def set_attachment_attr(args) 70 | content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path) 71 | content_type ||= (get_mime_type(args[:name]) || 'text/plain') 72 | 73 | attachments[args[:name]] = { 74 | 'content_type' => content_type, 75 | 'data' => args[:file].read 76 | } 77 | end 78 | 79 | end # module ExtendedAttachments 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/couchrest/model/persistence.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module Persistence 4 | extend ActiveSupport::Concern 5 | 6 | # Create the document. Validation is enabled by default and will return 7 | # false if the document is not valid. If all goes well, the document will 8 | # be returned. 9 | def create(options = {}) 10 | return false unless perform_validations(options) 11 | run_callbacks :create do 12 | run_callbacks :save do 13 | set_unique_id if new? && respond_to?(:set_unique_id) 14 | result = database.save_doc(self) 15 | ret = (result["ok"] == true) ? self : false 16 | clear_changes_information if ret 17 | ret 18 | end 19 | end 20 | end 21 | 22 | # Creates the document in the db. Raises an exception 23 | # if the document is not created properly. 24 | def create!(options = {}) 25 | self.class.fail_validate!(self) unless self.create(options) 26 | end 27 | 28 | # Trigger the callbacks (before, after, around) 29 | # only if the document isn't new 30 | def update(options = {}) 31 | raise "Cannot save a destroyed document!" if destroyed? 32 | raise "Calling #{self.class.name}#update on document that has not been created!" if new? 33 | return false unless perform_validations(options) 34 | return true unless changed? 35 | run_callbacks :update do 36 | run_callbacks :save do 37 | result = database.save_doc(self) 38 | ret = result["ok"] == true 39 | clear_changes_information if ret 40 | ret 41 | end 42 | end 43 | end 44 | 45 | # Trigger the callbacks (before, after, around) and save the document 46 | def save(options = {}) 47 | self.new? ? create(options) : update(options) 48 | end 49 | 50 | # Saves the document to the db using save. Raises an exception 51 | # if the document is not saved properly. 52 | def save! 53 | self.class.fail_validate!(self) unless self.save 54 | true 55 | end 56 | 57 | # Deletes the document from the database. Runs the :destroy callbacks. 58 | def destroy 59 | run_callbacks :destroy do 60 | result = database.delete_doc(self) 61 | if result['ok'] 62 | @_destroyed = true 63 | self.freeze 64 | end 65 | result['ok'] 66 | end 67 | end 68 | 69 | def destroyed? 70 | !!@_destroyed 71 | end 72 | 73 | def persisted? 74 | !new? && !destroyed? 75 | end 76 | 77 | # Update the document's attributes and save. For example: 78 | # 79 | # doc.update_attributes :name => "Fred" 80 | # Is the equivilent of doing the following: 81 | # 82 | # doc.attributes = { :name => "Fred" } 83 | # doc.save 84 | # 85 | def update_attributes(hash) 86 | write_attributes(hash) 87 | save 88 | end 89 | 90 | # Reloads the attributes of this object from the database. 91 | # It doesn't override custom instance variables. 92 | # 93 | # Returns self. 94 | def reload 95 | write_attributes_for_initialization( 96 | database.get!(id), :write_all_attributes => true 97 | ) 98 | self 99 | rescue CouchRest::NotFound 100 | raise CouchRest::Model::DocumentNotFound 101 | end 102 | 103 | protected 104 | 105 | def perform_validations(options = {}) 106 | perform_validation = case options 107 | when Hash 108 | options[:validate] != false 109 | else 110 | options 111 | end 112 | perform_validation ? valid? : true 113 | end 114 | 115 | 116 | module ClassMethods 117 | 118 | # Creates a new instance, bypassing attribute protection and 119 | # uses the type field to determine which model to use to instanatiate 120 | # the new object. 121 | # 122 | # ==== Returns 123 | # a document instance 124 | # 125 | def build_from_database(doc = {}, options = {}, &block) 126 | src = doc[model_type_key] 127 | base = (src.blank? || src == model_type_value) ? self : src.constantize 128 | base.new(doc, options.merge(:write_all_attributes => true), &block) 129 | end 130 | 131 | # Defines an instance and save it directly to the database 132 | # 133 | # ==== Returns 134 | # returns the reloaded document 135 | def create(attributes = {}, &block) 136 | instance = new(attributes, &block) 137 | instance.create 138 | instance 139 | end 140 | 141 | # Defines an instance and save it directly to the database 142 | # 143 | # ==== Returns 144 | # returns the reloaded document or raises an exception 145 | def create!(attributes = {}, &block) 146 | instance = new(attributes, &block) 147 | instance.create! 148 | instance 149 | end 150 | 151 | # Name a method that will be called before the document is first saved, 152 | # which returns a string to be used for the document's _id. 153 | # 154 | # Because CouchDB enforces a constraint that each id must be unique, 155 | # this can be used to enforce eg: uniq usernames. Note that this id 156 | # must be globally unique across all document types which share a 157 | # database, so if you'd like to scope uniqueness to this class, you 158 | # should use the class name as part of the unique id. 159 | def unique_id(method = nil, &block) 160 | return if method.nil? && !block 161 | define_method :set_unique_id do 162 | uniqid = method.nil? ? block.call(self) : send(method) 163 | raise ArgumentError, "unique_id cannot be nil nor empty" if uniqid.blank? 164 | self['_id'] ||= uniqid 165 | end 166 | end 167 | 168 | # The value to use for this model's model_type_key. 169 | # By default, this shouls always be the string representation of the class, 170 | # but if you need anything special, overwrite this method. 171 | def model_type_value 172 | to_s 173 | end 174 | 175 | # Raise an error if validation failed. 176 | def fail_validate!(document) 177 | raise Errors::Validations.new(document) 178 | end 179 | end 180 | 181 | 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /lib/couchrest/model/property.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module CouchRest::Model 3 | class Property 4 | 5 | include ::CouchRest::Model::Typecast 6 | 7 | attr_reader :name, :type, :array, :read_only, :alias, :default, :casted, :init_method, :options, :allow_blank 8 | 9 | # Attribute to define. 10 | # All Properties are assumed casted unless the type is nil. 11 | def initialize(name, options = {}, &block) 12 | @name = name.to_s 13 | parse_options(options) 14 | parse_type(options, &block) 15 | self 16 | end 17 | 18 | def to_s 19 | name 20 | end 21 | 22 | def to_sym 23 | @_sym_name ||= name.to_sym 24 | end 25 | 26 | # Cast the provided value using the properties details. 27 | def cast(parent, value) 28 | return value unless casted 29 | if array 30 | if value.nil? 31 | value = [] 32 | elsif value.is_a?(Hash) 33 | # Assume provided as a params hash where key is index 34 | value = parameter_hash_to_array(value) 35 | elsif !value.is_a?(Array) 36 | raise "Expecting an array or keyed hash for property #{parent.class.name}##{self.name}" 37 | end 38 | arr = value.collect { |data| cast_value(parent, data) } 39 | arr.reject!{ |data| data.nil? } unless allow_blank 40 | # allow casted_by calls to be passed up chain by wrapping in CastedArray 41 | CastedArray.new(arr, self, parent) 42 | elsif !value.nil? 43 | cast_value(parent, value) 44 | end 45 | end 46 | 47 | # Cast an individual value 48 | def cast_value(parent, value) 49 | if !allow_blank && value.to_s.empty? 50 | nil 51 | else 52 | value = typecast_value(parent, self, value) 53 | associate_casted_value_to_parent(parent, value) 54 | end 55 | end 56 | 57 | def default_value 58 | return if default.nil? 59 | if default.class == Proc 60 | default.call 61 | else 62 | # TODO identify cause of mutex errors 63 | Marshal.load(Marshal.dump(default)) 64 | end 65 | end 66 | 67 | # Initialize a new instance of a property's type ready to be 68 | # used. If a proc is defined for the init method, it will be used instead of 69 | # a normal call to the class. 70 | def build(*args) 71 | raise StandardError, "Cannot build property without a class" if @type.nil? 72 | 73 | if @init_method.is_a?(Proc) 74 | @init_method.call(*args) 75 | else 76 | @type.send(@init_method, *args) 77 | end 78 | end 79 | 80 | private 81 | 82 | def parameter_hash_to_array(source) 83 | value = [ ] 84 | source.keys.each do |k| 85 | value[k.to_i] = source[k] 86 | end 87 | value.compact 88 | end 89 | 90 | def associate_casted_value_to_parent(parent, value) 91 | value.casted_by = parent if value.respond_to?(:casted_by) 92 | value.casted_by_property = self if value.respond_to?(:casted_by_property) 93 | value 94 | end 95 | 96 | def parse_type(options, &block) 97 | set_type_from_block(&block) if block_given? 98 | if @type.nil? 99 | @casted = false 100 | else 101 | @casted = true 102 | if @type.is_a?(Array) 103 | @type = @type.first || Object 104 | @array = true 105 | end 106 | raise "Defining a property type as a #{@type.class.name.humanize} is not supported in CouchRest Model!" if @type.class != Class 107 | end 108 | end 109 | 110 | def parse_options(options) 111 | @type = options.delete(:type) || options.delete(:cast_as) 112 | @array = !!options.delete(:array) 113 | @validation_format = options.delete(:format) if options[:format] 114 | @read_only = options.delete(:read_only) if options[:read_only] 115 | @alias = options.delete(:alias) if options[:alias] 116 | @default = options.delete(:default) unless options[:default].nil? 117 | @init_method = options.delete(:init_method) || 'new' 118 | @allow_blank = options[:allow_blank].nil? ? true : options.delete(:allow_blank) 119 | @options = options 120 | end 121 | 122 | 123 | def set_type_from_block(&block) 124 | @type = Class.new do 125 | include Embeddable 126 | end 127 | if block.arity == 1 # Traditional, with options 128 | @type.class_eval(&block) 129 | else 130 | @type.instance_eval(&block) 131 | end 132 | end 133 | 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/couchrest/model/property_protection.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module PropertyProtection 4 | extend ActiveSupport::Concern 5 | 6 | # Property protection from mass assignment to CouchRest::Model properties 7 | # 8 | # Protected methods will be removed from 9 | # * new 10 | # * update_attributes 11 | # * upate_attributes_without_saving 12 | # * attributes= 13 | # 14 | # There are two modes of protection 15 | # 1) Declare accessible poperties, and assume all unspecified properties are protected 16 | # property :name, :accessible => true 17 | # property :admin # this will be automatically protected 18 | # 19 | # 2) Declare protected properties, and assume all unspecified properties are accessible 20 | # property :name # this will not be protected 21 | # property :admin, :protected => true 22 | # 23 | # 3) Mix and match, and assume all unspecified properties are protected. 24 | # property :name, :accessible => true 25 | # property :admin, :protected => true # ignored 26 | # property :phone # this will be automatically protected 27 | # 28 | # Note: the timestamps! method protectes the created_at and updated_at properties 29 | 30 | 31 | def self.included(base) 32 | base.extend(ClassMethods) 33 | end 34 | 35 | module ClassMethods 36 | def accessible_properties 37 | props = properties.select { |prop| prop.options[:accessible] } 38 | if props.empty? 39 | props = properties.select { |prop| !prop.options[:protected] } 40 | end 41 | props 42 | end 43 | 44 | def protected_properties 45 | accessibles = accessible_properties 46 | properties.reject { |prop| accessibles.include?(prop) } 47 | end 48 | end 49 | 50 | def accessible_properties 51 | self.class.accessible_properties 52 | end 53 | 54 | def protected_properties 55 | self.class.protected_properties 56 | end 57 | 58 | # Return a new copy of the attributes hash with protected attributes 59 | # removed. 60 | def remove_protected_attributes(attributes) 61 | protected_names = protected_properties.map { |prop| prop.name } 62 | return attributes if protected_names.empty? or attributes.nil? 63 | 64 | attributes.reject do |property_name, property_value| 65 | protected_names.include?(property_name.to_s) 66 | end 67 | end 68 | 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/couchrest/model/proxyable.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | # :nodoc: Because I like inventing words 4 | module Proxyable 5 | extend ActiveSupport::Concern 6 | 7 | def proxy_database(assoc_name) 8 | raise StandardError, "Please set the #proxy_database_method" if self.class.proxy_database_method.nil? 9 | db_name = self.send(self.class.proxy_database_method) 10 | db_suffix = self.class.proxy_database_suffixes[assoc_name.to_sym] 11 | @_proxy_databases ||= {} 12 | @_proxy_databases[assoc_name.to_sym] ||= begin 13 | self.class.prepare_database([db_name, db_suffix].compact.reject(&:blank?).join(self.class.connection[:join])) 14 | end 15 | end 16 | 17 | module ClassMethods 18 | 19 | 20 | # Define a collection that will use the base model for the database connection 21 | # details. 22 | def proxy_for(assoc_name, options = {}) 23 | db_method = (options[:database_method] || "proxy_database").to_sym 24 | db_suffix = options[:database_suffix] || (options[:use_suffix] ? assoc_name.to_s : nil) 25 | options[:class_name] ||= assoc_name.to_s.singularize.camelize 26 | proxy_method_names << assoc_name.to_sym unless proxy_method_names.include?(assoc_name.to_sym) 27 | proxied_model_names << options[:class_name] unless proxied_model_names.include?(options[:class_name]) 28 | proxy_database_suffixes[assoc_name.to_sym] = db_suffix 29 | db_method_call = "#{db_method}(:#{assoc_name.to_s})" 30 | class_eval <<-EOS, __FILE__, __LINE__ + 1 31 | def #{assoc_name} 32 | @#{assoc_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(::#{options[:class_name]}, self, self.class.to_s.underscore, #{db_method_call}) 33 | end 34 | EOS 35 | end 36 | 37 | # Tell this model which other model to use as a base for the database 38 | # connection to use. 39 | def proxied_by(model_name, options = {}) 40 | raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name) || !proxy_owner_method.nil? 41 | self.proxy_owner_method = model_name 42 | attr_accessor :model_proxy 43 | attr_accessor model_name 44 | overwrite_database_reader(model_name) 45 | end 46 | 47 | # Define an a class variable accessor ready to be inherited and unique 48 | # for each Class using the base. 49 | # Perhaps there is a shorter way of writing this. 50 | def proxy_owner_method=(name); @proxy_owner_method = name; end 51 | def proxy_owner_method; @proxy_owner_method; end 52 | 53 | # Define the name of a method to call to determine the name of 54 | # the database to use as a proxy. 55 | def proxy_database_method(name = nil) 56 | @proxy_database_method = name if name 57 | @proxy_database_method 58 | end 59 | 60 | def proxy_method_names 61 | @proxy_method_names ||= [] 62 | end 63 | 64 | def proxied_model_names 65 | @proxied_model_names ||= [] 66 | end 67 | 68 | def proxy_database_suffixes 69 | @proxy_database_suffixes ||= {} 70 | end 71 | 72 | private 73 | 74 | # Ensure that no attempt is made to autoload a database connection 75 | # by overwriting it to provide a basic accessor. 76 | def overwrite_database_reader(model_name) 77 | class_eval <<-EOS, __FILE__, __LINE__ + 1 78 | def self.database 79 | raise StandardError, "#{self.to_s} database must be accessed via '#{model_name}' proxy" 80 | end 81 | EOS 82 | end 83 | 84 | end 85 | 86 | class ModelProxy 87 | 88 | attr_reader :model, :owner, :owner_name, :database 89 | 90 | def initialize(model, owner, owner_name, database) 91 | @model = model 92 | @owner = owner 93 | @owner_name = owner_name 94 | @database = database 95 | 96 | create_view_methods 97 | end 98 | 99 | # Base 100 | def new(attrs = {}, options = {}, &block) 101 | proxy_block_update(:new, attrs, options, &block) 102 | end 103 | 104 | def build_from_database(attrs = {}, options = {}, &block) 105 | proxy_block_update(:build_from_database, attrs, options, &block) 106 | end 107 | 108 | # From DocumentQueries (The old fashioned way) 109 | 110 | def count(opts = {}) 111 | all(opts).count 112 | end 113 | 114 | def first(opts = {}) 115 | all(opts).first 116 | end 117 | 118 | def last(opts = {}) 119 | all(opts).last 120 | end 121 | 122 | def get(id) 123 | get!(id) 124 | rescue CouchRest::Model::DocumentNotFound 125 | nil 126 | end 127 | alias :find :get 128 | 129 | def get!(id) 130 | proxy_update(@model.fetch_and_build_from_database(id, @database)) 131 | end 132 | alias :find! :get! 133 | 134 | protected 135 | 136 | def create_view_methods 137 | model.design_docs.each do |doc| 138 | doc.view_names.each do |name| 139 | class_eval <<-EOS, __FILE__, __LINE__ + 1 140 | def #{name}(opts = {}) 141 | model.#{name}({:proxy => self}.merge(opts)) 142 | end 143 | def find_#{name}(*key) 144 | #{name}.key(*key).first() 145 | end 146 | def find_#{name}!(*key) 147 | find_#{name}(*key) || raise(CouchRest::Model::DocumentNotFound) 148 | end 149 | EOS 150 | end 151 | end 152 | end 153 | 154 | # Update the document's proxy details, specifically, the fields that 155 | # link back to the original document. 156 | def proxy_update(doc) 157 | if doc && doc.is_a?(model) 158 | doc.database = @database 159 | doc.model_proxy = self 160 | doc.send("#{owner_name}=", owner) 161 | end 162 | doc 163 | end 164 | 165 | def proxy_update_all(docs) 166 | docs.each do |doc| 167 | proxy_update(doc) 168 | end 169 | end 170 | 171 | def proxy_block_update(method, *args, &block) 172 | model.send(method, *args) do |doc| 173 | proxy_update(doc) 174 | yield doc if block_given? 175 | end 176 | end 177 | 178 | private 179 | 180 | def method_missing(m, *args, &block) 181 | model.respond_to?(m) ? model.send(m, self, *args, &block) : super 182 | end 183 | 184 | end 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /lib/couchrest/model/server_pool.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | 4 | # Simple Server Pool with thread safety so that a single server 5 | # instance can be shared with multiple classes. 6 | class ServerPool 7 | include Singleton 8 | 9 | def initialize 10 | @servers = {} 11 | @mutex = Mutex.new 12 | end 13 | 14 | def [](url) 15 | @mutex.synchronize do 16 | @servers[url] ||= CouchRest::Server.new(url) 17 | end 18 | end 19 | 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/couchrest/model/support/couchrest_database.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Extend CouchRest's normal database delete! method to ensure any caches are 3 | # also emptied. Given that this is a rare event, and the consequences are not 4 | # very severe, we just completely empty the cache. 5 | # 6 | module CouchRest::Model 7 | module Support 8 | module Database 9 | 10 | def delete! 11 | Thread.current[:couchrest_design_cache] = { } 12 | super 13 | end 14 | 15 | end 16 | end 17 | end 18 | 19 | class CouchRest::Database 20 | prepend CouchRest::Model::Support::Database 21 | end 22 | -------------------------------------------------------------------------------- /lib/couchrest/model/translation.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module Translation 4 | include ActiveModel::Translation 5 | 6 | def lookup_ancestors #:nodoc: 7 | klass = self 8 | classes = [klass] 9 | return classes if klass == CouchRest::Model::Base 10 | 11 | while klass.superclass != CouchRest::Model::Base 12 | classes << klass = klass.superclass 13 | end 14 | classes 15 | end 16 | 17 | def i18n_scope 18 | :couchrest 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/couchrest/model/typecast.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module Typecast 4 | 5 | def typecast_value(parent, property, value) 6 | return nil if value.nil? 7 | type = property.type 8 | if value.instance_of?(type) || type == Object 9 | if type == Time && !value.utc? 10 | value.utc # Ensure Time is always in UTC 11 | else 12 | value 13 | end 14 | elsif type.respond_to?(:couchrest_typecast) 15 | type.couchrest_typecast(parent, property, value) 16 | elsif [String, Symbol, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(type) 17 | send('typecast_to_'+type.to_s.downcase, value) 18 | else 19 | property.build(value) 20 | end 21 | end 22 | 23 | protected 24 | 25 | # Typecast a value to an Integer 26 | def typecast_to_integer(value) 27 | typecast_to_numeric(value, :to_i) 28 | end 29 | 30 | # Typecast a value to a BigDecimal 31 | def typecast_to_bigdecimal(value) 32 | typecast_to_numeric(value, :to_d) 33 | end 34 | 35 | # Typecast a value to a Float 36 | def typecast_to_float(value) 37 | typecast_to_numeric(value, :to_f) 38 | end 39 | 40 | # Convert some kind of object to a number that of the type 41 | # provided. 42 | # 43 | # When a string is provided, It'll attempt to filter out 44 | # region specific details such as commas instead of points 45 | # for decimal places, text units, and anything else that is 46 | # not a number and a human could make out. 47 | # 48 | # Esentially, the aim is to provide some kind of sanitary 49 | # conversion from values in incoming http forms. 50 | # 51 | # If what we get makes no sense at all, nil it. 52 | def typecast_to_numeric(value, method) 53 | if value.is_a?(String) 54 | value = value.strip.gsub(/,/, '.').gsub(/[^\d\-\.]/, '').gsub(/\.(?!\d*\Z)/, '') 55 | value.empty? ? nil : value.send(method) 56 | elsif value.respond_to?(method) 57 | value.send(method) 58 | else 59 | nil 60 | end 61 | end 62 | 63 | # Typecast a value to a String 64 | def typecast_to_string(value) 65 | value.to_s 66 | end 67 | 68 | def typecast_to_symbol(value) 69 | value.kind_of?(Symbol) || !value.to_s.empty? ? value.to_sym : nil 70 | end 71 | 72 | # Typecast a value to a true or false 73 | def typecast_to_trueclass(value) 74 | if value.kind_of?(Integer) 75 | return true if value == 1 76 | return false if value == 0 77 | elsif value.respond_to?(:to_s) 78 | return true if %w[ true 1 t ].include?(value.to_s.downcase) 79 | return false if %w[ false 0 f ].include?(value.to_s.downcase) 80 | end 81 | nil 82 | end 83 | 84 | # Typecasts an arbitrary value to a DateTime. 85 | # Handles both Hashes and DateTime instances. 86 | # This is slow!! Use Time instead. 87 | def typecast_to_datetime(value) 88 | if value.is_a?(Hash) 89 | typecast_hash_to_datetime(value) 90 | else 91 | DateTime.parse(value.to_s) 92 | end 93 | rescue ArgumentError 94 | nil 95 | end 96 | 97 | # Typecasts an arbitrary value to a Date 98 | # Handles both Hashes and Date instances. 99 | def typecast_to_date(value) 100 | if value.is_a?(Hash) 101 | typecast_hash_to_date(value) 102 | elsif value.is_a?(Time) # sometimes people think date is time! 103 | value.to_date 104 | elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/ 105 | # Faster than parsing the date 106 | Date.new($1.to_i, $2.to_i, $3.to_i) 107 | else 108 | Date.parse(value) 109 | end 110 | rescue ArgumentError 111 | nil 112 | end 113 | 114 | # Typecasts an arbitrary value to a Time 115 | # Handles both Hashes and Time instances. 116 | def typecast_to_time(value) 117 | case value 118 | when Float # JSON oj already parses Time, FTW. 119 | Time.at(value).utc 120 | when Hash 121 | typecast_hash_to_time(value) 122 | else 123 | Time.parse_iso8601(value.to_s) 124 | end 125 | rescue ArgumentError 126 | nil 127 | rescue TypeError 128 | nil 129 | end 130 | 131 | # Creates a DateTime instance from a Hash with keys :year, :month, :day, 132 | # :hour, :min, :sec 133 | def typecast_hash_to_datetime(value) 134 | DateTime.new(*extract_time(value)) 135 | end 136 | 137 | # Creates a Date instance from a Hash with keys :year, :month, :day 138 | def typecast_hash_to_date(value) 139 | Date.new(*extract_time(value)[0, 3].map(&:to_i)) 140 | end 141 | 142 | # Creates a Time instance from a Hash with keys :year, :month, :day, 143 | # :hour, :min, :sec 144 | def typecast_hash_to_time(value) 145 | Time.utc(*extract_time(value)) 146 | end 147 | 148 | # Extracts the given args from the hash. If a value does not exist, it 149 | # uses the value of Time.now. 150 | def extract_time(value) 151 | now = Time.now 152 | [:year, :month, :day, :hour, :min, :sec].map do |segment| 153 | typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i) 154 | end 155 | end 156 | 157 | # Typecast a value to a Class 158 | def typecast_to_class(value) 159 | value.to_s.constantize 160 | rescue NameError 161 | nil 162 | end 163 | 164 | end 165 | end 166 | end 167 | 168 | -------------------------------------------------------------------------------- /lib/couchrest/model/utils/migrate.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module Utils 4 | 5 | # Handle CouchDB Design Document migrations. 6 | # 7 | # Actual migrations are handled by the Design document, this serves as a utility 8 | # to find all the CouchRest Model submodels and perform the migration on them. 9 | # 10 | # Also contains some more advanced support for handling proxied models. 11 | # 12 | # Examples of usage: 13 | # 14 | # # Ensure all models have been loaded (only Rails) 15 | # CouchRest::Model::Utils::Migrate.load_all_models 16 | # 17 | # # Migrate all regular models (not proxied) 18 | # CouchRest::Model::Utils::Migrate.all_models 19 | # 20 | # # Migrate all models and submodels of proxies 21 | # CouchRest::Model::Utils::Migrate.all_models_and_proxies 22 | # 23 | # Typically however you'd want to run these methods from the rake tasks: 24 | # 25 | # $ rake couchrest:designs:migrate_with_proxies 26 | # 27 | # In production environments, you sometimes want to prepare large view 28 | # indexes that take a long term to generate before activating them. To 29 | # support this scenario we provide the `:activate` option: 30 | # 31 | # # Prepare all models, but do not activate 32 | # CouchRest::Model::Utils::Migrate.all_models(activate: false) 33 | # 34 | # Or from Rake: 35 | # 36 | # $ rake couchrest:designs:prepare 37 | # 38 | # Once finished, just before uploading your code you can repeat without 39 | # the `activate` option so that the view indexes are ready for the new 40 | # designs. 41 | # 42 | # NOTE: This is an experimental feature that is not yet properly tested. 43 | # 44 | module Migrate 45 | extend self 46 | 47 | # Make an attempt at loading all the files in this Rails application's 48 | # models directory. 49 | def load_all_models 50 | # Make a reasonable effort to load all models 51 | return unless defined?(Rails) 52 | 53 | Dir[Rails.root + 'app/models/**/*.rb'].each do |path| 54 | # For compatibility issues with Rails version >= 4.1 until 5.2.3 55 | # we have to avoid to load more than once the Rails concerns 56 | # https://github.com/rails/rails/pull/34553 57 | next if path.include?('models/concerns/') 58 | 59 | require path 60 | end 61 | end 62 | 63 | # Go through each class that inherits from CouchRest::Model::Base and 64 | # attempt to migrate the design documents. 65 | def all_models(opts = {}) 66 | opts.reverse_merge!(activate: true, with_proxies: false) 67 | callbacks = migrate_each_model(find_models) 68 | callbacks += migrate_each_proxying_model(find_proxying_base_models) if opts[:with_proxies] 69 | activate_designs(callbacks) if opts[:activate] 70 | end 71 | 72 | def all_models_and_proxies(opts = {}) 73 | opts[:with_proxies] = true 74 | all_models(opts) 75 | end 76 | 77 | protected 78 | 79 | def find_models 80 | CouchRest::Model::Base.subclasses.reject{|m| m.proxy_owner_method.present?} 81 | end 82 | 83 | def find_proxying_base_models 84 | CouchRest::Model::Base.subclasses.reject{|m| m.proxy_method_names.empty? || m.proxy_owner_method.present?} 85 | end 86 | 87 | def migrate_each_model(models, db = nil) 88 | callbacks = [ ] 89 | models.each do |model| 90 | model.design_docs.each do |design| 91 | callbacks << migrate_design(model, design, db) 92 | end 93 | end 94 | callbacks 95 | end 96 | 97 | def migrate_each_proxying_model(models) 98 | callbacks = [ ] 99 | models.each do |model| 100 | model_class = model.is_a?(CouchRest::Model::Proxyable::ModelProxy) ? model.model : model 101 | methods = model_class.proxy_method_names 102 | methods.each do |method| 103 | puts "Finding proxied models for #{model_class}##{method}" 104 | model_class.design_doc.auto_update = false 105 | model.all.each do |obj| 106 | proxy = obj.send(method) 107 | callbacks += migrate_each_model([proxy.model], proxy.database) 108 | callbacks += migrate_each_proxying_model([proxy]) unless model_class.proxy_method_names.empty? 109 | end 110 | end 111 | end 112 | callbacks 113 | end 114 | 115 | def migrate_design(model, design, db = nil) 116 | print "Migrating #{model.to_s}##{design.method_name}" 117 | print " on #{db.name}" if db 118 | print "... " 119 | callback = design.migrate(db) do |result| 120 | puts "#{result.to_s.gsub(/_/, ' ')}" 121 | end 122 | # Return the callback hash if there is one 123 | callback ? {:design => design, :proc => callback, :db => db || model.database} : nil 124 | end 125 | 126 | def activate_designs(methods) 127 | methods.compact.each do |cb| 128 | name = "/#{cb[:db].name}/#{cb[:design]['_id']}" 129 | puts "Activating new design: #{name}" 130 | cb[:proc].call 131 | end 132 | end 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/couchrest/model/validations.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require "couchrest/model/validations/casted_model" 4 | require "couchrest/model/validations/uniqueness" 5 | 6 | I18n.load_path << File.join( 7 | File.dirname(__FILE__), "validations", "locale", "en.yml" 8 | ) 9 | 10 | module CouchRest 11 | module Model 12 | 13 | # Validations may be applied to both Model::Base and Model::CastedModel 14 | module Validations 15 | extend ActiveSupport::Concern 16 | include ActiveModel::Validations 17 | 18 | # Determine if the document is valid. 19 | # 20 | # @example Is the document valid? 21 | # person.valid? 22 | # 23 | # @example Is the document valid in a context? 24 | # person.valid?(:create) 25 | # 26 | # @param [ Symbol ] context The optional validation context. 27 | # 28 | # @return [ true, false ] True if valid, false if not. 29 | # 30 | def valid?(context = nil) 31 | super context ? context : (new? ? :create : :update) 32 | end 33 | 34 | module ClassMethods 35 | 36 | # Validates the associated casted model. This method should not be 37 | # used within your code as it is automatically included when a CastedModel 38 | # is used inside the model. 39 | def validates_casted_model(*args) 40 | validates_with(CastedModelValidator, _merge_attributes(args)) 41 | end 42 | 43 | # Validates if the field is unique for this type of document. Automatically creates 44 | # a view if one does not already exist and performs a search for all matching 45 | # documents. 46 | # 47 | # Example: 48 | # 49 | # class Person < CouchRest::Model::Base 50 | # property :title, String 51 | # 52 | # validates_uniqueness_of :title 53 | # end 54 | # 55 | # Asside from the standard options, you can specify the name of the view you'd like 56 | # to use for the search inside the +:view+ option. The following example would search 57 | # for the code in side the +all+ view, useful for when +unique_id+ is used and you'd 58 | # like to check before receiving a CouchRest::Conflict error: 59 | # 60 | # validates_uniqueness_of :code, :view => 'all' 61 | # 62 | # A +:proxy+ parameter is also accepted if you would 63 | # like to call a method on the document on which the view should be performed. 64 | # 65 | # For Example: 66 | # 67 | # # Same as not including proxy: 68 | # validates_uniqueness_of :title, :proxy => 'class' 69 | # 70 | # # Person#company.people provides a proxy object for people 71 | # validates_uniqueness_of :title, :proxy => 'company.people' 72 | # 73 | def validates_uniqueness_of(*args) 74 | validates_with(UniquenessValidator, _merge_attributes(args)) 75 | end 76 | end 77 | 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/couchrest/model/validations/casted_model.rb: -------------------------------------------------------------------------------- 1 | module CouchRest 2 | module Model 3 | module Validations 4 | class CastedModelValidator < ActiveModel::EachValidator 5 | 6 | def validate_each(document, attribute, value) 7 | values = value.is_a?(Array) ? value : [value] 8 | return if values.collect {|doc| doc.nil? || doc.valid? }.all? 9 | error_options = { :value => value } 10 | error_options[:message] = options[:message] if options[:message] 11 | document.errors.add(attribute, :invalid, error_options) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/couchrest/model/validations/locale/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | taken: "has already been taken" 5 | 6 | -------------------------------------------------------------------------------- /lib/couchrest/model/validations/uniqueness.rb: -------------------------------------------------------------------------------- 1 | 2 | I18n.load_path << File.join( 3 | File.dirname(__FILE__), "locale", "en.yml" 4 | ) 5 | 6 | module CouchRest 7 | module Model 8 | module Validations 9 | 10 | # Validates if a field is unique 11 | class UniquenessValidator < ActiveModel::EachValidator 12 | 13 | SETUP_DEPRECATED = ActiveModel.respond_to?(:version) && ActiveModel.version >= Gem::Version.new('4.1') 14 | 15 | def initialize(options = {}) 16 | super 17 | setup_uniqueness_validation(options[:class]) if options[:class] 18 | end 19 | 20 | # Ensure we have a class available so we can check for a usable view 21 | # or add one if necessary. 22 | def setup_uniqueness_validation(model) 23 | @model = model 24 | if options[:view].blank? 25 | attributes.each do |attribute| 26 | opts = merge_view_options(attribute) 27 | 28 | unless model.respond_to?(opts[:view_name]) 29 | model.design do 30 | view opts[:view_name], :allow_nil => true 31 | end 32 | end 33 | end 34 | end 35 | end 36 | 37 | # Provide backwards compatibility for Rails < 4.1, which expects `#setup` to be defined. 38 | alias_method :setup, :setup_uniqueness_validation unless SETUP_DEPRECATED 39 | 40 | def validate_each(document, attribute, value) 41 | opts = merge_view_options(attribute) 42 | 43 | values = opts[:keys].map{|k| document.send(k)} 44 | values = values.first if values.length == 1 45 | 46 | model = (document.respond_to?(:model_proxy) && document.model_proxy ? document.model_proxy : @model) 47 | # Determine the base of the search 48 | base = opts[:proxy].nil? ? model : document.instance_eval(opts[:proxy]) 49 | 50 | unless base.respond_to?(opts[:view_name]) 51 | raise "View #{document.class.name}.#{opts[:view_name]} does not exist for validation!" 52 | end 53 | 54 | rows = base.send(opts[:view_name], :key => values, :limit => 2, :include_docs => false).rows 55 | return if rows.empty? 56 | 57 | unless document.new? 58 | return if rows.find{|row| row.id == document.id} 59 | end 60 | 61 | if rows.length > 0 62 | opts = options.merge(:value => value) 63 | opts.delete(:scope) # Has meaning with I18n! 64 | document.errors.add(attribute, :taken, opts) 65 | end 66 | end 67 | 68 | private 69 | 70 | def merge_view_options(attr) 71 | keys = [attr] 72 | keys.unshift(*options[:scope]) unless options[:scope].nil? 73 | 74 | view_name = options[:view].nil? ? "by_#{keys.join('_and_')}" : options[:view] 75 | 76 | options.merge({:keys => keys, :view_name => view_name}) 77 | end 78 | 79 | end 80 | 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/couchrest/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rails" 2 | require "active_model/railtie" 3 | 4 | module CouchRest 5 | class ModelRailtie < Rails::Railtie 6 | def self.generator 7 | config.respond_to?(:app_generators) ? :app_generators : :generators 8 | end 9 | 10 | config.send(generator).orm :couchrest_model 11 | config.send(generator).test_framework :test_unit, :fixture => false 12 | 13 | initializer "couchrest_model.configure_default_connection" do 14 | CouchRest::Model::Base.configure do |conf| 15 | conf.environment = Rails.env 16 | conf.connection_config_file = File.join(Rails.root, 'config', 'couchdb.yml') 17 | conf.connection[:prefix] = 18 | Rails.application.class.to_s.underscore.gsub(/\/.*/, '') 19 | end 20 | end 21 | 22 | config.before_configuration do 23 | config.couchrest_model = CouchRest::Model::Base 24 | end 25 | 26 | rake_tasks do 27 | Dir[File.join(File.dirname(__FILE__),'../tasks/*.rake')].each { |f| load f } 28 | end 29 | end 30 | 31 | end 32 | 33 | -------------------------------------------------------------------------------- /lib/couchrest_model.rb: -------------------------------------------------------------------------------- 1 | require "active_model" 2 | require "active_model/callbacks" 3 | require "active_model/conversion" 4 | require "active_model/errors" 5 | require "active_model/naming" 6 | require "active_model/serialization" 7 | require "active_model/translation" 8 | require "active_model/validator" 9 | require "active_model/validations" 10 | require "active_model/dirty" 11 | 12 | require "active_support/core_ext" 13 | require "active_support/json" 14 | 15 | require "mime/types" 16 | require "enumerator" 17 | require "time" 18 | require "digest/md5" 19 | require "yaml" 20 | require "hashdiff" 21 | 22 | require "bigdecimal" # used in typecast 23 | require "bigdecimal/util" # used in typecast 24 | 25 | require "couchrest" 26 | 27 | require "couchrest/model" 28 | require "couchrest/model/errors" 29 | require "couchrest/model/server_pool" 30 | require "couchrest/model/connection_config" 31 | require "couchrest/model/configuration" 32 | require "couchrest/model/translation" 33 | require "couchrest/model/persistence" 34 | require "couchrest/model/typecast" 35 | require "couchrest/model/casted_by" 36 | require "couchrest/model/dirty" 37 | require "couchrest/model/property" 38 | require "couchrest/model/property_protection" 39 | require "couchrest/model/properties" 40 | require "couchrest/model/casted_array" 41 | require "couchrest/model/validations" 42 | require "couchrest/model/callbacks" 43 | require "couchrest/model/document_queries" 44 | require "couchrest/model/extended_attachments" 45 | require "couchrest/model/proxyable" 46 | require "couchrest/model/associations" 47 | require "couchrest/model/connection" 48 | require "couchrest/model/designs/migrations" 49 | require "couchrest/model/designs/design_mapper" 50 | require "couchrest/model/designs/view" 51 | require "couchrest/model/design" 52 | require "couchrest/model/designs" 53 | 54 | # Monkey patches applied to couchrest 55 | require "couchrest/model/support/couchrest_database" 56 | 57 | # Core Extensions 58 | require "couchrest/model/core_extensions/hash" 59 | require "couchrest/model/core_extensions/time_parsing" 60 | 61 | # Base libraries 62 | require "couchrest/model/embeddable" 63 | require "couchrest/model/base" 64 | 65 | # Design Migration support 66 | require "couchrest/model/utils/migrate.rb" 67 | 68 | # Add rails support *after* everything has loaded 69 | require "couchrest/railtie" if defined?(Rails) 70 | -------------------------------------------------------------------------------- /lib/rails/generators/couchrest_model.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/named_base' 2 | require 'rails/generators/active_model' 3 | require 'couchrest_model' 4 | 5 | module CouchrestModel 6 | module Generators 7 | class Base < Rails::Generators::NamedBase #:nodoc: 8 | 9 | # Set the current directory as base for the inherited generators. 10 | def self.base_root 11 | File.dirname(__FILE__) 12 | end 13 | 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/rails/generators/couchrest_model/config/config_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/couchrest_model' 2 | 3 | module CouchrestModel 4 | module Generators 5 | class ConfigGenerator < Rails::Generators::Base 6 | source_root File.expand_path('../templates', __FILE__) 7 | 8 | def app_name 9 | Rails::Application.subclasses.first.parent.to_s.underscore 10 | end 11 | 12 | def copy_configuration_file 13 | template 'couchdb.yml', File.join('config', "couchdb.yml") 14 | end 15 | 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/rails/generators/couchrest_model/config/templates/couchdb.yml: -------------------------------------------------------------------------------- 1 | development: &development 2 | protocol: 'http' 3 | host: localhost 4 | port: 5984 5 | prefix: <%= app_name %> 6 | suffix: development 7 | username: 8 | password: 9 | 10 | test: 11 | <<: *development 12 | suffix: test 13 | 14 | production: 15 | protocol: 'https' 16 | host: localhost 17 | port: 5984 18 | prefix: <%= app_name %> 19 | suffix: production 20 | username: root 21 | password: 123 -------------------------------------------------------------------------------- /lib/rails/generators/couchrest_model/model/model_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/couchrest_model' 2 | 3 | module CouchrestModel 4 | module Generators 5 | class ModelGenerator < Base 6 | check_class_collision 7 | 8 | def create_model_file 9 | template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") 10 | end 11 | 12 | def create_module_file 13 | return if class_path.empty? 14 | template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke 15 | end 16 | 17 | hook_for :test_framework 18 | 19 | protected 20 | 21 | def parent_class_name 22 | "CouchRest::Model::Base" 23 | end 24 | 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/rails/generators/couchrest_model/model/templates/model.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %> < <%= parent_class_name.classify %> 2 | end 3 | -------------------------------------------------------------------------------- /lib/tasks/migrations.rake: -------------------------------------------------------------------------------- 1 | # 2 | # CouchRest Migration Rake Tasks 3 | # 4 | # See the CouchRest::Model::Utils::Migrate class for more details. 5 | # 6 | namespace :couchrest do 7 | 8 | namespace :designs do 9 | 10 | desc "Migrate all the design docs found in each model" 11 | task :migrate => :environment do 12 | CouchRest::Model::Utils::Migrate.load_all_models 13 | CouchRest::Model::Utils::Migrate.all_models 14 | end 15 | 16 | desc "Migrate all the design docs found in each model, but do not active the designs" 17 | task :prepare => :environment do 18 | CouchRest::Model::Utils::Migrate.load_all_models 19 | CouchRest::Model::Utils::Migrate.all_models(activate: false) 20 | end 21 | 22 | desc "Migrate all the design docs " 23 | task :migrate_with_proxies => :environment do 24 | CouchRest::Model::Utils::Migrate.load_all_models 25 | CouchRest::Model::Utils::Migrate.all_models_and_proxies 26 | end 27 | 28 | desc "Migrate all the design docs " 29 | task :prepare_with_proxies => :environment do 30 | CouchRest::Model::Utils::Migrate.load_all_models 31 | CouchRest::Model::Utils::Migrate.all_models_and_proxies(activate: false) 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/.gitignore: -------------------------------------------------------------------------------- 1 | tmp -------------------------------------------------------------------------------- /spec/fixtures/attachments/README: -------------------------------------------------------------------------------- 1 | This is an example README file. 2 | 3 | More of the README, whee. -------------------------------------------------------------------------------- /spec/fixtures/attachments/couchdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchrest/couchrest_model/6c8467e0dd4bfdbd2405c73c2609d81b00651400/spec/fixtures/attachments/couchdb.png -------------------------------------------------------------------------------- /spec/fixtures/attachments/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 |

8 | Test 9 |

10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/fixtures/config/couchdb.yml: -------------------------------------------------------------------------------- 1 | 2 | development: 3 | protocol: 'https' 4 | host: sample.cloudant.com 5 | port: 443 6 | prefix: project 7 | suffix: text 8 | username: test 9 | password: user 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/models/article.rb: -------------------------------------------------------------------------------- 1 | class Article < CouchRest::Model::Base 2 | use_database DB 3 | unique_id :slug 4 | 5 | design do 6 | view :by_date # Default options not supported: :descending => true 7 | view :by_user_id_and_date 8 | 9 | view :by_tags, 10 | :map => 11 | "function(doc) { 12 | if (doc['#{model.model_type_key}'] == 'Article' && doc.tags) { 13 | doc.tags.forEach(function(tag){ 14 | emit(tag, 1); 15 | }); 16 | } 17 | }", 18 | :reduce => 19 | "function(keys, values, rereduce) { 20 | return sum(values); 21 | }" 22 | 23 | end 24 | 25 | property :date, Date 26 | property :slug, :read_only => true 27 | property :user_id 28 | property :title 29 | property :tags, [String] 30 | 31 | timestamps! 32 | 33 | before_save :generate_slug_from_title 34 | 35 | def generate_slug_from_title 36 | self['slug'] = title.to_s.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new? 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/fixtures/models/base.rb: -------------------------------------------------------------------------------- 1 | class WithDefaultValues < CouchRest::Model::Base 2 | use_database DB 3 | property :preset, Object, :default => {:right => 10, :top_align => false} 4 | property :set_by_proc, Time, :default => Proc.new{Time.now} 5 | property :tags, [String], :default => [] 6 | property :read_only_with_default, :default => 'generic', :read_only => true 7 | property :default_false, TrueClass, :default => false 8 | property :name 9 | timestamps! 10 | end 11 | 12 | class WithSimplePropertyType < CouchRest::Model::Base 13 | use_database DB 14 | property :name, String 15 | property :preset, String, :default => 'none' 16 | property :tags, [String] 17 | timestamps! 18 | end 19 | 20 | class WithCallBacks < CouchRest::Model::Base 21 | use_database DB 22 | property :name 23 | property :run_before_validation 24 | property :run_after_validation 25 | property :run_before_save 26 | property :run_after_save 27 | property :run_before_create 28 | property :run_after_create 29 | property :run_before_update 30 | property :run_after_update 31 | 32 | validates_presence_of :run_before_validation 33 | 34 | before_validation do |object| 35 | object.run_before_validation = true 36 | end 37 | after_validation do |object| 38 | object.run_after_validation = true 39 | end 40 | before_save do |object| 41 | object.run_before_save = true 42 | end 43 | after_save do |object| 44 | object.run_after_save = true 45 | end 46 | before_create do |object| 47 | object.run_before_create = true 48 | end 49 | after_create do |object| 50 | object.run_after_create = true 51 | end 52 | before_update do |object| 53 | object.run_before_update = true 54 | end 55 | after_update do |object| 56 | object.run_after_update = true 57 | end 58 | 59 | property :run_one 60 | property :run_two 61 | property :run_three 62 | 63 | before_save :run_one_method, :run_two_method do |object| 64 | object.run_three = true 65 | end 66 | def run_one_method 67 | self.run_one = true 68 | end 69 | def run_two_method 70 | self.run_two = true 71 | end 72 | 73 | attr_accessor :run_it 74 | property :conditional_one 75 | property :conditional_two 76 | 77 | before_save :conditional_one_method, :conditional_two_method, :if => proc { self.run_it } 78 | def conditional_one_method 79 | self.conditional_one = true 80 | end 81 | def conditional_two_method 82 | self.conditional_two = true 83 | end 84 | end 85 | 86 | # Following two fixture classes have __intentionally__ diffent syntax for setting the validation context 87 | class WithContextualValidationOnCreate < CouchRest::Model::Base 88 | use_database DB 89 | property(:name, String) 90 | validates(:name, :presence => {:on => :create}) 91 | end 92 | 93 | class WithContextualValidationOnUpdate < CouchRest::Model::Base 94 | use_database DB 95 | property(:name, String) 96 | validates(:name, :presence => true, :on => :update) 97 | end 98 | 99 | class WithTemplateAndUniqueID < CouchRest::Model::Base 100 | use_database DB 101 | unique_id do |model| 102 | model.slug 103 | end 104 | property :slug 105 | property :preset, :default => 'value' 106 | property :has_no_default 107 | design 108 | end 109 | 110 | class WithGetterAndSetterMethods < CouchRest::Model::Base 111 | use_database DB 112 | 113 | property :other_arg 114 | def arg 115 | other_arg 116 | end 117 | 118 | def arg=(value) 119 | self.other_arg = "foo-#{value}" 120 | end 121 | end 122 | 123 | class WithAfterInitializeMethod < CouchRest::Model::Base 124 | use_database DB 125 | 126 | property :some_value 127 | 128 | def after_initialize 129 | self.some_value ||= "value" 130 | end 131 | 132 | end 133 | 134 | class WithUniqueValidation < CouchRest::Model::Base 135 | use_database DB 136 | property :title 137 | validates_uniqueness_of :title 138 | end 139 | class WithUniqueValidationProxy < CouchRest::Model::Base 140 | use_database DB 141 | property :title 142 | validates_uniqueness_of :title, :proxy => 'proxy' 143 | end 144 | class WithUniqueValidationView < CouchRest::Model::Base 145 | use_database DB 146 | attr_accessor :code 147 | unique_id :code 148 | def code 149 | @code 150 | end 151 | property :title 152 | 153 | design 154 | validates_uniqueness_of :code, :view => 'all' 155 | end 156 | 157 | class WithScopedUniqueValidation < CouchRest::Model::Base 158 | use_database DB 159 | 160 | property :parent_id 161 | property :title 162 | 163 | validates_uniqueness_of :title, :scope => :parent_id 164 | end 165 | 166 | class WithDateAndTime < CouchRest::Model::Base 167 | use_database DB 168 | property :exec_date, Date 169 | property :exec_time, Time 170 | end 171 | -------------------------------------------------------------------------------- /spec/fixtures/models/card.rb: -------------------------------------------------------------------------------- 1 | require 'person' 2 | 3 | class Card < CouchRest::Model::Base 4 | # Set the default database to use 5 | use_database DB 6 | 7 | # Official Schema 8 | property :first_name 9 | property :last_name, :alias => :family_name 10 | property :read_only_value, :read_only => true 11 | property :cast_alias, Person, :alias => :calias 12 | property :fg_color, String, :default => '#000' 13 | property :bg_color, String, :protected => true 14 | 15 | timestamps! 16 | 17 | # Validation 18 | validates_presence_of :first_name 19 | 20 | end 21 | -------------------------------------------------------------------------------- /spec/fixtures/models/cat.rb: -------------------------------------------------------------------------------- 1 | 2 | class CatToy 3 | include CouchRest::Model::Embeddable 4 | 5 | property :name 6 | 7 | validates_presence_of :name 8 | end 9 | 10 | class Cat < CouchRest::Model::Base 11 | # Set the default database to use 12 | use_database DB 13 | 14 | property :name, :accessible => true 15 | property :toys, [CatToy], :default => [], :accessible => true 16 | property :favorite_toy, CatToy, :accessible => true 17 | property :number 18 | end 19 | 20 | class ChildCat < Cat 21 | property :mother, Cat 22 | property :siblings, [Cat] 23 | end 24 | -------------------------------------------------------------------------------- /spec/fixtures/models/client.rb: -------------------------------------------------------------------------------- 1 | class Client < CouchRest::Model::Base 2 | use_database DB 3 | 4 | property :name 5 | property :tax_code 6 | end -------------------------------------------------------------------------------- /spec/fixtures/models/concerns/attachable.rb: -------------------------------------------------------------------------------- 1 | module Attachable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/models/course.rb: -------------------------------------------------------------------------------- 1 | require 'question' 2 | require 'person' 3 | require 'money' 4 | 5 | class Course < CouchRest::Model::Base 6 | use_database DB 7 | 8 | property :title, String 9 | property :subtitle, String, :allow_blank => false 10 | property :questions, [Question] 11 | property :professor, Person 12 | property :participants, [Object] 13 | property :ends_at, Time 14 | property :estimate, Float 15 | property :hours, Integer 16 | property :profit, BigDecimal 17 | property :started_on, :type => Date 18 | property :updated_at, DateTime 19 | property :active, :type => TrueClass 20 | property :very_active, :type => TrueClass 21 | property :klass, :type => Class 22 | property :currency, String, :default => 'EUR' 23 | property :price, Money 24 | property :symbol, Symbol 25 | 26 | design do 27 | view :by_title 28 | view :by_title_and_active 29 | 30 | view :by_dept, :ducktype => true 31 | 32 | view :by_active, :map => "function(d) { if (d['#{model_type_key}'] == 'Course' && d['active']) { emit(d['updated_at'], 1); }}", :reduce => "function(k,v,r) { return sum(v); }" 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/fixtures/models/designs.rb: -------------------------------------------------------------------------------- 1 | 2 | class DesignModel < CouchRest::Model::Base 3 | use_database DB 4 | property :name 5 | end 6 | 7 | class DesignsModel < CouchRest::Model::Base 8 | use_database DB 9 | property :name 10 | end 11 | 12 | 13 | class DesignsNoAutoUpdate < CouchRest::Model::Base 14 | use_database DB 15 | property :title, String 16 | design do 17 | disable_auto_update 18 | view :by_title_fail, :by => ['title'] 19 | view :by_title, :reduce => true 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /spec/fixtures/models/event.rb: -------------------------------------------------------------------------------- 1 | class Event < CouchRest::Model::Base 2 | use_database DB 3 | 4 | property :subject 5 | property :occurs_at, Time, :init_method => 'parse' 6 | property :end_date, Date, :init_method => 'parse' 7 | 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/models/invoice.rb: -------------------------------------------------------------------------------- 1 | class Invoice < CouchRest::Model::Base 2 | # Set the default database to use 3 | use_database DB 4 | 5 | # Official Schema 6 | property :client_name 7 | property :employee_name 8 | property :location 9 | 10 | # Validation 11 | validates_presence_of :client_name, :employee_name 12 | validates_presence_of :location, :message => "Hey stupid!, you forgot the location" 13 | 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/models/key_chain.rb: -------------------------------------------------------------------------------- 1 | class KeyChain < CouchRest::Model::Base 2 | use_database(DB) 3 | 4 | property(:keys, Hash) 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/models/membership.rb: -------------------------------------------------------------------------------- 1 | class Membership 2 | include CouchRest::Model::Embeddable 3 | 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/models/money.rb: -------------------------------------------------------------------------------- 1 | 2 | # Really simple money class for testing 3 | class Money 4 | 5 | attr_accessor :cents, :currency 6 | 7 | def initialize(cents, currency = nil) 8 | self.cents = cents.to_i 9 | self.currency = currency 10 | end 11 | 12 | def to_s 13 | (self.cents.to_f / 100).to_s 14 | end 15 | 16 | def self.couchrest_typecast(parent, property, value) 17 | if parent.respond_to?(:currency) 18 | new(value, parent.currency) 19 | else 20 | new(value) 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /spec/fixtures/models/person.rb: -------------------------------------------------------------------------------- 1 | require 'cat' 2 | 3 | class Person 4 | include ::CouchRest::Model::Embeddable 5 | 6 | property :pet, Cat 7 | property :name, [String] 8 | 9 | def last_name 10 | name.last 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/models/project.rb: -------------------------------------------------------------------------------- 1 | class Project < CouchRest::Model::Base 2 | use_database DB 3 | 4 | disable_dirty_tracking true 5 | 6 | property :name, String 7 | timestamps! 8 | 9 | design do 10 | view :by_name 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/models/question.rb: -------------------------------------------------------------------------------- 1 | class Question 2 | include ::CouchRest::Model::Embeddable 3 | 4 | property :q 5 | property :a 6 | 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/models/sale_entry.rb: -------------------------------------------------------------------------------- 1 | class SaleEntry < CouchRest::Model::Base 2 | use_database DB 3 | 4 | property :description 5 | property :price 6 | 7 | design do 8 | view :by_description 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/models/sale_invoice.rb: -------------------------------------------------------------------------------- 1 | require 'client' 2 | require 'sale_entry' 3 | 4 | class SaleInvoice < CouchRest::Model::Base 5 | use_database DB 6 | 7 | belongs_to :client 8 | belongs_to :alternate_client, :class_name => 'Client', :foreign_key => 'alt_client_id' 9 | 10 | collection_of :entries, :class_name => 'SaleEntry' 11 | 12 | property :date, Date 13 | property :price, Integer 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/models/service.rb: -------------------------------------------------------------------------------- 1 | class Service < CouchRest::Model::Base 2 | # Set the default database to use 3 | use_database DB 4 | 5 | # Official Schema 6 | property :name 7 | property :price, Integer 8 | 9 | validates_length_of :name, :minimum => 4, :maximum => 20 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < CouchRest::Model::Base 2 | # Set the default database to use 3 | use_database DB 4 | property :name, :accessible => true 5 | property :admin # this will be automatically protected 6 | end 7 | 8 | class SpecialUser < CouchRest::Model::Base 9 | # Set the default database to use 10 | use_database DB 11 | property :name # this will not be protected 12 | property :admin, :protected => true 13 | end 14 | 15 | # There are two modes of protection 16 | # 1) Declare accessible poperties, assume all the rest are protected 17 | # property :name, :accessible => true 18 | # property :admin # this will be automatically protected 19 | # 20 | # 2) Declare protected properties, assume all the rest are accessible 21 | # property :name # this will not be protected 22 | # property :admin, :protected => true 23 | -------------------------------------------------------------------------------- /spec/fixtures/views/lib.js: -------------------------------------------------------------------------------- 1 | function globalLib() { 2 | return "fixture"; 3 | }; -------------------------------------------------------------------------------- /spec/fixtures/views/test_view/lib.js: -------------------------------------------------------------------------------- 1 | function justThisView() { 2 | return "fixture"; 3 | }; -------------------------------------------------------------------------------- /spec/fixtures/views/test_view/only-map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | //include-lib 3 | emit(null, null); 4 | }; -------------------------------------------------------------------------------- /spec/fixtures/views/test_view/test-map.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | emit(null, null); 3 | }; -------------------------------------------------------------------------------- /spec/fixtures/views/test_view/test-reduce.js: -------------------------------------------------------------------------------- 1 | function(ks,vs,co) { 2 | return vs.length; 3 | }; -------------------------------------------------------------------------------- /spec/functional/validations_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper', __FILE__) 2 | 3 | describe CouchRest::Model::Validations do 4 | 5 | let(:invoice) do 6 | Invoice.new() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $VERBOSE=true 2 | 3 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 4 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) 5 | 6 | require "bundler/setup" 7 | require "rubygems" 8 | require "rspec" 9 | 10 | #require 'oj' 11 | require 'couchrest_model' 12 | 13 | unless defined?(FIXTURE_PATH) 14 | MODEL_PATH = File.join(File.dirname(__FILE__), "fixtures", "models") 15 | $LOAD_PATH.unshift(MODEL_PATH) 16 | 17 | FIXTURE_PATH = File.join(File.dirname(__FILE__), '/fixtures') 18 | SCRATCH_PATH = File.join(File.dirname(__FILE__), '/tmp') 19 | 20 | COUCHHOST = ENV["COUCH_HOST"] || "http://127.0.0.1:5984" 21 | TESTDB = 'couchrest-model-test' 22 | TEST_SERVER = CouchRest.new COUCHHOST 23 | # TEST_SERVER.default_database = TESTDB 24 | DB = TEST_SERVER.database(TESTDB) 25 | end 26 | 27 | RSpec.configure do |config| 28 | config.before(:suite) do 29 | couch_uri = URI.parse(ENV['COUCH_HOST'] || "http://127.0.0.1:5984") 30 | CouchRest::Model::Base.configure do |config| 31 | config.connection = { 32 | :protocol => couch_uri.scheme, 33 | :host => couch_uri.host, 34 | :port => couch_uri.port, 35 | :username => couch_uri.user, 36 | :password => couch_uri.password, 37 | :prefix => "couchrest", 38 | :join => "_" 39 | } 40 | end 41 | end 42 | 43 | config.before(:all) { reset_test_db! } 44 | 45 | config.after(:all) do 46 | cr = TEST_SERVER 47 | test_dbs = cr.databases.select { |db| db =~ /^#{TESTDB}/ } 48 | test_dbs.each do |db| 49 | cr.database(db).delete! rescue nil 50 | end 51 | end 52 | end 53 | 54 | # Require each of the fixture models 55 | Dir[ File.join(MODEL_PATH, "*.rb") ].sort.each { |file| require File.basename(file) } 56 | 57 | class Basic < CouchRest::Model::Base 58 | use_database DB 59 | end 60 | 61 | def reset_test_db! 62 | DB.recreate! rescue nil 63 | # Reset the Design Cache 64 | Thread.current[:couchrest_design_cache] = {} 65 | DB 66 | end 67 | 68 | 69 | def couchdb_lucene_available? 70 | lucene_path = "http://localhost:5985/" 71 | url = URI.parse(lucene_path) 72 | req = Net::HTTP::Get.new(url.path) 73 | res = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) } 74 | true 75 | rescue Exception => e 76 | false 77 | end 78 | 79 | -------------------------------------------------------------------------------- /spec/unit/active_model_lint_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | require 'test/unit/assertions' 4 | require 'active_model/lint' 5 | 6 | class CompliantModel < CouchRest::Model::Base 7 | end 8 | 9 | 10 | describe CouchRest::Model::Base do 11 | include Test::Unit::Assertions 12 | include ActiveModel::Lint::Tests 13 | 14 | before :each do 15 | @model = CompliantModel.new 16 | end 17 | 18 | describe "active model lint tests" do 19 | ActiveModel::Lint::Tests.public_instance_methods.map{|m| m.to_s}.grep(/^test/).each do |m| 20 | example m.gsub('_',' ') do 21 | send m 22 | end 23 | end 24 | end 25 | 26 | def model 27 | @model 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/attachment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Model attachments" do 4 | 5 | describe "#has_attachment?" do 6 | before(:each) do 7 | reset_test_db! 8 | @obj = Basic.new 9 | expect(@obj.save).to be_truthy 10 | @file = File.open(FIXTURE_PATH + '/attachments/test.html') 11 | @attachment_name = 'my_attachment' 12 | @obj.create_attachment(:file => @file, :name => @attachment_name) 13 | end 14 | 15 | it 'should return false if there is no attachment' do 16 | expect(@obj.has_attachment?('bogus')).to be_falsey 17 | end 18 | 19 | it 'should return true if there is an attachment' do 20 | expect(@obj.has_attachment?(@attachment_name)).to be_truthy 21 | end 22 | 23 | it 'should return true if an object with an attachment is reloaded' do 24 | expect(@obj.save).to be_truthy 25 | reloaded_obj = Basic.get(@obj.id) 26 | expect(reloaded_obj.has_attachment?(@attachment_name)).to be_truthy 27 | end 28 | 29 | it 'should return false if an attachment has been removed' do 30 | @obj.delete_attachment(@attachment_name) 31 | expect(@obj.has_attachment?(@attachment_name)).to be_falsey 32 | end 33 | 34 | it 'should return false if an attachment has been removed and reloaded' do 35 | @obj.delete_attachment(@attachment_name) 36 | reloaded_obj = Basic.get(@obj.id) 37 | expect(reloaded_obj.has_attachment?(@attachment_name)).to be_falsey 38 | end 39 | 40 | end 41 | 42 | describe "creating an attachment" do 43 | before(:each) do 44 | @obj = Basic.new 45 | expect(@obj.save).to be_truthy 46 | @file_ext = File.open(FIXTURE_PATH + '/attachments/test.html') 47 | @file_no_ext = File.open(FIXTURE_PATH + '/attachments/README') 48 | @attachment_name = 'my_attachment' 49 | @content_type = 'media/mp3' 50 | end 51 | 52 | it "should create an attachment from file with an extension" do 53 | @obj.create_attachment(:file => @file_ext, :name => @attachment_name) 54 | expect(@obj.save).to be_truthy 55 | reloaded_obj = Basic.get(@obj.id) 56 | expect(reloaded_obj.attachments[@attachment_name]).not_to be_nil 57 | end 58 | 59 | it "should create an attachment from file without an extension" do 60 | @obj.create_attachment(:file => @file_no_ext, :name => @attachment_name) 61 | expect(@obj.save).to be_truthy 62 | reloaded_obj = Basic.get(@obj.id) 63 | expect(reloaded_obj.attachments[@attachment_name]).not_to be_nil 64 | end 65 | 66 | it 'should raise ArgumentError if :file is missing' do 67 | expect{ @obj.create_attachment(:name => @attachment_name) }.to raise_error(ArgumentError, /:file/) 68 | end 69 | 70 | it 'should raise ArgumentError if :name is missing' do 71 | expect{ @obj.create_attachment(:file => @file_ext) }.to raise_error(ArgumentError, /:name/) 72 | end 73 | 74 | it 'should set the content-type if passed' do 75 | @obj.create_attachment(:file => @file_ext, :name => @attachment_name, :content_type => @content_type) 76 | expect(@obj.attachments[@attachment_name]['content_type']).to eq(@content_type) 77 | end 78 | 79 | it "should detect the content-type automatically" do 80 | @obj.create_attachment(:file => File.open(FIXTURE_PATH + '/attachments/couchdb.png'), :name => "couchdb.png") 81 | expect(@obj.attachments['couchdb.png']['content_type']).to eq("image/png") 82 | end 83 | 84 | it "should use name to detect the content-type automatically if no file" do 85 | file = File.open(FIXTURE_PATH + '/attachments/couchdb.png') 86 | allow(file).to receive(:path).and_return("badfilname") 87 | @obj.create_attachment(:file => File.open(FIXTURE_PATH + '/attachments/couchdb.png'), :name => "couchdb.png") 88 | expect(@obj.attachments['couchdb.png']['content_type']).to eq("image/png") 89 | end 90 | 91 | end 92 | 93 | describe 'reading, updating, and deleting an attachment' do 94 | before(:each) do 95 | @obj = Basic.new 96 | @file = File.open(FIXTURE_PATH + '/attachments/test.html') 97 | @attachment_name = 'my_attachment' 98 | @obj.create_attachment(:file => @file, :name => @attachment_name) 99 | expect(@obj.save).to be_truthy 100 | @file.rewind 101 | @content_type = 'media/mp3' 102 | end 103 | 104 | it 'should read an attachment that exists' do 105 | expect(@obj.read_attachment(@attachment_name)).to eq(@file.read) 106 | end 107 | 108 | it 'should update an attachment that exists' do 109 | file = File.open(FIXTURE_PATH + '/attachments/README') 110 | expect(@file).not_to eq(file) 111 | @obj.update_attachment(:file => file, :name => @attachment_name) 112 | @obj.save 113 | reloaded_obj = Basic.get(@obj.id) 114 | file.rewind 115 | expect(reloaded_obj.read_attachment(@attachment_name)).not_to eq(@file.read) 116 | expect(reloaded_obj.read_attachment(@attachment_name)).to eq(file.read) 117 | end 118 | 119 | it 'should set the content-type if passed' do 120 | file = File.open(FIXTURE_PATH + '/attachments/README') 121 | expect(@file).not_to eq(file) 122 | @obj.update_attachment(:file => file, :name => @attachment_name, :content_type => @content_type) 123 | expect(@obj.attachments[@attachment_name]['content_type']).to eq(@content_type) 124 | end 125 | 126 | it 'should delete an attachment that exists' do 127 | @obj.delete_attachment(@attachment_name) 128 | @obj.save 129 | expect{Basic.get(@obj.id).read_attachment(@attachment_name)}.to raise_error(/404 Not Found/) 130 | end 131 | end 132 | 133 | describe "#attachment_url" do 134 | before(:each) do 135 | @obj = Basic.new 136 | @file = File.open(FIXTURE_PATH + '/attachments/test.html') 137 | @attachment_name = 'my_attachment' 138 | @obj.create_attachment(:file => @file, :name => @attachment_name) 139 | expect(@obj.save).to be_truthy 140 | end 141 | 142 | it 'should return nil if attachment does not exist' do 143 | expect(@obj.attachment_url('bogus')).to be_nil 144 | end 145 | 146 | it 'should return the attachment URL as specified by CouchDB HttpDocumentApi' do 147 | expect(@obj.attachment_url(@attachment_name)).to eq("#{Basic.database}/#{@obj.id}/#{@attachment_name}") 148 | end 149 | 150 | it 'should return the attachment URI' do 151 | expect(@obj.attachment_uri(@attachment_name)).to eq("#{Basic.database.uri}/#{@obj.id}/#{@attachment_name}") 152 | end 153 | end 154 | 155 | describe "#attachments" do 156 | before(:each) do 157 | @obj = Basic.new 158 | @file = File.open(FIXTURE_PATH + '/attachments/test.html') 159 | @attachment_name = 'my_attachment' 160 | @obj.create_attachment(:file => @file, :name => @attachment_name) 161 | expect(@obj.save).to be_truthy 162 | end 163 | 164 | it 'should return an empty Hash when document does not have any attachment' do 165 | new_obj = Basic.new 166 | expect(new_obj.save).to be_truthy 167 | expect(new_obj.attachments).to eq({}) 168 | end 169 | 170 | it 'should return a Hash with all attachments' do 171 | @file.rewind 172 | expect(@obj.attachments).to eq({ @attachment_name =>{ "data" => "PCFET0NUWVBFIGh0bWw+CjxodG1sPgogIDxoZWFkPgogICAgPHRpdGxlPlRlc3Q8L3RpdGxlPgogIDwvaGVhZD4KICA8Ym9keT4KICAgIDxwPgogICAgICBUZXN0CiAgICA8L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==", "content_type" => "text/html"}}) 173 | end 174 | 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /spec/unit/casted_array_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "spec_helper" 3 | 4 | # 5 | # TODO This requires much more testing, although most of the basics 6 | # are checked by other parts of the code. 7 | # 8 | 9 | describe CouchRest::Model::CastedArray do 10 | 11 | let :klass do 12 | CouchRest::Model::CastedArray 13 | end 14 | 15 | describe "#initialize" do 16 | it "should set the casted properties" do 17 | prop = double('Property') 18 | parent = double('Parent') 19 | obj = klass.new([], prop, parent) 20 | expect(obj.casted_by_property).to eql(prop) 21 | expect(obj.casted_by).to eql(parent) 22 | expect(obj).to be_empty 23 | end 24 | end 25 | 26 | describe "#as_couch_json" do 27 | let :property do 28 | CouchRest::Model::Property.new(:cat, :type => Cat) 29 | end 30 | let :obj do 31 | klass.new([ 32 | { :name => 'Felix' }, 33 | { :name => 'Garfield' } 34 | ], property) 35 | end 36 | it "should return an array" do 37 | expect(obj.as_couch_json).to be_a(Array) 38 | end 39 | it "should call as_couch_json on each value" do 40 | expect(obj.first).to receive(:as_couch_json) 41 | obj.as_couch_json 42 | end 43 | it "should return value if no as_couch_json method" do 44 | obj = klass.new(['Felix', 'Garfield'], CouchRest::Model::Property.new(:title, :type => String)) 45 | expect(obj.first).not_to respond_to(:as_couch_json) 46 | expect(obj.as_couch_json.first).to eql('Felix') 47 | end 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/unit/casted_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | class Driver < CouchRest::Model::Base 4 | use_database DB 5 | # You have to add a casted_by accessor if you want to reach a casted extended doc parent 6 | attr_accessor :casted_by 7 | 8 | property :name 9 | end 10 | 11 | class Car < CouchRest::Model::Base 12 | use_database DB 13 | 14 | property :name 15 | property :driver, Driver 16 | end 17 | 18 | describe "casting an extended document" do 19 | 20 | before(:each) do 21 | @driver = Driver.new(:name => 'Matt') 22 | @car = Car.new(:name => 'Renault 306', :driver => @driver) 23 | end 24 | 25 | it "should retain all properties of the casted attribute" do 26 | expect(@car.driver).to eq(@driver) 27 | end 28 | 29 | it "should let the casted document know who casted it" do 30 | expect(@car.driver.casted_by).to eq(@car) 31 | end 32 | end 33 | 34 | describe "assigning a value to casted attribute after initializing an object" do 35 | 36 | before(:each) do 37 | @car = Car.new(:name => 'Renault 306') 38 | @driver = Driver.new(:name => 'Matt') 39 | end 40 | 41 | it "should not create an empty casted object" do 42 | expect(@car.driver).to be_nil 43 | end 44 | 45 | it "should let you assign the value" do 46 | @car.driver = @driver 47 | expect(@car.driver.name).to eq('Matt') 48 | end 49 | 50 | it "should cast attribute" do 51 | @car.driver = JSON.parse(@driver.to_json) 52 | expect(@car.driver).to be_instance_of(Driver) 53 | end 54 | 55 | end 56 | 57 | describe "casting a model from parsed JSON" do 58 | 59 | before(:each) do 60 | @driver = Driver.new(:name => 'Matt') 61 | @car = Car.new(:name => 'Renault 306', :driver => @driver) 62 | @new_car = Car.new(JSON.parse(@car.to_json)) 63 | end 64 | 65 | it "should cast casted attribute" do 66 | expect(@new_car.driver).to be_instance_of(Driver) 67 | end 68 | 69 | it "should retain all properties of the casted attribute" do 70 | expect(@new_car.driver).to eq(@driver) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/unit/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "spec_helper" 3 | 4 | describe CouchRest::Model::Configuration do 5 | 6 | before do 7 | @class = Class.new(CouchRest::Model::Base) 8 | end 9 | 10 | describe '.configure' do 11 | it "should set a configuration parameter" do 12 | @class.add_config :foo_bar 13 | @class.configure do |config| 14 | config.foo_bar = 'monkey' 15 | end 16 | expect(@class.foo_bar).to eq('monkey') 17 | end 18 | end 19 | 20 | describe '.add_config' do 21 | 22 | it "should add a class level accessor" do 23 | @class.add_config :foo_bar 24 | @class.foo_bar = 'foo' 25 | expect(@class.foo_bar).to eq('foo') 26 | end 27 | 28 | ['foo', :foo, 45, ['foo', :bar]].each do |val| 29 | it "should be inheritable for a #{val.class}" do 30 | @class.add_config :foo_bar 31 | @child_class = Class.new(@class) 32 | 33 | @class.foo_bar = val 34 | expect(@class.foo_bar).to eq(val) 35 | expect(@child_class.foo_bar).to eq(val) 36 | 37 | @child_class.foo_bar = "bar" 38 | expect(@child_class.foo_bar).to eq("bar") 39 | 40 | expect(@class.foo_bar).to eq(val) 41 | end 42 | end 43 | 44 | 45 | it "should add an instance level accessor" do 46 | @class.add_config :foo_bar 47 | @class.foo_bar = 'foo' 48 | expect(@class.new.foo_bar).to eq('foo') 49 | end 50 | 51 | it "should add a convenient in-class setter" do 52 | @class.add_config :foo_bar 53 | @class.foo_bar "monkey" 54 | expect(@class.foo_bar).to eq("monkey") 55 | end 56 | end 57 | 58 | describe "General examples" do 59 | 60 | before(:all) do 61 | @default_model_key = 'model-type' 62 | end 63 | 64 | 65 | it "should be possible to override on class using configure method" do 66 | default_model_key = Cat.model_type_key 67 | Cat.instance_eval do 68 | model_type_key 'cat-type' 69 | end 70 | expect(CouchRest::Model::Base.model_type_key).to eql(default_model_key) 71 | expect(Cat.model_type_key).to eql('cat-type') 72 | cat = Cat.new 73 | expect(cat.model_type_key).to eql('cat-type') 74 | end 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /spec/unit/connection_config_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require "spec_helper" 3 | 4 | describe CouchRest::Model::ConnectionConfig do 5 | 6 | subject { CouchRest::Model::ConnectionConfig } 7 | 8 | describe ".instance" do 9 | 10 | it "should provide a singleton" do 11 | expect(subject.instance).to be_a(CouchRest::Model::ConnectionConfig) 12 | end 13 | 14 | end 15 | 16 | describe "#[file]" do 17 | 18 | let :file do 19 | File.join(FIXTURE_PATH, "config", "couchdb.yml") 20 | end 21 | 22 | it "should provide a config file hash" do 23 | conf = subject.instance[file] 24 | expect(conf).to be_a(Hash) 25 | end 26 | 27 | it "should provide a config file hash with symbolized keys" do 28 | conf = subject.instance[file] 29 | expect(conf[:development]).to be_a(Hash) 30 | expect(conf[:development]['host']).to be_a(String) 31 | end 32 | 33 | it "should always provide same hash" do 34 | f1 = subject.instance[file] 35 | f2 = subject.instance[file] 36 | expect(f1.object_id).to eql(f2.object_id) 37 | end 38 | 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/unit/connection_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe CouchRest::Model::Connection do 5 | 6 | before do 7 | @class = Class.new(CouchRest::Model::Base) 8 | end 9 | 10 | describe "instance methods" do 11 | before :each do 12 | @obj = @class.new 13 | end 14 | 15 | describe "#database" do 16 | it "should respond to" do 17 | expect(@obj).to respond_to(:database) 18 | end 19 | it "should provided class's database" do 20 | expect(@obj.class).to receive :database 21 | @obj.database 22 | end 23 | end 24 | 25 | describe "#server" do 26 | it "should respond to method" do 27 | expect(@obj).to respond_to(:server) 28 | end 29 | it "should return class's server" do 30 | expect(@obj.class).to receive :server 31 | @obj.server 32 | end 33 | end 34 | end 35 | 36 | describe "default configuration" do 37 | 38 | it "should provide environment" do 39 | expect(@class.environment).to eql(:development) 40 | end 41 | it "should provide connection config file" do 42 | expect(@class.connection_config_file).to eql(File.join(Dir.pwd, 'config', 'couchdb.yml')) 43 | end 44 | it "should provided simple connection details" do 45 | expect(@class.connection[:prefix]).to eql('couchrest') 46 | end 47 | 48 | end 49 | 50 | describe "class methods" do 51 | 52 | describe ".use_database" do 53 | it "should respond to" do 54 | expect(@class).to respond_to(:use_database) 55 | end 56 | it "should set the database if object provided" do 57 | db = @class.server.database('test') 58 | @class.use_database(db) 59 | expect(@class.database).to eql(db) 60 | end 61 | it "should use the database specified" do 62 | @class.use_database(:test) 63 | expect(@class.database.name).to eql('couchrest_test') 64 | end 65 | end 66 | 67 | describe ".database" do 68 | it "should respond to" do 69 | expect(@class).to respond_to(:database) 70 | end 71 | it "should provide a database object" do 72 | expect(@class.database).to be_a(CouchRest::Database) 73 | end 74 | it "should provide a database with default name" do 75 | expect(@class.database.name).to eql('couchrest') 76 | end 77 | end 78 | 79 | describe ".server" do 80 | it "should respond to" do 81 | expect(@class).to respond_to(:server) 82 | end 83 | it "should provide a server object" do 84 | expect(@class.server).to be_a(CouchRest::Server) 85 | end 86 | it "should provide a server with default config" do 87 | expect(@class.server.uri.to_s).to eql(CouchRest::Model::Base.server.uri.to_s) 88 | end 89 | it "should allow the configuration to be overwritten" do 90 | @class.connection = { 91 | :protocol => "https", 92 | :host => "127.0.0.1", 93 | :port => '5985', 94 | :prefix => 'sample', 95 | :suffix => 'test', 96 | :username => 'foo', 97 | :password => 'bar' 98 | } 99 | expect(@class.server.uri.to_s).to eql("https://foo:bar@127.0.0.1:5985") 100 | end 101 | 102 | it "should pass through the persistent connection option" do 103 | @class.connection[:persistent] = false 104 | expect(@class.server.connection_options[:persistent]).to be_falsey 105 | end 106 | 107 | end 108 | 109 | describe ".prepare_database" do 110 | it "should respond to" do 111 | expect(@class).to respond_to(:prepare_database) 112 | end 113 | 114 | it "should join the database name correctly" do 115 | @class.connection[:suffix] = 'db' 116 | db = @class.prepare_database('test') 117 | expect(db.name).to eql('couchrest_test_db') 118 | end 119 | 120 | it "should ignore nil values in database name" do 121 | @class.connection[:suffix] = nil 122 | db = @class.prepare_database('test') 123 | expect(db.name).to eql('couchrest_test') 124 | end 125 | 126 | it "should use the .use_database value" do 127 | @class.use_database('testing') 128 | db = @class.database 129 | expect(db.name).to eql('couchrest_testing') 130 | end 131 | 132 | end 133 | 134 | describe "protected methods" do 135 | 136 | describe ".connection_configuration" do 137 | it "should provide main config by default" do 138 | expect(@class.send(:connection_configuration)).to eql(@class.connection) 139 | end 140 | it "should load file if available" do 141 | @class.connection_config_file = File.join(FIXTURE_PATH, 'config', 'couchdb.yml') 142 | hash = @class.send(:connection_configuration) 143 | expect(hash[:protocol]).to eql('https') 144 | expect(hash[:host]).to eql('sample.cloudant.com') 145 | expect(hash[:join]).to eql('_') 146 | end 147 | end 148 | 149 | describe ".load_connection_config_file" do 150 | it "should provide an empty hash if config not found" do 151 | expect(@class.send(:load_connection_config_file)).to eql({}) 152 | end 153 | it "should load file if available" do 154 | @class.connection_config_file = File.join(FIXTURE_PATH, 'config', 'couchdb.yml') 155 | hash = @class.send(:load_connection_config_file) 156 | expect(hash[:development]).not_to be_nil 157 | expect(@class.server.uri.to_s).to eql("https://test:user@sample.cloudant.com") 158 | end 159 | 160 | end 161 | 162 | end 163 | 164 | end 165 | 166 | 167 | end 168 | -------------------------------------------------------------------------------- /spec/unit/core_extensions/time_parsing_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require File.expand_path('../../../spec_helper', __FILE__) 3 | 4 | describe "Time Parsing core extension" do 5 | 6 | describe "Time" do 7 | 8 | it "should respond to .parse_iso8601" do 9 | expect(Time.respond_to?("parse_iso8601")).to be_truthy 10 | end 11 | 12 | describe "#as_json" do 13 | 14 | it "should convert local time to JSON string" do 15 | time = Time.new(2011, 04, 01, 19, 05, 30, "+02:00") 16 | expect(time.as_json).to eql("2011-04-01T19:05:30.000+02:00") 17 | end 18 | 19 | it "should convert utc time to JSON string" do 20 | time = Time.utc(2011, 04, 01, 19, 05, 30) 21 | expect(time.as_json).to eql("2011-04-01T19:05:30.000Z") 22 | end 23 | 24 | it "should convert local time with fraction to JSON" do 25 | time = Time.new(2011, 04, 01, 19, 05, 30.123, "+02:00") 26 | expect(time.as_json).to eql("2011-04-01T19:05:30.123+02:00") 27 | end 28 | 29 | it "should convert utc time with fraction to JSON" do 30 | time = Time.utc(2011, 04, 01, 19, 05, 30.123) 31 | expect(time.as_json).to eql("2011-04-01T19:05:30.123Z") 32 | end 33 | 34 | it "should allow fraction digits" do 35 | time = Time.utc(2011, 04, 01, 19, 05, 30.123456) 36 | expect(time.as_json(:fraction_digits => 6)).to eql("2011-04-01T19:05:30.123456Z") 37 | end 38 | 39 | it "should use CouchRest::Model::Base.time_fraction_digits config option" do 40 | CouchRest::Model::Base.time_fraction_digits = 6 41 | time = Time.utc(2011, 04, 01, 19, 05, 30.123456) 42 | expect(time.as_json).to eql("2011-04-01T19:05:30.123456Z") 43 | CouchRest::Model::Base.time_fraction_digits = 3 # Back to normal 44 | end 45 | 46 | it "should cope with a nil options parameter" do 47 | time = Time.utc(2011, 04, 01, 19, 05, 30.123456) 48 | expect { time.as_json(nil) }.not_to raise_error 49 | end 50 | 51 | end 52 | 53 | describe ".parse_iso8601" do 54 | 55 | describe "parsing" do 56 | 57 | before :each do 58 | # Time.parse should not be called for these tests! 59 | allow(Time).to receive(:parse).and_return(nil) 60 | end 61 | 62 | it "should parse JSON time" do 63 | txt = "2011-04-01T19:05:30Z" 64 | expect(Time.parse_iso8601(txt)).to eql(Time.utc(2011, 04, 01, 19, 05, 30)) 65 | end 66 | 67 | it "should parse JSON time as UTC without Z" do 68 | txt = "2011-04-01T19:05:30" 69 | expect(Time.parse_iso8601(txt)).to eql(Time.utc(2011, 04, 01, 19, 05, 30)) 70 | end 71 | 72 | it "should parse basic time as UTC" do 73 | txt = "2011-04-01 19:05:30" 74 | expect(Time.parse_iso8601(txt)).to eql(Time.utc(2011, 04, 01, 19, 05, 30)) 75 | end 76 | 77 | it "should parse JSON time with zone" do 78 | txt = "2011-04-01T19:05:30 +02:00" 79 | expect(Time.parse_iso8601(txt)).to eql(Time.new(2011, 04, 01, 19, 05, 30, "+02:00")) 80 | end 81 | 82 | it "should parse JSON time with zone 2" do 83 | txt = "2011-04-01T19:05:30-0200" 84 | expect(Time.parse_iso8601(txt)).to eql(Time.new(2011, 04, 01, 19, 05, 30, "-02:00")) 85 | end 86 | 87 | it "should parse dodgy time with zone" do 88 | txt = "2011-04-01 19:05:30 +0200" 89 | expect(Time.parse_iso8601(txt)).to eql(Time.new(2011, 04, 01, 19, 05, 30, "+02:00")) 90 | end 91 | 92 | it "should parse dodgy time with zone 2" do 93 | txt = "2011-04-01 19:05:30+0230" 94 | expect(Time.parse_iso8601(txt)).to eql(Time.new(2011, 04, 01, 19, 05, 30, "+02:30")) 95 | end 96 | 97 | it "should parse dodgy time with zone 3" do 98 | txt = "2011-04-01 19:05:30 0230" 99 | expect(Time.parse_iso8601(txt)).to eql(Time.new(2011, 04, 01, 19, 05, 30, "+02:30")) 100 | end 101 | 102 | it "should parse JSON time with second fractions" do 103 | txt = "2014-12-11T16:53:54.000Z" 104 | expect(Time.parse_iso8601(txt)).to eql(Time.utc(2014, 12, 11, 16, 53, Rational(54000, 1000))) 105 | end 106 | 107 | it "should avoid rounding errors parsing JSON time with second fractions" do 108 | txt = "2014-12-11T16:53:54.548Z" 109 | expect(Time.parse_iso8601(txt)).to eql(Time.utc(2014, 12, 11, 16, 53, Rational(54548,1000))) 110 | end 111 | 112 | end 113 | 114 | it "avoids seconds rounding error" do 115 | time_string = "2014-12-11T16:54:54.549Z" 116 | expect(Time.parse_iso8601(time_string).as_json).to eql(time_string) 117 | end 118 | 119 | describe "resorting back to normal parse" do 120 | before :each do 121 | expect(Time).to receive(:parse) 122 | end 123 | it "should work with weird time" do 124 | txt = "16/07/1981 05:04:00" 125 | Time.parse_iso8601(txt) 126 | end 127 | 128 | end 129 | end 130 | 131 | end 132 | 133 | end 134 | -------------------------------------------------------------------------------- /spec/unit/designs/design_mapper_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe CouchRest::Model::Designs::DesignMapper do 6 | 7 | before :all do 8 | @klass = CouchRest::Model::Designs::DesignMapper 9 | end 10 | 11 | describe 'initialize without prefix' do 12 | 13 | before :all do 14 | @object = @klass.new(DesignModel) 15 | end 16 | 17 | it "should set basic variables" do 18 | expect(@object.send(:model)).to eql(DesignModel) 19 | expect(@object.send(:prefix)).to be_nil 20 | expect(@object.send(:method)).to eql('design_doc') 21 | end 22 | 23 | it "should add design doc to list" do 24 | expect(@object.model.design_docs).to include(@object.model.design_doc) 25 | end 26 | 27 | it "should create a design doc method" do 28 | expect(@object.model).to respond_to('design_doc') 29 | expect(@object.design_doc).to eql(@object.model.design_doc) 30 | end 31 | 32 | it "should use default for autoupdate" do 33 | expect(@object.design_doc.auto_update).to be_truthy 34 | end 35 | 36 | end 37 | 38 | describe 'initialize with prefix' do 39 | before :all do 40 | @object = @klass.new(DesignModel, 'stats') 41 | end 42 | 43 | it "should set basics" do 44 | expect(@object.send(:model)).to eql(DesignModel) 45 | expect(@object.send(:prefix)).to eql('stats') 46 | expect(@object.send(:method)).to eql('stats_design_doc') 47 | end 48 | 49 | it "should add design doc to list" do 50 | expect(@object.model.design_docs).to include(@object.model.stats_design_doc) 51 | end 52 | 53 | it "should not create an all method" do 54 | expect(@object.model).not_to respond_to('all') 55 | end 56 | 57 | it "should create a design doc method" do 58 | expect(@object.model).to respond_to('stats_design_doc') 59 | expect(@object.design_doc).to eql(@object.model.stats_design_doc) 60 | end 61 | 62 | end 63 | 64 | describe "#disable_auto_update" do 65 | it "should disable auto updates" do 66 | @object = @klass.new(DesignModel) 67 | @object.disable_auto_update 68 | expect(@object.design_doc.auto_update).to be_falsey 69 | end 70 | end 71 | 72 | describe "#enable_auto_update" do 73 | it "should enable auto updates" do 74 | @object = @klass.new(DesignModel) 75 | @object.enable_auto_update 76 | expect(@object.design_doc.auto_update).to be_truthy 77 | end 78 | end 79 | 80 | describe "#model_type_key" do 81 | it "should return models type key" do 82 | @object = @klass.new(DesignModel) 83 | expect(@object.model_type_key).to eql(@object.model.model_type_key) 84 | end 85 | end 86 | 87 | describe "#view" do 88 | 89 | before :each do 90 | @object = @klass.new(DesignModel) 91 | end 92 | 93 | it "should call create method on view" do 94 | expect(CouchRest::Model::Designs::View).to receive(:define).with(@object.design_doc, 'test', {}) 95 | @object.view('test') 96 | end 97 | 98 | it "should create a method on parent model" do 99 | allow(CouchRest::Model::Designs::View).to receive(:define) 100 | @object.view('test_view') 101 | expect(DesignModel).to respond_to(:test_view) 102 | end 103 | 104 | it "should create a method for view instance" do 105 | expect(@object.design_doc).to receive(:create_view).with('test', {}) 106 | @object.view('test') 107 | end 108 | end 109 | 110 | describe "#filter" do 111 | 112 | before :each do 113 | @object = @klass.new(DesignModel) 114 | end 115 | 116 | it "should add the provided function to the design doc" do 117 | @object.filter(:important, "function(doc, req) { return doc.priority == 'high'; }") 118 | expect(DesignModel.design_doc['filters']).not_to be_empty 119 | expect(DesignModel.design_doc['filters']['important']).not_to be_blank 120 | end 121 | end 122 | 123 | describe "#view_lib" do 124 | before :each do 125 | @object = @klass.new(DesignModel) 126 | end 127 | 128 | it "should add the #view_lib function to the design doc" do 129 | val = "exports.bar = 42;" 130 | @object.view_lib(:foo, val) 131 | expect(DesignModel.design_doc['views']['lib']).not_to be_empty 132 | expect(DesignModel.design_doc['views']['lib']).not_to be_blank 133 | expect(DesignModel.design_doc['views']['lib']['foo']).to eql(val) 134 | end 135 | end 136 | 137 | end 138 | 139 | -------------------------------------------------------------------------------- /spec/unit/designs/migrations_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | 4 | describe CouchRest::Model::Designs::Migrations do 5 | 6 | before :all do 7 | reset_test_db! 8 | end 9 | 10 | describe "base methods" do 11 | 12 | describe "#migrate" do 13 | # WARNING! ORDER IS IMPORTANT! 14 | 15 | describe "with limited changes" do 16 | 17 | class MigrationModelBase < CouchRest::Model::Base 18 | use_database DB 19 | property :name 20 | property :surname 21 | design do 22 | view :by_name 23 | end 24 | end 25 | 26 | class DesignSampleModelMigrate < MigrationModelBase 27 | end 28 | 29 | before :all do 30 | reset_test_db! 31 | @mod = DesignSampleModelMigrate 32 | @doc = @mod.design_doc 33 | @db = @mod.database 34 | end 35 | 36 | it "should create new design if non exists" do 37 | expect(@db).to receive(:view).with("#{@doc.name}/#{@doc['views'].keys.first}", { 38 | :limit => 1, :stale => 'update_after', :reduce => false 39 | }) 40 | callback = @doc.migrate do |res| 41 | expect(res).to eql(:created) 42 | end 43 | doc = @db.get(@doc['_id']) 44 | expect(doc['views']['all']).to eql(@doc['views']['all']) 45 | expect(doc['couchrest-hash']).not_to be_nil 46 | expect(callback).to be_nil 47 | end 48 | 49 | it "should not change anything if design is up to date" do 50 | @doc.sync 51 | expect(@db).not_to receive(:view) 52 | callback = @doc.migrate do |res| 53 | expect(res).to eql(:no_change) 54 | end 55 | expect(callback).to be_nil 56 | end 57 | 58 | end 59 | 60 | describe "migrating a document if there are changes" do 61 | 62 | class DesignSampleModelMigrate2 < MigrationModelBase 63 | end 64 | 65 | before :all do 66 | reset_test_db! 67 | @mod = DesignSampleModelMigrate2 68 | @doc = @mod.design_doc 69 | @db = @mod.database 70 | @doc.sync! 71 | @doc.create_view(:by_name_and_surname) 72 | @doc_id = @doc['_id'] + '_migration' 73 | end 74 | 75 | it "should save new migration design doc" do 76 | expect(@db).to receive(:view).with("#{@doc.name}_migration/by_name", { 77 | :limit => 1, :reduce => false, :stale => 'update_after' 78 | }) 79 | @callback = @doc.migrate do |res| 80 | expect(res).to eql(:migrated) 81 | end 82 | expect(@callback).not_to be_nil 83 | 84 | # should not have updated original view until cleanup 85 | doc = @db.get(@doc['_id']) 86 | expect(doc['views']).not_to have_key('by_name_and_surname') 87 | 88 | # Should have created the migration 89 | new_doc = @db.get(@doc_id) 90 | expect(new_doc).not_to be_nil 91 | 92 | # should be possible to perform cleanup 93 | @callback.call 94 | expect(@db.get(@doc_id)).to be_nil 95 | 96 | doc = @db.get(@doc['_id']) 97 | expect(doc['views']).to have_key('by_name_and_surname') 98 | end 99 | 100 | end 101 | 102 | end 103 | 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/unit/designs_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | 4 | describe CouchRest::Model::Designs do 5 | 6 | it "should accessable from model" do 7 | expect(DesignModel.respond_to?(:design)).to be_truthy 8 | end 9 | 10 | describe "class methods" do 11 | 12 | describe ".design" do 13 | 14 | before :each do 15 | @klass = DesignsModel.dup 16 | end 17 | 18 | describe "without block" do 19 | it "should create design_doc and all methods" do 20 | @klass.design 21 | expect(@klass).to respond_to(:design_doc) 22 | expect(@klass).to respond_to(:all) 23 | end 24 | 25 | it "should created named design_doc method and not all" do 26 | @klass.design :stats 27 | expect(@klass).to respond_to(:stats_design_doc) 28 | expect(@klass).not_to respond_to(:all) 29 | end 30 | 31 | it "should have added itself to a design_blocks array" do 32 | @klass.design 33 | blocks = @klass.instance_variable_get(:@_design_blocks) 34 | expect(blocks.length).to eql(1) 35 | expect(blocks.first).to eql({:args => [], :block => nil}) 36 | end 37 | 38 | it "should have added itself to a design_blocks array" do 39 | @klass.design 40 | blocks = @klass.instance_variable_get(:@_design_blocks) 41 | expect(blocks.length).to eql(1) 42 | expect(blocks.first).to eql({:args => [], :block => nil}) 43 | end 44 | 45 | it "should have added itself to a design_blocks array with prefix" do 46 | @klass.design :stats 47 | blocks = @klass.instance_variable_get(:@_design_blocks) 48 | expect(blocks.length).to eql(1) 49 | expect(blocks.first).to eql({:args => [:stats], :block => nil}) 50 | end 51 | end 52 | 53 | describe "with block" do 54 | before :each do 55 | @block = Proc.new do 56 | disable_auto_update 57 | end 58 | @klass.design &@block 59 | end 60 | 61 | it "should pass calls to mapper" do 62 | expect(@klass.design_doc.auto_update).to be_falsey 63 | end 64 | 65 | it "should have added itself to a design_blocks array" do 66 | blocks = @klass.instance_variable_get(:@_design_blocks) 67 | expect(blocks.length).to eql(1) 68 | expect(blocks.first).to eql({:args => [], :block => @block}) 69 | end 70 | 71 | it "should handle multiple designs" do 72 | @block2 = Proc.new do 73 | view :by_name 74 | end 75 | @klass.design :stats, &@block2 76 | blocks = @klass.instance_variable_get(:@_design_blocks) 77 | expect(blocks.length).to eql(2) 78 | expect(blocks.first).to eql({:args => [], :block => @block}) 79 | expect(blocks.last).to eql({:args => [:stats], :block => @block2}) 80 | end 81 | end 82 | 83 | end 84 | 85 | describe "inheritance" do 86 | before :each do 87 | klass = DesignModel.dup 88 | klass.design do 89 | view :by_name 90 | end 91 | @klass = Class.new(klass) 92 | end 93 | 94 | it "should add designs to sub module" do 95 | expect(@klass).to respond_to(:design_doc) 96 | end 97 | 98 | end 99 | 100 | describe "default_per_page" do 101 | it "should return 25 default" do 102 | expect(DesignModel.default_per_page).to eql(25) 103 | end 104 | end 105 | 106 | describe ".paginates_per" do 107 | it "should set the default per page value" do 108 | DesignModel.paginates_per(21) 109 | expect(DesignModel.default_per_page).to eql(21) 110 | end 111 | end 112 | end 113 | 114 | describe "Scenario testing" do 115 | 116 | describe "with auto update disabled" do 117 | 118 | before :all do 119 | reset_test_db! 120 | @mod = DesignsNoAutoUpdate 121 | end 122 | 123 | before(:all) do 124 | id = @mod.to_s 125 | doc = CouchRest::Document.new("_id" => "_design/#{id}") 126 | doc["language"] = "javascript" 127 | doc["views"] = {"all" => {"map" => "function(doc) { if (doc['type'] == '#{id}') { emit(doc['_id'],1); } }"}, 128 | "by_title" => {"map" => 129 | "function(doc) { 130 | if ((doc['type'] == '#{id}') && (doc['title'] != null)) { 131 | emit(doc['title'], 1); 132 | } 133 | }", "reduce" => "function(k,v,r) { return sum(v); }"}} 134 | DB.save_doc doc 135 | end 136 | 137 | it "will fail if reduce is not specific in view" do 138 | @mod.create(:title => 'This is a test') 139 | expect { @mod.by_title_fail.first }.to raise_error(CouchRest::NotFound) 140 | end 141 | 142 | it "will perform view request" do 143 | @mod.create(:title => 'This is a test') 144 | expect(@mod.by_title.first.title).to eql("This is a test") 145 | end 146 | 147 | end 148 | 149 | describe "using views" do 150 | 151 | describe "to find a single item" do 152 | 153 | before(:all) do 154 | reset_test_db! 155 | %w{aaa bbb ddd eee}.each do |title| 156 | Course.new(:title => title, :active => (title == 'bbb')).save 157 | end 158 | end 159 | 160 | it "should return single matched record with find helper" do 161 | course = Course.find_by_title('bbb') 162 | expect(course).not_to be_nil 163 | expect(course.title).to eql('bbb') # Ensure really is a Course! 164 | end 165 | 166 | it "should return nil if not found" do 167 | course = Course.find_by_title('fff') 168 | expect(course).to be_nil 169 | end 170 | 171 | it "should peform search on view with two properties" do 172 | course = Course.find_by_title_and_active(['bbb', true]) 173 | expect(course).not_to be_nil 174 | expect(course.title).to eql('bbb') # Ensure really is a Course! 175 | end 176 | 177 | it "should return nil if not found" do 178 | course = Course.find_by_title_and_active(['bbb', false]) 179 | expect(course).to be_nil 180 | end 181 | 182 | it "should raise exception if view not present" do 183 | expect { Course.find_by_foobar('123') }.to raise_error(NoMethodError) 184 | end 185 | 186 | end 187 | 188 | describe "a model with a compound key view" do 189 | before(:all) do 190 | reset_test_db! 191 | written_at = Time.now - 24 * 3600 * 7 192 | @titles = ["uniq one", "even more interesting", "less fun", "not junk"] 193 | @user_ids = ["quentin", "aaron"] 194 | @titles.each_with_index do |title,i| 195 | u = i % 2 196 | a = Article.new(:title => title, :user_id => @user_ids[u]) 197 | a.date = written_at 198 | a.save 199 | written_at += 24 * 3600 200 | end 201 | end 202 | it "should create the design doc" do 203 | Article.by_user_id_and_date rescue nil 204 | doc = Article.design_doc 205 | expect(doc['views']['by_date']).not_to be_nil 206 | end 207 | it "should sort correctly" do 208 | articles = Article.by_user_id_and_date.all 209 | expect(articles.collect{|a|a['user_id']}).to eq(['aaron', 'aaron', 'quentin', 210 | 'quentin']) 211 | expect(articles[1].title).to eq('not junk') 212 | end 213 | it "should be queryable with couchrest options" do 214 | articles = Article.by_user_id_and_date(:limit => 1, :startkey => 'quentin').all 215 | expect(articles.length).to eq(1) 216 | expect(articles[0].title).to eq("even more interesting") 217 | end 218 | end 219 | 220 | 221 | end 222 | 223 | end 224 | 225 | end 226 | -------------------------------------------------------------------------------- /spec/unit/inherited_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class PlainParent 4 | class_attribute :foo 5 | self.foo = :bar 6 | end 7 | 8 | class PlainChild < PlainParent 9 | end 10 | 11 | class ExtendedParent < CouchRest::Model::Base 12 | class_attribute :foo 13 | self.foo = :bar 14 | end 15 | 16 | class ExtendedChild < ExtendedParent 17 | end 18 | 19 | describe "Using chained inheritance without CouchRest::Model::Base" do 20 | it "should preserve inheritable attributes" do 21 | expect(PlainParent.foo).to eq(:bar) 22 | expect(PlainChild.foo).to eq(:bar) 23 | end 24 | end 25 | 26 | describe "Using chained inheritance with CouchRest::Model::Base" do 27 | it "should preserve inheritable attributes" do 28 | expect(ExtendedParent.foo).to eq(:bar) 29 | expect(ExtendedChild.foo).to eq(:bar) 30 | end 31 | end 32 | 33 | 34 | -------------------------------------------------------------------------------- /spec/unit/property_protection_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Model Attributes" do 4 | 5 | describe "no declarations" do 6 | class NoProtection < CouchRest::Model::Base 7 | use_database DB 8 | property :name 9 | property :phone 10 | end 11 | 12 | it "should not protect anything through new" do 13 | user = NoProtection.new(:name => "will", :phone => "555-5555") 14 | 15 | expect(user.name).to eq("will") 16 | expect(user.phone).to eq("555-5555") 17 | end 18 | 19 | it "should not protect anything through attributes=" do 20 | user = NoProtection.new 21 | user.attributes = {:name => "will", :phone => "555-5555"} 22 | 23 | expect(user.name).to eq("will") 24 | expect(user.phone).to eq("555-5555") 25 | end 26 | 27 | it "should recreate from the database properly" do 28 | user = NoProtection.new 29 | user.name = "will" 30 | user.phone = "555-5555" 31 | user.save! 32 | 33 | user = NoProtection.get(user.id) 34 | expect(user.name).to eq("will") 35 | expect(user.phone).to eq("555-5555") 36 | end 37 | 38 | it "should provide a list of all properties as accessible" do 39 | user = NoProtection.new(:name => "will", :phone => "555-5555") 40 | expect(user.accessible_properties.length).to eql(2) 41 | expect(user.protected_properties).to be_empty 42 | end 43 | end 44 | 45 | describe "Model Base", "accessible flag" do 46 | class WithAccessible < CouchRest::Model::Base 47 | use_database DB 48 | property :name, :accessible => true 49 | property :admin, :default => false 50 | end 51 | 52 | it { expect { WithAccessible.new(nil) }.to_not raise_error } 53 | 54 | it "should recognize accessible properties" do 55 | props = WithAccessible.accessible_properties.map { |prop| prop.name} 56 | expect(props).to include("name") 57 | expect(props).not_to include("admin") 58 | end 59 | 60 | it "should protect non-accessible properties set through new" do 61 | user = WithAccessible.new(:name => "will", :admin => true) 62 | 63 | expect(user.name).to eq("will") 64 | expect(user.admin).to eq(false) 65 | end 66 | 67 | it "should protect non-accessible properties set through attributes=" do 68 | user = WithAccessible.new 69 | user.attributes = {:name => "will", :admin => true} 70 | 71 | expect(user.name).to eq("will") 72 | expect(user.admin).to eq(false) 73 | end 74 | 75 | it "should provide correct accessible and protected property lists" do 76 | user = WithAccessible.new(:name => 'will', :admin => true) 77 | expect(user.accessible_properties.map{|p| p.to_s}).to eql(['name']) 78 | expect(user.protected_properties.map{|p| p.to_s}).to eql(['admin']) 79 | end 80 | end 81 | 82 | describe "Model Base", "protected flag" do 83 | class WithProtected < CouchRest::Model::Base 84 | use_database DB 85 | property :name 86 | property :admin, :default => false, :protected => true 87 | end 88 | 89 | it { expect { WithProtected.new(nil) }.to_not raise_error } 90 | 91 | it "should recognize protected properties" do 92 | props = WithProtected.protected_properties.map { |prop| prop.name} 93 | expect(props).not_to include("name") 94 | expect(props).to include("admin") 95 | end 96 | 97 | it "should protect non-accessible properties set through new" do 98 | user = WithProtected.new(:name => "will", :admin => true) 99 | 100 | expect(user.name).to eq("will") 101 | expect(user.admin).to eq(false) 102 | end 103 | 104 | it "should protect non-accessible properties set through attributes=" do 105 | user = WithProtected.new 106 | user.attributes = {:name => "will", :admin => true} 107 | 108 | expect(user.name).to eq("will") 109 | expect(user.admin).to eq(false) 110 | end 111 | 112 | it "should not modify the provided attribute hash" do 113 | user = WithProtected.new 114 | attrs = {:name => "will", :admin => true} 115 | user.attributes = attrs 116 | expect(attrs[:admin]).to be_truthy 117 | expect(attrs[:name]).to eql('will') 118 | end 119 | 120 | it "should provide correct accessible and protected property lists" do 121 | user = WithProtected.new(:name => 'will', :admin => true) 122 | expect(user.accessible_properties.map{|p| p.to_s}).to eql(['name']) 123 | expect(user.protected_properties.map{|p| p.to_s}).to eql(['admin']) 124 | end 125 | 126 | end 127 | 128 | describe "Model Base", "mixing protected and accessible flags" do 129 | class WithBothAndUnspecified < CouchRest::Model::Base 130 | use_database DB 131 | property :name, :accessible => true 132 | property :admin, :default => false, :protected => true 133 | property :phone, :default => 'unset phone number' 134 | end 135 | 136 | it { expect { WithBothAndUnspecified.new }.to_not raise_error } 137 | 138 | it 'should assume that any unspecified property is protected by default' do 139 | user = WithBothAndUnspecified.new(:name => 'will', :admin => true, :phone => '555-1234') 140 | 141 | expect(user.name).to eq('will') 142 | expect(user.admin).to eq(false) 143 | expect(user.phone).to eq('unset phone number') 144 | end 145 | 146 | end 147 | 148 | describe "from database" do 149 | class WithProtected < CouchRest::Model::Base 150 | use_database DB 151 | property :name 152 | property :admin, :default => false, :protected => true 153 | design do 154 | view :by_name 155 | end 156 | end 157 | 158 | before(:each) do 159 | @user = WithProtected.new 160 | @user.name = "will" 161 | @user.admin = true 162 | @user.save! 163 | end 164 | 165 | def verify_attrs(user) 166 | expect(user.name).to eq("will") 167 | expect(user.admin).to eq(true) 168 | end 169 | 170 | it "Base#get should not strip protected attributes" do 171 | reloaded = WithProtected.get( @user.id ) 172 | verify_attrs reloaded 173 | end 174 | 175 | it "Base#get! should not strip protected attributes" do 176 | reloaded = WithProtected.get!( @user.id ) 177 | verify_attrs reloaded 178 | end 179 | 180 | it "Base#all should not strip protected attributes" do 181 | # all creates a CollectionProxy 182 | docs = WithProtected.all(:key => @user.id) 183 | expect(docs.length).to eq(1) 184 | reloaded = docs.first 185 | verify_attrs reloaded 186 | end 187 | 188 | it "views should not strip protected attributes" do 189 | docs = WithProtected.by_name(:startkey => "will", :endkey => "will") 190 | reloaded = docs.first 191 | verify_attrs reloaded 192 | end 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /spec/unit/property_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe CouchRest::Model::Property do 5 | 6 | let :klass do 7 | CouchRest::Model::Property 8 | end 9 | 10 | it "should provide name as string" do 11 | property = CouchRest::Model::Property.new(:test, :type => String) 12 | expect(property.name).to eql('test') 13 | expect(property.to_s).to eql('test') 14 | end 15 | 16 | it "should provide name as a symbol" do 17 | property = CouchRest::Model::Property.new(:test, :type => String) 18 | expect(property.name.to_sym).to eql(:test) 19 | expect(property.to_sym).to eql(:test) 20 | end 21 | 22 | it "should provide class from type" do 23 | property = CouchRest::Model::Property.new(:test, :type => String) 24 | expect(property.type).to eql(String) 25 | expect(property.array).to be_falsey 26 | end 27 | 28 | it "should provide base class from type in array" do 29 | property = CouchRest::Model::Property.new(:test, :type => [String]) 30 | expect(property.type).to eql(String) 31 | expect(property.array).to be_truthy 32 | end 33 | 34 | it "should provide base class and set array type" do 35 | property = CouchRest::Model::Property.new(:test, :type => String, :array => true) 36 | expect(property.type).to eql(String) 37 | expect(property.array).to be_truthy 38 | end 39 | 40 | it "should raise error if type as string requested" do 41 | expect { 42 | CouchRest::Model::Property.new(:test, :type => 'String') 43 | }.to raise_error(/Defining a property type as a String is not supported/) 44 | end 45 | 46 | it "should leave type nil and return class as nil also" do 47 | property = CouchRest::Model::Property.new(:test, :type => nil) 48 | expect(property.type).to be_nil 49 | end 50 | 51 | it "should convert empty type array to [Object]" do 52 | property = CouchRest::Model::Property.new(:test, :type => []) 53 | expect(property.type).to eql(Object) 54 | end 55 | 56 | it "should set init method option or leave as 'new'" do 57 | # (bad example! Time already typecast) 58 | property = CouchRest::Model::Property.new(:test, :type => Time) 59 | expect(property.init_method).to eql('new') 60 | property = CouchRest::Model::Property.new(:test, :type => Time, :init_method => 'parse') 61 | expect(property.init_method).to eql('parse') 62 | end 63 | 64 | it "should set the allow_blank option to true by default" do 65 | property = CouchRest::Model::Property.new(:test, :type => String) 66 | expect(property.allow_blank).to be_truthy 67 | end 68 | 69 | it "should allow setting of the allow_blank option to false" do 70 | property = CouchRest::Model::Property.new(:test, :type => String, :allow_blank => false) 71 | expect(property.allow_blank).to be_falsey 72 | end 73 | 74 | it "should convert block to type" do 75 | prop = klass.new(:test) do 76 | property :testing 77 | end 78 | expect(prop.array).to be_falsey 79 | expect(prop.type).not_to be_nil 80 | expect(prop.type.class).to eql(Class) 81 | obj = prop.type.new 82 | expect(obj).to respond_to(:testing) 83 | end 84 | 85 | it "should convert block to type with array" do 86 | prop = klass.new(:test, :array => true) do 87 | property :testing 88 | end 89 | expect(prop.type).not_to be_nil 90 | expect(prop.type.class).to eql(Class) 91 | expect(prop.array).to be_truthy 92 | end 93 | 94 | describe "#build" do 95 | it "should allow instantiation of new object" do 96 | property = CouchRest::Model::Property.new(:test, :type => Date) 97 | obj = property.build(2011, 05, 21) 98 | expect(obj).to eql(Date.new(2011, 05, 21)) 99 | end 100 | it "should use init_method if provided" do 101 | property = CouchRest::Model::Property.new(:test, :type => Date, :init_method => 'parse') 102 | obj = property.build("2011-05-21") 103 | expect(obj).to eql(Date.new(2011, 05, 21)) 104 | end 105 | it "should use init_method Proc if provided" do 106 | property = CouchRest::Model::Property.new(:test, :type => Date, :init_method => Proc.new{|v| Date.parse(v)}) 107 | obj = property.build("2011-05-21") 108 | expect(obj).to eql(Date.new(2011, 05, 21)) 109 | end 110 | it "should raise error if no class" do 111 | property = CouchRest::Model::Property.new(:test) 112 | expect { property.build }.to raise_error(StandardError, /Cannot build/) 113 | end 114 | end 115 | 116 | ## Property Casting method. More thoroughly tested in typecast_spec. 117 | 118 | describe "casting" do 119 | it "should cast a value" do 120 | property = CouchRest::Model::Property.new(:test, :type => Date) 121 | parent = double("FooObject") 122 | expect(property.cast(parent, "2010-06-16")).to eql(Date.new(2010, 6, 16)) 123 | expect(property.cast_value(parent, "2010-06-16")).to eql(Date.new(2010, 6, 16)) 124 | end 125 | 126 | it "should cast an array of values" do 127 | property = CouchRest::Model::Property.new(:test, :type => [Date]) 128 | parent = double("FooObject") 129 | expect(property.cast(parent, ["2010-06-01", "2010-06-02"])).to eql([Date.new(2010, 6, 1), Date.new(2010, 6, 2)]) 130 | end 131 | 132 | it "should cast an array of values with array option" do 133 | property = CouchRest::Model::Property.new(:test, :type => Date, :array => true) 134 | parent = double("FooObject") 135 | expect(property.cast(parent, ["2010-06-01", "2010-06-02"])).to eql([Date.new(2010, 6, 1), Date.new(2010, 6, 2)]) 136 | end 137 | 138 | context "when allow_blank is false" do 139 | let :parent do 140 | double("FooObject") 141 | end 142 | 143 | it "should convert blank to nil" do 144 | property = CouchRest::Model::Property.new(:test, :type => String, :allow_blank => false) 145 | expect(property.cast(parent, "")).to be_nil 146 | end 147 | 148 | it "should remove blank array entries" do 149 | property = CouchRest::Model::Property.new(:test, :type => [String], :allow_blank => false) 150 | expect(property.cast(parent, ["", "foo"])).to eql(["foo"]) 151 | end 152 | end 153 | 154 | it "should set a CastedArray on array of Objects" do 155 | property = CouchRest::Model::Property.new(:test, :type => [Object]) 156 | parent = double("FooObject") 157 | expect(property.cast(parent, ["2010-06-01", "2010-06-02"]).class).to eql(CouchRest::Model::CastedArray) 158 | end 159 | 160 | it "should set a CastedArray on array of Strings" do 161 | property = CouchRest::Model::Property.new(:test, :type => [String]) 162 | parent = double("FooObject") 163 | expect(property.cast(parent, ["2010-06-01", "2010-06-02"]).class).to eql(CouchRest::Model::CastedArray) 164 | end 165 | 166 | it "should allow instantion of model via CastedArray#build" do 167 | property = CouchRest::Model::Property.new(:dates, :type => [Date]) 168 | parent = Article.new 169 | ary = property.cast(parent, []) 170 | obj = ary.build(2011, 05, 21) 171 | expect(ary.length).to eql(1) 172 | expect(ary.first).to eql(Date.new(2011, 05, 21)) 173 | obj = ary.build(2011, 05, 22) 174 | expect(ary.length).to eql(2) 175 | expect(ary.last).to eql(Date.new(2011, 05, 22)) 176 | end 177 | 178 | it "should cast an object that provides an array" do 179 | prop = Class.new do 180 | attr_accessor :ary 181 | def initialize(val); self.ary = val; end 182 | def as_json; ary; end 183 | end 184 | property = CouchRest::Model::Property.new(:test, :type => prop) 185 | parent = double("FooClass") 186 | cast = property.cast(parent, [1, 2]) 187 | expect(cast.ary).to eql([1, 2]) 188 | end 189 | 190 | it "should set parent as casted_by object in CastedArray" do 191 | property = CouchRest::Model::Property.new(:test, :type => [Object]) 192 | parent = double("FooObject") 193 | expect(property.cast(parent, ["2010-06-01", "2010-06-02"]).casted_by).to eql(parent) 194 | end 195 | 196 | it "should set casted_by on new value" do 197 | property = CouchRest::Model::Property.new(:test, :type => CatToy) 198 | parent = double("CatObject") 199 | cast = property.cast(parent, {:name => 'catnip'}) 200 | expect(cast.casted_by).to eql(parent) 201 | end 202 | 203 | end 204 | 205 | end 206 | 207 | -------------------------------------------------------------------------------- /spec/unit/server_pool_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require "spec_helper" 3 | 4 | describe CouchRest::Model::ServerPool do 5 | 6 | subject { CouchRest::Model::ServerPool } 7 | 8 | describe ".instance" do 9 | 10 | it "should provide a singleton" do 11 | expect(subject.instance).to be_a(CouchRest::Model::ServerPool) 12 | end 13 | 14 | end 15 | 16 | describe "#[url]" do 17 | 18 | it "should provide a server object" do 19 | srv = subject.instance[COUCHHOST] 20 | expect(srv).to be_a(CouchRest::Server) 21 | end 22 | 23 | it "should always provide same object" do 24 | srv = subject.instance[COUCHHOST] 25 | srv2 = subject.instance[COUCHHOST] 26 | expect(srv.object_id).to eql(srv2.object_id) 27 | end 28 | 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /spec/unit/subclass_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | # add a default value 4 | Card.property :bg_color, :default => '#ccc' 5 | 6 | class BusinessCard < Card 7 | property :extension_code 8 | property :job_title 9 | 10 | validates_presence_of :extension_code 11 | validates_presence_of :job_title 12 | end 13 | 14 | class DesignBusinessCard < BusinessCard 15 | property :bg_color, :default => '#eee' 16 | end 17 | 18 | class OnlineCourse < Course 19 | property :url 20 | design do 21 | view :by_url 22 | end 23 | end 24 | 25 | class Animal < CouchRest::Model::Base 26 | use_database DB 27 | property :name 28 | design do 29 | view :by_name 30 | end 31 | end 32 | 33 | class Dog < Animal; end 34 | 35 | describe "Subclassing a Model" do 36 | 37 | before(:each) do 38 | @card = BusinessCard.new 39 | end 40 | 41 | it "shouldn't messup the parent's properties" do 42 | expect(Card.properties).not_to eq(BusinessCard.properties) 43 | end 44 | 45 | it "should share the same db default" do 46 | expect(@card.database.uri).to eq(Card.database.uri) 47 | end 48 | 49 | it "should have kept the validation details" do 50 | expect(@card).not_to be_valid 51 | end 52 | 53 | it "should have added the new validation details" do 54 | validated_fields = @card.class.validators.map{|v| v.attributes}.flatten 55 | expect(validated_fields).to include(:extension_code) 56 | expect(validated_fields).to include(:job_title) 57 | end 58 | 59 | it "should not add to the parent's validations" do 60 | validated_fields = Card.validators.map{|v| v.attributes}.flatten 61 | expect(validated_fields).not_to include(:extension_code) 62 | expect(validated_fields).not_to include(:job_title) 63 | end 64 | 65 | it "should inherit default property values" do 66 | expect(@card.bg_color).to eq('#ccc') 67 | end 68 | 69 | it "should be able to overwrite a default property" do 70 | expect(DesignBusinessCard.new.bg_color).to eq('#eee') 71 | end 72 | 73 | it "should have a design doc slug based on the subclass name" do 74 | expect(OnlineCourse.design_doc['_id']).to match(/OnlineCourse$/) 75 | end 76 | 77 | it "should not add views to the parent's design_doc" do 78 | expect(Course.design_doc['views'].keys).not_to include('by_url') 79 | end 80 | 81 | it "should add the parent's views to its design doc" do 82 | expect(OnlineCourse.design_doc['views'].keys).to include('by_title') 83 | end 84 | 85 | it "should add the parent's views but alter the model names in map function" do 86 | expect(OnlineCourse.design_doc['views']['by_title']['map']).to match(/doc\['#{OnlineCourse.model_type_key}'\] == 'OnlineCourse'/) 87 | end 88 | 89 | it "should have an all view with a guard clause for model == subclass name in the map function" do 90 | expect(OnlineCourse.design_doc['views']['all']['map']).to match(/if \(doc\['#{OnlineCourse.model_type_key}'\] == 'OnlineCourse'\)/) 91 | end 92 | 93 | end 94 | 95 | -------------------------------------------------------------------------------- /spec/unit/support/couchrest_database_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe CouchRest::Model::Support::Database do 4 | 5 | describe "#delete!" do 6 | 7 | it "should empty design cache after a database is destroyed" do 8 | Thread.current[:couchrest_design_cache] = { :foo => :bar } 9 | DB.delete! 10 | expect(Thread.current[:couchrest_design_cache]).to be_empty 11 | end 12 | 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/translations_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "spec_helper" 3 | 4 | describe "ActiveModel Translations" do 5 | 6 | describe ".human_attribute_name" do 7 | it "should provide translation" do 8 | expect(Card.human_attribute_name(:first_name)).to eql("First name") 9 | end 10 | end 11 | 12 | describe ".i18n_scope" do 13 | it "should provide activemodel default" do 14 | expect(Card.i18n_scope).to eql(:couchrest) 15 | end 16 | end 17 | 18 | describe ".lookup_ancestors" do 19 | it "should provide basic lookup" do 20 | expect(Cat.lookup_ancestors).to eql([Cat]) 21 | end 22 | 23 | it "should provide lookup with ancestors" do 24 | expect(ChildCat.lookup_ancestors).to eql([ChildCat, Cat]) 25 | end 26 | 27 | it "should provide Base if request directly" do 28 | expect(CouchRest::Model::Base.lookup_ancestors).to eql([CouchRest::Model::Base]) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/unit/utils/migrate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class MigrateModel < CouchRest::Model::Base 4 | use_database :migrations 5 | proxy_database_method :id 6 | proxy_for :migrate_proxy_models 7 | property :name 8 | property :value 9 | design { view :by_name } 10 | end 11 | 12 | class MigrateProxyModel < CouchRest::Model::Base 13 | proxied_by :migrate_model 14 | proxy_database_method :id 15 | proxy_for :migrate_proxy_nested_models 16 | property :name 17 | property :value 18 | design { view :by_name } 19 | end 20 | 21 | class MigrateProxyNestedModel < CouchRest::Model::Base 22 | proxied_by :migrate_proxy_model 23 | property :name 24 | property :value 25 | design { view :by_name } 26 | end 27 | 28 | RSpec::Matchers.define :database_matching do |database| 29 | match do |actual| 30 | actual.server == database.server && actual.name == database.name 31 | end 32 | end 33 | 34 | describe CouchRest::Model::Utils::Migrate do 35 | 36 | before :each do 37 | @module = CouchRest::Model::Utils::Migrate 38 | end 39 | 40 | describe "#load_all_models" do 41 | it "should not do anything if Rails is not available" do 42 | @module.load_all_models 43 | end 44 | 45 | context "when detect Rails" do 46 | before do 47 | Rails = double() 48 | end 49 | 50 | it "should reload models but should not load anything inside models/concerns folder" do 51 | allow(Rails).to receive(:root).and_return("") 52 | expect(Dir).to receive(:[]).with("app/models/**/*.rb").and_return(Dir[ File.join(MODEL_PATH, "**/*.rb") ]) 53 | 54 | @module.load_all_models 55 | 56 | expect(defined?(Cat)).to eq("constant") # as an example 57 | expect(defined?(Attachable)).to eq(nil) 58 | end 59 | 60 | it "should raise a LoadError for invalid model paths" do 61 | allow(Rails).to receive(:root).and_return("") 62 | expect(Dir).to receive(:[]).with("app/models/**/*.rb").and_return(['failed_require']) 63 | 64 | expect { 65 | @module.load_all_models 66 | }.to raise_error(LoadError) 67 | end 68 | end 69 | end 70 | 71 | describe "migrations" do 72 | let!(:stdout) { $stdout } 73 | before :each do 74 | allow(CouchRest::Model::Base).to receive(:subclasses).and_return([MigrateModel, MigrateProxyModel, MigrateProxyNestedModel]) 75 | $stdout = StringIO.new 76 | end 77 | 78 | after :each do 79 | $stdout = stdout 80 | end 81 | 82 | describe "#all_models" do 83 | it "should migrate root subclasses of CouchRest::Model::Base" do 84 | expect(MigrateModel.design_docs.first).to receive(:migrate) 85 | @module.all_models 86 | end 87 | 88 | it "shouldn't migrate proxied subclasses with of CouchRest::Model::Base" do 89 | expect(MigrateProxyModel.design_docs.first).not_to receive(:migrate) 90 | expect(MigrateProxyNestedModel.design_docs.first).not_to receive(:migrate) 91 | @module.all_models 92 | end 93 | 94 | context "migration design docs" do 95 | before :each do 96 | @module.all_models 97 | @design_doc = MigrateModel.design_doc 98 | end 99 | 100 | it "shouldn't modify the original design doc if activate is false" do 101 | @design_doc.create_view(:by_name_and_id) 102 | @module.all_models(activate: false) 103 | 104 | fetched_ddoc = MigrateModel.get(@design_doc.id) 105 | expect(fetched_ddoc['views']).not_to have_key('by_name_and_id') 106 | end 107 | 108 | it "should remove a leftover migration doc" do 109 | @design_doc.create_view(:by_name_and_value) 110 | @module.all_models(activate: false) 111 | 112 | expect(MigrateModel.get("#{@design_doc.id}_migration")).not_to be_nil 113 | @module.all_models 114 | expect(MigrateModel.get("#{@design_doc.id}_migration")).to be_nil 115 | end 116 | end 117 | end 118 | 119 | describe "#all_models_and_proxies" do 120 | before :each do 121 | # clear data from previous test runs 122 | MigrateModel.all.each do |mm| 123 | next if mm.nil? 124 | mm.migrate_proxy_models.all.each do |mpm| 125 | mpm.migrate_proxy_nested_models.database.delete! rescue nil 126 | end rescue nil 127 | mm.migrate_proxy_models.database.delete! 128 | mm.destroy rescue nil 129 | end 130 | MigrateModel.database.recreate! 131 | end 132 | 133 | it "should migrate first level proxied subclasses of CouchRest::Model::Base" do 134 | mm = MigrateModel.new(name: "Migration").save 135 | expect(MigrateProxyModel.design_docs.first).to receive(:migrate).with(database_matching(mm.migrate_proxy_models.database)).and_call_original 136 | @module.all_models_and_proxies 137 | end 138 | 139 | it "should migrate the second level proxied subclasses of CouchRest::Model::Base" do 140 | mm = MigrateModel.new(name: "Migration").save 141 | mpm = mm.migrate_proxy_models.new(name: "Migration Proxy").save 142 | expect(MigrateProxyNestedModel.design_docs.first).to receive(:migrate).with(database_matching(mpm.migrate_proxy_nested_models.database)) 143 | @module.all_models_and_proxies 144 | end 145 | 146 | context "migration design docs" do 147 | before :each do 148 | @mm_instance = MigrateModel.new(name: "Migration").save 149 | mpm = @mm_instance.migrate_proxy_models.new(name: "Migration Proxy") 150 | 151 | @module.all_models_and_proxies 152 | 153 | @design_doc = MigrateProxyModel.design_doc 154 | end 155 | 156 | it "shouldn't modify the original design doc if activate is false" do 157 | @design_doc.create_view(:by_name_and_id) 158 | @module.all_models_and_proxies(activate: false) 159 | 160 | fetched_ddoc = @mm_instance.migrate_proxy_models.get(@design_doc.id) 161 | expect(fetched_ddoc['views']).not_to have_key('by_name_and_id') 162 | end 163 | 164 | it "should remove a leftover migration doc" do 165 | @design_doc.create_view(:by_name_and_value) 166 | @module.all_models_and_proxies(activate: false) 167 | 168 | expect(@mm_instance.migrate_proxy_models.get("#{@design_doc.id}_migration")).not_to be_nil 169 | @module.all_models_and_proxies 170 | expect(@mm_instance.migrate_proxy_models.get("#{@design_doc.id}_migration")).to be_nil 171 | end 172 | end 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /spec/unit/validations_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe CouchRest::Model::Validations do 4 | 5 | describe "Uniqueness" do 6 | 7 | context "basic" do 8 | before(:all) do 9 | @objs = ['title 1', 'title 2', 'title 3'].map{|t| WithUniqueValidation.create(:title => t)} 10 | end 11 | 12 | it "should create a new view if none defined before performing" do 13 | expect(WithUniqueValidation.design_doc.has_view?(:by_title)).to be_truthy 14 | end 15 | 16 | it "should validate a new unique document" do 17 | @obj = WithUniqueValidation.create(:title => 'title 4') 18 | expect(@obj.new?).not_to be_truthy 19 | expect(@obj).to be_valid 20 | end 21 | 22 | it "should not validate a non-unique document" do 23 | @obj = WithUniqueValidation.create(:title => 'title 1') 24 | expect(@obj).not_to be_valid 25 | expect(@obj.errors[:title]).to eq(["has already been taken"]) 26 | end 27 | 28 | it "should save already created document" do 29 | @obj = @objs.first 30 | expect(@obj.save).not_to be_falsey 31 | expect(@obj).to be_valid 32 | end 33 | 34 | 35 | it "should allow own view to be specified" do 36 | # validates_uniqueness_of :code, :view => 'all' 37 | WithUniqueValidationView.create(:title => 'title 1', :code => '1234') 38 | @obj = WithUniqueValidationView.new(:title => 'title 5', :code => '1234') 39 | expect(@obj).not_to be_valid 40 | end 41 | 42 | it "should raise an error if specified view does not exist" do 43 | WithUniqueValidationView.validates_uniqueness_of :title, :view => 'fooobar' 44 | @obj = WithUniqueValidationView.new(:title => 'title 2', :code => '12345') 45 | expect { 46 | @obj.valid? 47 | }.to raise_error(/WithUniqueValidationView.fooobar does not exist for validation/) 48 | end 49 | 50 | it "should not try to create a defined view" do 51 | WithUniqueValidationView.validates_uniqueness_of :title, :view => 'fooobar' 52 | expect(WithUniqueValidationView.design_doc.has_view?('fooobar')).to be_falsey 53 | expect(WithUniqueValidationView.design_doc.has_view?('by_title')).to be_falsey 54 | end 55 | 56 | 57 | it "should not try to create new view when already defined" do 58 | @obj = @objs[1] 59 | expect(@obj.class.design_doc).not_to receive('create_view') 60 | @obj.valid? 61 | end 62 | end 63 | 64 | context "with a proxy parameter" do 65 | 66 | it "should create a new view despite proxy" do 67 | expect(WithUniqueValidationProxy.design_doc.has_view?(:by_title)).to be_truthy 68 | end 69 | 70 | it "should be used" do 71 | @obj = WithUniqueValidationProxy.new(:title => 'test 6') 72 | proxy = expect(@obj).to receive('proxy').and_return(@obj.class) 73 | expect(@obj.valid?).to be_truthy 74 | end 75 | 76 | it "should allow specific view" do 77 | @obj = WithUniqueValidationProxy.new(:title => 'test 7') 78 | expect(@obj.class).not_to receive('by_title') 79 | view = double('View') 80 | allow(view).to receive(:rows).and_return([]) 81 | proxy = double('Proxy') 82 | expect(proxy).to receive('by_title').and_return(view) 83 | expect(proxy).to receive('respond_to?').with('by_title').and_return(true) 84 | expect(@obj).to receive('proxy').and_return(proxy) 85 | @obj.valid? 86 | end 87 | end 88 | 89 | context "when proxied" do 90 | it "should lookup the model_proxy" do 91 | view = double('View') 92 | allow(view).to receive(:rows).and_return([]) 93 | mp = double(:ModelProxy) 94 | expect(mp).to receive(:by_title).and_return(view) 95 | @obj = WithUniqueValidation.new(:title => 'test 8') 96 | allow(@obj).to receive(:model_proxy).twice.and_return(mp) 97 | @obj.valid? 98 | end 99 | end 100 | 101 | context "with a scope" do 102 | before(:all) do 103 | @objs = [['title 1', 1], ['title 2', 1], ['title 3', 1]].map{|t| WithScopedUniqueValidation.create(:title => t[0], :parent_id => t[1])} 104 | @objs_nil = [['title 1', nil], ['title 2', nil], ['title 3', nil]].map{|t| WithScopedUniqueValidation.create(:title => t[0], :parent_id => t[1])} 105 | end 106 | 107 | it "should create the view" do 108 | @objs.first.class.design_doc.has_view?('by_parent_id_and_title') 109 | end 110 | 111 | it "should validate unique document" do 112 | @obj = WithScopedUniqueValidation.create(:title => 'title 4', :parent_id => 1) 113 | expect(@obj).to be_valid 114 | end 115 | 116 | it "should validate unique document outside of scope" do 117 | @obj = WithScopedUniqueValidation.create(:title => 'title 1', :parent_id => 2) 118 | expect(@obj).to be_valid 119 | end 120 | 121 | it "should validate non-unique document" do 122 | @obj = WithScopedUniqueValidation.create(:title => 'title 1', :parent_id => 1) 123 | expect(@obj).not_to be_valid 124 | expect(@obj.errors[:title]).to eq(["has already been taken"]) 125 | end 126 | 127 | it "should validate unique document will nil scope" do 128 | @obj = WithScopedUniqueValidation.create(:title => 'title 4', :parent_id => nil) 129 | expect(@obj).to be_valid 130 | end 131 | 132 | it "should validate non-unique document with nil scope" do 133 | @obj = WithScopedUniqueValidation.create(:title => 'title 1', :parent_id => nil) 134 | expect(@obj).not_to be_valid 135 | expect(@obj.errors[:title]).to eq(["has already been taken"]) 136 | end 137 | 138 | end 139 | 140 | end 141 | 142 | end 143 | --------------------------------------------------------------------------------