├── .gitignore ├── .travis.yml ├── .yardopts ├── CONTRIBUTING.markdown ├── Gemfile ├── HISTORY.markdown ├── README.markdown ├── Rakefile ├── couchbase-model.gemspec ├── lib ├── couchbase-model.rb ├── couchbase │ ├── active_model.rb │ ├── model.rb │ ├── model │ │ ├── configuration.rb │ │ ├── ext │ │ │ ├── camelize.rb │ │ │ ├── constantize.rb │ │ │ └── singleton_class.rb │ │ ├── uuid.rb │ │ └── version.rb │ └── railtie.rb └── rails │ └── generators │ ├── couchbase │ ├── config │ │ ├── config_generator.rb │ │ └── templates │ │ │ └── couchbase.yml │ └── view │ │ ├── templates │ │ ├── map.js │ │ └── reduce.js │ │ └── view_generator.rb │ └── couchbase_generator.rb ├── tasks ├── package.rake ├── test.rake └── util.rake └── test ├── setup.rb ├── test_active_model_integration.rb ├── test_model.rb ├── test_model_rails_integration.rb └── test_uuid.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | test/CouchbaseMock.jar 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - wget -O- http://packages.couchbase.com/ubuntu/couchbase.key | sudo apt-key add - 3 | - echo deb http://packages.couchbase.com/preview/ubuntu lucid lucid/main | sudo tee /etc/apt/sources.list.d/couchbase.list 4 | - sudo apt-get update 5 | - sudo apt-get -y install libevent-dev libvbucket-dev libcouchbase-dev 6 | 7 | rvm: 8 | - 1.8.7 9 | - 1.9.2 10 | - 1.9.3 11 | - ree 12 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --protected 2 | --no-private 3 | - 4 | README.markdown 5 | HISTORY.markdown 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.markdown: -------------------------------------------------------------------------------- 1 | We've decided to use "gerrit" for our code review system, making it 2 | easier for all of us to contribute with code and comments. 3 | 4 | 1. Visit http://review.couchbase.org and "Register" for an account 5 | 2. Review http://review.couchbase.org/static/individual_agreement.html 6 | 3. Agree to agreement by visiting http://review.couchbase.org/#/settings/agreements 7 | 4. If you do not receive an email, please contact us 8 | 5. Check out the `couchbase-ruby-model` area http://review.couchbase.org/#/q/status:open+project:couchbase-ruby-model,n,z 9 | 6. Join us on IRC at #libcouchbase on Freenode :-) 10 | 11 | We normally don't go looking for stuff in gerrit, so you should add at 12 | least me `"Sergey Avseyev" ` as a reviewer 13 | for your patch (and I'll know who else to add and add them for you). 14 | 15 | ## Contributing Using Repo Tool 16 | 17 | Follow ["Uploading Changes" guide][1] on the site if you have some code to contribute. 18 | 19 | All you should need to set up your development environment should be: 20 | 21 | ~ % mkdir couchbase-ruby 22 | ~ % cd couchbase-ruby 23 | ~/couchbase-ruby % repo init -u git://github.com/trondn/manifests.git -m ruby.xml 24 | ~/couchbase-ruby % repo sync 25 | ~/couchbase-ruby % repo start my-branch-name --all 26 | ~/couchbase-ruby % make 27 | 28 | This will build the latest version of `libcouchbase`, 29 | `couchbase-ruby-client` and `couchbase-ruby-model` libraries. You must 30 | have a C and C++ compiler installed, automake, autoconf. 31 | 32 | If you have to make any changes just commit them before you upload 33 | them to gerrit with the following command: 34 | 35 | ~/couchbase-ruby/model % repo upload 36 | 37 | You might experience a problem trying to upload the patches if you've 38 | selected a different login name at http://review.couchbase.org than 39 | your login name. Don't worry, all you need to do is to add the 40 | following to your ~/.gitconfig file: 41 | 42 | [review "review.couchbase.org"] 43 | username = YOURNAME 44 | 45 | ## Contributing Using Plain Git 46 | 47 | If you not so familiar with repo tool and its workflow there is 48 | alternative way to do the same job. Lets assume you have installed 49 | couchbase gem and libcouchbase from official packages and would you to 50 | contribute to couchbase-model gem only. Then you just need to complete 51 | gerrit registration steps above and clone the source repository 52 | (remember the repository on github.com is just a mirror): 53 | 54 | ~ % git clone ssh://YOURNAME@review.couchbase.org:29418/couchbase-ruby-model.git 55 | 56 | Install [`commit-msg` hook][2]: 57 | 58 | ~/couchbase-ruby-model % scp -p -P 29418 YOURNAME@review.couchbase.org:hooks/commit-msg .git/hooks/ 59 | 60 | Make your changes and upload them for review: 61 | 62 | ~/couchbase-ruby-model % git commit 63 | ~/couchbase-ruby-model % git push origin HEAD:refs/for/master 64 | 65 | If you need to fix or add something to your patch, do it and re-upload 66 | the changes (all you need is to keep `Change-Id:` line the same to 67 | allow gerrit to track the patch. 68 | 69 | ~/couchbase-ruby-model % git commit --amend 70 | ~/couchbase-ruby-model % git push origin HEAD:refs/for/master 71 | 72 | Happy hacking! 73 | 74 | [1]: http://review.couchbase.org/Documentation/user-upload.html 75 | [2]: http://review.couchbase.org/Documentation/user-changeid.html 76 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in couchbase-model.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /HISTORY.markdown: -------------------------------------------------------------------------------- 1 | ## 0.5.4 / 2014-08-30 2 | 3 | * Improved ActiveModel support 4 | * Fixed stale CAS on model reload 5 | 6 | ## 0.5.3 / 2013-06-06 7 | 8 | * Prefer single-quoted strings (Andrey Koleshko) 9 | * Test for Model.design_document (Andrey Koleshko) 10 | * Support for batch finding multiple objects by id (Jon Moses) 11 | * Test for activemodel instead of rails for activemodel validations 12 | * Update couchbase dependency to 1.3.0 13 | 14 | ## 0.5.2 / 2013-02-25 15 | 16 | * Fix attribute inheritance when subclassing (Mike Evans) 17 | * Added as_json method for rails JSON responses (Stephen von Takach) 18 | * Add contributing document 19 | * Fix test hiding 20 | * Remove comments from the javascript sources 21 | * Reduce development dependencies and update jar version 22 | 23 | ## 0.5.1 / 2012-11-29 24 | 25 | * Introduce save! and create! methods and raise RecordInvalid only from them 26 | 27 | ## 0.5.0 / 2012-11-21 28 | 29 | * Update template for map function 30 | * Use extended get for #find_by_id 31 | * Do not use HashWithIndifferentAccess class unless it defined 32 | * Pass options to #create method 33 | * Ensure validness on create 34 | * Fix storing raw data 35 | * Define read_attribute and write_attribute methods 36 | * Support couchbase 1.2.0.z.beta4 37 | 38 | ## 0.4.4 / 2012-10-17 39 | 40 | * Make #to_param aware about keys 41 | 42 | ## 0.4.2 / 2012-10-17 43 | 44 | * Update CAS value after mutation 45 | * Added ability to pass options to mutators. Thanks to @kierangraham 46 | * Always try to include Rails stuff into model 47 | * Use key if id is nil (makes sense for some view results) 48 | 49 | ## 0.4.1 / 2012-09-26 50 | 51 | * Put support notes in README 52 | * Add note about validations in the README 53 | * Update repo URL 54 | * RCBC-85 Fix typo in `save' method 55 | 56 | ## 0.4.0 / 2012-09-25 57 | 58 | * Add validation hooks for Rails application 59 | * Check meta presence as more robust indicator of key presence 60 | 61 | ## 0.3.1 / 2012-09-22 62 | 63 | * Allow to specify default storage options 64 | 65 | ## 0.3.0 / 2012-09-22 66 | 67 | * Implement belongs_to asscociation 68 | * Use ActiveModel naming and conversion 69 | * Define persisted? method 70 | * Allow optional CAS value for mutators 71 | * Use replace in save method. Thanks to @scalabl3 72 | * Add callbacks for :save, :create, :update and :delete methods 73 | 74 | ## 0.2.0 / 2012-09-18 75 | 76 | * Add Rails 3 configuration possibilities, allow configuring 77 | ensure_design_documents to disable/enable the auto view upgrade 78 | (thanks to David Rice) 79 | * Ensure views directory is always set (thanks to David Rice) 80 | * Fix tests for ruby 1.8.7 81 | * Reword header in README 82 | * Merge pull request #3 from davidjrice/master 83 | * Use debugger gem 84 | * Update Model wrapper to match latest API changes 85 | * Strip contents of the JS file 86 | * Do not submit empty views 87 | * Allow to specify default view options 88 | * Display only non-nil values 89 | * Rename underscored methods 90 | * Load spatial views into design document 91 | 92 | ## 0.1.0 / 2012-04-10 93 | 94 | * Allows to define several attributes at once 95 | * Allow to specify default value 96 | * Add missing @since and @return tags 97 | * Add railtie 98 | * Add config generator 99 | * Use verbose mode by default for GET operation 100 | * Add views generators 101 | * Update document wrapper 102 | * Add code to upgrade design docs automatically 103 | * Cache design document signature in memory 104 | * Use symbols for attribute hash 105 | * Skip connection errors during start up 106 | * Don't show config warning for config generator 107 | * Calculate mtime of the design document 108 | * Assign current_doc after creation 109 | * Update readme file 110 | * Use preview repository for travis 111 | * Do not make zipball 112 | * Show model attributes with model class 113 | * Update test. The couchbase gem is using new defaults 114 | 115 | ## 0.0.1/ 2012-03-17 116 | 117 | * Initial version 118 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Couchbase Model 2 | 3 | This library allows to declare models for [couchbase gem][1]. 4 | 5 | ## SUPPORT 6 | 7 | If you found an issue, please file it in our [JIRA][3]. Also you are 8 | always welcome on `#libcouchbase` channel at [freenode.net IRC servers][4]. 9 | 10 | Documentation: [http://rdoc.info/gems/couchbase-model](http://rdoc.info/gems/couchbase-model) 11 | 12 | ## Rails integration 13 | 14 | To generate config you can use `rails generate couchbase:config`: 15 | 16 | $ rails generate couchbase:config 17 | create config/couchbase.yml 18 | 19 | It will generate this `config/couchbase.yml` for you: 20 | 21 | common: &common 22 | hostname: localhost 23 | port: 8091 24 | username: 25 | password: 26 | pool: default 27 | 28 | development: 29 | <<: *common 30 | bucket: couchbase_tinyurl_development 31 | 32 | test: 33 | <<: *common 34 | bucket: couchbase_tinyurl_test 35 | 36 | # set these environment variables on your production server 37 | production: 38 | hostname: <%= ENV['COUCHBASE_HOST'] %> 39 | port: <%= ENV['COUCHBASE_PORT'] %> 40 | username: <%= ENV['COUCHBASE_USERNAME'] %> 41 | password: <%= ENV['COUCHBASE_PASSWORD'] %> 42 | pool: <%= ENV['COUCHBASE_POOL'] %> 43 | bucket: <%= ENV['COUCHBASE_BUCKET'] %> 44 | 45 | ## Examples 46 | 47 | require 'couchbase/model' 48 | 49 | class Post < Couchbase::Model 50 | attribute :title 51 | attribute :body 52 | attribute :draft 53 | end 54 | 55 | p = Post.new(:id => 'hello-world', 56 | :title => 'Hello world', 57 | :draft => true) 58 | p.save 59 | p = Post.find('hello-world') 60 | p.body = "Once upon the times...." 61 | p.save 62 | p.update(:draft => false) 63 | Post.bucket.get('hello-world') #=> {"title"=>"Hello world", "draft"=>false, 64 | # "body"=>"Once upon the times...."} 65 | 66 | You can also let the library generate the unique identifier for you: 67 | 68 | p = Post.create(:title => 'How to generate ID', 69 | :body => 'Open up the editor...') 70 | p.id #=> "74f43c3116e788d09853226603000809" 71 | 72 | There are several algorithms available. By default it use `:sequential` 73 | algorithm, but you can change it to more suitable one for you: 74 | 75 | class Post < Couchbase::Model 76 | attribute :title 77 | attribute :body 78 | attribute :draft 79 | 80 | uuid_algorithm :random 81 | end 82 | 83 | You can define connection options on per model basis: 84 | 85 | class Post < Couchbase::Model 86 | attribute :title 87 | attribute :body 88 | attribute :draft 89 | 90 | connect :port => 80, :bucket => 'blog' 91 | end 92 | 93 | ## Validations 94 | 95 | There are all methods from ActiveModel::Validations accessible in 96 | context of rails application: 97 | 98 | class Comment < Couchbase::Model 99 | attribute :author, :body 100 | 101 | validates_presence_of :author, :body 102 | end 103 | 104 | ## Views (aka Map/Reduce indexes) 105 | 106 | Views are stored in models directory in subdirectory named after the 107 | model (to be precious `design_document` attribute of the model class). 108 | Here is an example of directory layout for `Link` model with three 109 | views. 110 | 111 | . 112 | └── app 113 | └── models 114 | ├── link 115 | │   ├── total_count 116 | │   │   ├── map.js 117 | │   │   └── reduce.js 118 | │   ├── by_created_at 119 | │   │   └── map.js 120 | │   └── by_view_count 121 | │   └── map.js 122 | └── link.rb 123 | 124 | To generate view you can use yet another generator `rails generate 125 | couchbase:view DESIGNDOCNAME VIEWNAME`. For example how `total_count` 126 | view could be generated: 127 | 128 | $ rails generate couchbase:view link total_count 129 | 130 | The generated files contains useful info and links about how to write 131 | map and reduce functions, you can take a look at them in the [templates 132 | directory][2]. 133 | 134 | In the model class you should declare accessible views: 135 | 136 | class Post < Couchbase::Model 137 | attribute :title 138 | attribute :body 139 | attribute :draft 140 | attribute :view_count 141 | attribute :created_at, :default => lambda { Time.now } 142 | 143 | view :total_count, :by_created_at, :by_view_count 144 | end 145 | 146 | And request them later: 147 | 148 | Post.by_created_at(:include_docs => true).each do |post| 149 | puts post.title 150 | end 151 | 152 | Post.by_view_count(:include_docs => true).group_by(&:view_count) do |count, posts| 153 | p "#{count} -> #{posts.map{|pp| pp.inspect}.join(', ')}" 154 | end 155 | 156 | 157 | [1]: https://github.com/couchbase/couchbase-ruby-client/ 158 | [2]: https://github.com/couchbase/couchbase-ruby-model/blob/master/lib/rails/generators/couchbase/view/templates/ 159 | [3]: http://couchbase.com/issues/browse/RCBC 160 | [4]: http://freenode.net/irc_servers.shtml 161 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'bundler/gem_tasks' 19 | 20 | Dir['tasks/*.rake'].sort.each { |f| load f } 21 | 22 | task :default => :test 23 | -------------------------------------------------------------------------------- /couchbase-model.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'couchbase/model/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'couchbase-model' 7 | s.version = Couchbase::Model::VERSION 8 | s.author = 'Couchbase' 9 | s.email = 'support@couchbase.com' 10 | s.homepage = 'https://github.com/couchbase/couchbase-ruby-model' 11 | s.summary = %q{Declarative interface to Couchbase} 12 | s.description = %q{ORM-like interface allows you to persist your models to Couchbase} 13 | 14 | s.files = `git ls-files`.split("\n") 15 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 16 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 17 | s.require_paths = ['lib'] 18 | 19 | s.add_runtime_dependency 'couchbase', '~> 1.3.3' 20 | s.add_runtime_dependency 'activemodel' 21 | 22 | s.add_development_dependency 'rake' 23 | s.add_development_dependency 'minitest' 24 | s.add_development_dependency 'activesupport' 25 | end 26 | -------------------------------------------------------------------------------- /lib/couchbase-model.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'couchbase/model' 19 | 20 | # If we are using Rails then we will include the Couchbase railtie. 21 | if defined?(Rails) 22 | require 'couchbase/railtie' 23 | end 24 | -------------------------------------------------------------------------------- /lib/couchbase/active_model.rb: -------------------------------------------------------------------------------- 1 | module Couchbase 2 | module ActiveModel 3 | 4 | def self.included(base) 5 | base.class_eval do 6 | extend ::ActiveModel::Callbacks 7 | extend ::ActiveModel::Naming 8 | include ::ActiveModel::Conversion 9 | include ::ActiveModel::Validations 10 | include ::ActiveModel::Validations::Callbacks 11 | include ::ActiveModel::Dirty 12 | 13 | define_model_callbacks :create, :update, :delete, :destroy, :save, :initialize 14 | [:save, :create, :update, :delete, :destroy, :initialize].each do |meth| 15 | class_eval <<-EOC 16 | alias #{meth}_without_callbacks #{meth} 17 | def #{meth}(*args, &block) 18 | run_callbacks(:#{meth}) do 19 | #{meth}_without_callbacks(*args, &block) 20 | end 21 | end 22 | EOC 23 | end 24 | end 25 | end 26 | 27 | # Public: Allows for access to ActiveModel functionality. 28 | # 29 | # Returns self. 30 | def to_model 31 | self 32 | end 33 | 34 | # Public: Hashes our unique key instead of the entire object. 35 | # Ruby normally hashes an object to be used in comparisons. In our case 36 | # we may have two techincally different objects referencing the same entity id, 37 | # so we will hash just the class and id (via to_key) to compare so we get the 38 | # expected result 39 | # 40 | # Returns a string representing the unique key. 41 | def hash 42 | to_param.hash 43 | end 44 | 45 | # Public: Overrides eql? to use == in the comparison. 46 | # 47 | # other - Another object to compare to 48 | # 49 | # Returns a boolean. 50 | def eql?(other) 51 | self == other 52 | end 53 | 54 | # Public: Overrides == to compare via class and entity id. 55 | # 56 | # other - Another object to compare to 57 | # 58 | # Example 59 | # 60 | # movie = Movie.find(1234) 61 | # movie.to_key 62 | # # => 'movie-1234' 63 | # 64 | # Returns a string representing the unique key. 65 | def ==(other) 66 | hash == other.hash 67 | end 68 | 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/couchbase/model.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'digest/md5' 19 | 20 | require 'couchbase' 21 | require 'couchbase/active_model' 22 | require 'couchbase/model/version' 23 | require 'couchbase/model/uuid' 24 | require 'couchbase/model/configuration' 25 | require 'active_model' 26 | 27 | unless Object.respond_to?(:singleton_class) 28 | require 'couchbase/model/ext/singleton_class' 29 | end 30 | unless ''.respond_to?(:constantize) 31 | require 'couchbase/model/ext/constantize' 32 | end 33 | unless ''.respond_to?(:camelize) 34 | require 'couchbase/model/ext/camelize' 35 | end 36 | 37 | module Couchbase 38 | 39 | # @since 0.0.1 40 | class Error::MissingId < Error::Base; end 41 | 42 | # @since 0.4.0 43 | class Error::RecordInvalid < Error::Base 44 | attr_reader :record 45 | def initialize(record) 46 | @record = record 47 | if @record.errors 48 | super(@record.errors.full_messages.join(', ')) 49 | else 50 | super('Record invalid') 51 | end 52 | end 53 | end 54 | 55 | # Declarative layer for Couchbase gem 56 | # 57 | # @since 0.0.1 58 | # 59 | # require 'couchbase/model' 60 | # 61 | # class Post < Couchbase::Model 62 | # attribute :title 63 | # attribute :body 64 | # attribute :draft 65 | # end 66 | # 67 | # p = Post.new(:id => 'hello-world', 68 | # :title => 'Hello world', 69 | # :draft => true) 70 | # p.save 71 | # p = Post.find('hello-world') 72 | # p.body = "Once upon the times...." 73 | # p.save 74 | # p.update(:draft => false) 75 | # Post.bucket.get('hello-world') #=> {"title"=>"Hello world", "draft"=>false, 76 | # # "body"=>"Once upon the times...."} 77 | # 78 | # You can also let the library generate the unique identifier for you: 79 | # 80 | # p = Post.create(:title => 'How to generate ID', 81 | # :body => 'Open up the editor...') 82 | # p.id #=> "74f43c3116e788d09853226603000809" 83 | # 84 | # There are several algorithms available. By default it use `:sequential` 85 | # algorithm, but you can change it to more suitable one for you: 86 | # 87 | # class Post < Couchbase::Model 88 | # attribute :title 89 | # attribute :body 90 | # attribute :draft 91 | # 92 | # uuid_algorithm :random 93 | # end 94 | # 95 | # You can define connection options on per model basis: 96 | # 97 | # class Post < Couchbase::Model 98 | # attribute :title 99 | # attribute :body 100 | # attribute :draft 101 | # 102 | # connect :port => 80, :bucket => 'blog' 103 | # end 104 | class Model 105 | 106 | # Each model must have identifier 107 | # 108 | # @since 0.0.1 109 | attr_accessor :id 110 | 111 | # @since 0.2.0 112 | attr_reader :key 113 | 114 | # @since 0.2.0 115 | attr_reader :value 116 | 117 | # @since 0.2.0 118 | attr_reader :doc 119 | 120 | # @since 0.2.0 121 | attr_reader :meta 122 | 123 | # @since 0.4.5 124 | attr_reader :errors 125 | 126 | # @since 0.4.5 127 | attr_reader :raw 128 | 129 | # @private Container for all attributes with defaults of all subclasses 130 | @@attributes = {} 131 | 132 | # @private Container for all view names of all subclasses 133 | @@views = {} 134 | 135 | # Use custom connection options 136 | # 137 | # @since 0.0.1 138 | # 139 | # @param [String, Hash, Array] options options for establishing 140 | # connection. 141 | # @return [Couchbase::Bucket] 142 | # 143 | # @see Couchbase::Bucket#initialize 144 | # 145 | # @example Choose specific bucket 146 | # class Post < Couchbase::Model 147 | # connect :bucket => 'posts' 148 | # ... 149 | # end 150 | def self.connect(*options) 151 | self.bucket = Couchbase.connect(*options) 152 | end 153 | 154 | # Associate custom design document with the model 155 | # 156 | # Design document is the special document which contains views, the 157 | # chunks of code for building map/reduce indexes. When this method 158 | # called without argument, it just returns the effective design document 159 | # name. 160 | # 161 | # @since 0.1.0 162 | # 163 | # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views.html 164 | # 165 | # @param [String, Symbol] name the name for the design document. By 166 | # default underscored model name is used. 167 | # @return [String] the effective design document 168 | # 169 | # @example Choose specific design document name 170 | # class Post < Couchbase::Model 171 | # design_document :my_posts 172 | # ... 173 | # end 174 | def self.design_document(name = nil) 175 | if name 176 | @_design_doc = name.to_s 177 | else 178 | @_design_doc ||= begin 179 | name = self.name.dup 180 | name.gsub!(/::/, '_') 181 | name.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') 182 | name.gsub!(/([a-z\d])([A-Z])/,'\1_\2') 183 | name.downcase! 184 | end 185 | end 186 | end 187 | 188 | def self.defaults(options = nil) 189 | if options 190 | @_defaults = options 191 | else 192 | @_defaults || {} 193 | end 194 | end 195 | 196 | # Ensure that design document is up to date. 197 | # 198 | # @since 0.1.0 199 | # 200 | # This method also cares about organizing view in separate javascript 201 | # files. The general structure is the following (+[root]+ is the 202 | # directory, one of the {Model::Configuration.design_documents_paths}): 203 | # 204 | # [root] 205 | # | 206 | # `- link 207 | # | | 208 | # | `- by_created_at 209 | # | | | 210 | # | | `- map.js 211 | # | | 212 | # | `- by_session_id 213 | # | | | 214 | # | | `- map.js 215 | # | | 216 | # | `- total_views 217 | # | | | 218 | # | | `- map.js 219 | # | | | 220 | # | | `- reduce.js 221 | # 222 | # The directory structure above demonstrate layout for design document 223 | # with id +_design/link+ and three views: +by_created_at+, 224 | # +by_session_id` and `total_views`. 225 | def self.ensure_design_document! 226 | unless Configuration.design_documents_paths 227 | raise 'Configuration.design_documents_path must be directory' 228 | end 229 | 230 | doc = {'_id' => "_design/#{design_document}", 'views' => {}} 231 | digest = Digest::MD5.new 232 | mtime = 0 233 | views.each do |name, _| 234 | doc['views'][name] = {} 235 | doc['spatial'] = {} 236 | ['map', 'reduce', 'spatial'].each do |type| 237 | Configuration.design_documents_paths.each do |path| 238 | ff = File.join(path, design_document.to_s, name.to_s, "#{type}.js") 239 | if File.file?(ff) 240 | contents = File.read(ff).gsub(/^\s*\/\/.*$\n\r?/, '').strip 241 | next if contents.empty? 242 | mtime = [mtime, File.mtime(ff).to_i].max 243 | digest << contents 244 | case type 245 | when 'map', 'reduce' 246 | doc['views'][name][type] = contents 247 | when 'spatial' 248 | doc['spatial'][name] = contents 249 | end 250 | break # pick first matching file 251 | end 252 | end 253 | end 254 | end 255 | 256 | doc['views'].delete_if {|_, v| v.empty? } 257 | doc.delete('spatial') if doc['spatial'] && doc['spatial'].empty? 258 | doc['signature'] = digest.to_s 259 | doc['timestamp'] = mtime 260 | if doc['signature'] != thread_storage[:signature] && doc['timestamp'] > thread_storage[:timestamp].to_i 261 | current_doc = bucket.design_docs[design_document.to_s] 262 | if current_doc.nil? || (current_doc['signature'] != doc['signature'] && doc['timestamp'] > current_doc[:timestamp].to_i) 263 | bucket.save_design_doc(doc) 264 | current_doc = doc 265 | end 266 | thread_storage[:signature] = current_doc['signature'] 267 | thread_storage[:timestamp] = current_doc['timestamp'].to_i 268 | end 269 | end 270 | 271 | # Choose the UUID generation algorithms 272 | # 273 | # @since 0.0.1 274 | # 275 | # @param [Symbol] algorithm (:sequential) one of the available 276 | # algorithms. 277 | # 278 | # @see Couchbase::UUID#next 279 | # 280 | # @example Select :random UUID generation algorithm 281 | # class Post < Couchbase::Model 282 | # uuid_algorithm :random 283 | # ... 284 | # end 285 | # 286 | # @return [Symbol] 287 | def self.uuid_algorithm(algorithm) 288 | self.thread_storage[:uuid_algorithm] = algorithm 289 | end 290 | 291 | def read_attribute(attr_name) 292 | @_attributes[attr_name] 293 | end 294 | alias :[] :read_attribute 295 | 296 | def write_attribute(attr_name, value) 297 | attribute_will_change!(attr_name) unless @_attributes[attr_name] == value 298 | @_attributes[attr_name] = value 299 | end 300 | alias :[]= :write_attribute 301 | 302 | # Defines an attribute for the model 303 | # 304 | # @since 0.0.1 305 | # 306 | # @param [Symbol, String] name name of the attribute 307 | # 308 | # @example Define some attributes for a model 309 | # class Post < Couchbase::Model 310 | # attribute :title 311 | # attribute :body 312 | # attribute :published_at 313 | # end 314 | # 315 | # post = Post.new(:title => 'Hello world', 316 | # :body => 'This is the first example...', 317 | # :published_at => Time.now) 318 | def self.attribute(*names) 319 | options = {} 320 | if names.last.is_a?(Hash) 321 | options = names.pop 322 | end 323 | names.each do |name| 324 | name = name.to_sym 325 | attributes[name] = options[:default] 326 | next if self.instance_methods.include?(name) 327 | define_method(name) do 328 | read_attribute(name) 329 | end 330 | define_method(:"#{name}=") do |value| 331 | write_attribute(name, value) 332 | end 333 | end 334 | end 335 | 336 | # Defines a view for the model 337 | # 338 | # @since 0.0.1 339 | # 340 | # @param [Symbol, String, Array] names names of the views 341 | # @param [Hash] options options passed to the {Couchbase::View} 342 | # 343 | # @example Define some views for a model 344 | # class Post < Couchbase::Model 345 | # view :all, :published 346 | # view :by_rating, :include_docs => false 347 | # end 348 | # 349 | # post = Post.find("hello") 350 | # post.by_rating.each do |r| 351 | # # ... 352 | # end 353 | def self.view(*names) 354 | options = {:wrapper_class => self, :include_docs => true} 355 | if names.last.is_a?(Hash) 356 | options.update(names.pop) 357 | end 358 | is_spatial = options.delete(:spatial) 359 | names.each do |name| 360 | path = '_design/%s/_%s/%s' % [design_document, is_spatial ? 'spatial' : 'view', name] 361 | views[name] = lambda do |*params| 362 | params = options.merge(params.first || {}) 363 | View.new(bucket, path, params) 364 | end 365 | singleton_class.send(:define_method, name, &views[name]) 366 | end 367 | end 368 | 369 | # Defines a belongs_to association for the model 370 | # 371 | # @since 0.3.0 372 | # 373 | # @param [Symbol, String] name name of the associated model 374 | # @param [Hash] options association options 375 | # @option options [String, Symbol] :class_name the name of the 376 | # association class 377 | # 378 | # @example Define some association for a model 379 | # class Brewery < Couchbase::Model 380 | # attribute :name 381 | # end 382 | # 383 | # class Beer < Couchbase::Model 384 | # attribute :name, :brewery_id 385 | # belongs_to :brewery 386 | # end 387 | # 388 | # Beer.find("heineken").brewery.name 389 | def self.belongs_to(name, options = {}) 390 | ref = "#{name}_id" 391 | attribute(ref) 392 | assoc = (options[:class_name] || name).to_s.camelize.constantize 393 | 394 | # Define reader 395 | define_method(name) do 396 | begin 397 | assoc.find(self.send(ref)) 398 | rescue Couchbase::Error::NotFound 399 | end 400 | end 401 | # Define writer 402 | attr_writer name 403 | define_method(:"#{name}=") do |value| 404 | if value 405 | self.send("#{ref}=", value.id) 406 | else 407 | self.send("#{ref}=", nil) 408 | end 409 | 410 | instance_variable_set("@#{name}", value) 411 | end 412 | end 413 | 414 | class << self 415 | def _find(quiet, *ids) 416 | wants_array = ids.first.kind_of?(Array) 417 | ids = ids.flatten.compact.uniq 418 | if ids.empty? 419 | raise Couchbase::Error::NotFound unless quiet 420 | else 421 | res = bucket.get(ids, :quiet => quiet, :extended => true).map do |id, (obj, flags, cas)| 422 | obj = {:raw => obj} unless obj.is_a?(Hash) 423 | new({:id => id, :meta => {'flags' => flags, 'cas' => cas}}.merge(obj)) 424 | end 425 | wants_array ? res : res.first 426 | end 427 | end 428 | 429 | private :_find 430 | end 431 | 432 | # Find the model using +id+ attribute 433 | # 434 | # @since 0.0.1 435 | # 436 | # @param [String, Symbol, Array] id model identificator(s) 437 | # @return [Couchbase::Model, Array] an instance of the model, or an array of instances 438 | # @raise [Couchbase::Error::NotFound] when given key isn't exist 439 | # 440 | # @example Find model using +id+ 441 | # post = Post.find('the-id') 442 | # 443 | # @example Find multiple models using +id+ 444 | # post = Post.find('one', 'two') 445 | def self.find(*id) 446 | _find(false, *id) 447 | end 448 | 449 | # Find the model using +id+ attribute 450 | # 451 | # Unlike {Couchbase::Model.find}, this method won't raise 452 | # {Couchbase::Error::NotFound} error when key doesn't exist in the 453 | # bucket 454 | # 455 | # @since 0.1.0 456 | # 457 | # @param [String, Symbol] id model identificator(s) 458 | # @return [Couchbase::Model, Array, nil] an instance of the model, an array 459 | # of found instances of the model, or +nil+ if 460 | # given key isn't exist 461 | # 462 | # @example Find model using +id+ 463 | # post = Post.find_by_id('the-id') 464 | # @example Find multiple models using +id+ 465 | # posts = Post.find_by_id(['the-id', 'the-id2']) 466 | def self.find_by_id(*id) 467 | _find(true, *id) 468 | end 469 | 470 | # Create the model with given attributes 471 | # 472 | # @since 0.0.1 473 | # 474 | # @param [Hash] args attribute-value pairs for the object 475 | # @return [Couchbase::Model, false] an instance of the model 476 | def self.create(*args) 477 | new(*args).create 478 | end 479 | 480 | # Creates an object just like {{Model.create} but raises an exception if 481 | # the record is invalid. 482 | # 483 | # @since 0.5.1 484 | # @raise [Couchbase::Error::RecordInvalid] if the instance is invalid 485 | def self.create!(*args) 486 | new(*args).create! 487 | end 488 | 489 | # Constructor for all subclasses of Couchbase::Model 490 | # 491 | # @since 0.0.1 492 | # 493 | # Optionally takes a Hash of attribute value pairs. 494 | # 495 | # @param [Hash] attrs attribute-value pairs 496 | def initialize(attrs = {}) 497 | @errors = ::ActiveModel::Errors.new(self) if defined?(::ActiveModel) 498 | @_attributes = ::Hash.new do |h, k| 499 | default = self.class.attributes[k] 500 | h[k] = if default.respond_to?(:call) 501 | default.call 502 | else 503 | default 504 | end 505 | end 506 | case attrs 507 | when Hash 508 | if defined?(HashWithIndifferentAccess) && !attrs.is_a?(HashWithIndifferentAccess) 509 | if attrs.respond_to?(:with_indifferent_access) 510 | attrs = attrs.with_indifferent_access 511 | end 512 | end 513 | @id = attrs.delete(:id) 514 | @key = attrs.delete(:key) 515 | @value = attrs.delete(:value) 516 | @doc = attrs.delete(:doc) 517 | @meta = attrs.delete(:meta) 518 | @raw = attrs.delete(:raw) 519 | assign_attributes(@doc || attrs) 520 | @previously_changed = nil 521 | @changed_attributes.clear unless @changed_attributes.nil? 522 | else 523 | @raw = attrs 524 | end 525 | end 526 | 527 | # Create this model and assign new id if necessary 528 | # 529 | # @since 0.0.1 530 | # 531 | # @return [Couchbase::Model, false] newly created object 532 | # 533 | # @raise [Couchbase::Error::KeyExists] if model with the same +id+ 534 | # exists in the bucket 535 | # 536 | # @example Create the instance of the Post model 537 | # p = Post.new(:title => 'Hello world', :draft => true) 538 | # p.create 539 | def create(options = {}) 540 | @id ||= Couchbase::Model::UUID.generator.next(1, model.thread_storage[:uuid_algorithm]) 541 | return false if failed_validations?(options) 542 | 543 | options = model.defaults.merge(options) 544 | value = (options[:format] == :plain) ? @raw : attributes_with_values 545 | unless @meta 546 | @meta = {} 547 | if @meta.respond_to?(:with_indifferent_access) 548 | @meta = @meta.with_indifferent_access 549 | end 550 | end 551 | @meta['cas'] = model.bucket.add(@id, value, options) 552 | self 553 | end 554 | 555 | # Creates an object just like {{Model#create} but raises an exception if 556 | # the record is invalid. 557 | # 558 | # @since 0.5.1 559 | # 560 | # @raise [Couchbase::Error::RecordInvalid] if the instance is invalid 561 | def create!(options = {}) 562 | create(options) || raise(Couchbase::Error::RecordInvalid.new(self)) 563 | end 564 | 565 | # Create or update this object based on the state of #new?. 566 | # 567 | # @since 0.0.1 568 | # 569 | # @param [Hash] options options for operation, see 570 | # {{Couchbase::Bucket#set}} 571 | # 572 | # @return [Couchbase::Model, false] saved object or false if there 573 | # are validation errors 574 | # 575 | # @example Update the Post model 576 | # p = Post.find('hello-world') 577 | # p.draft = false 578 | # p.save 579 | # 580 | # @example Use CAS value for optimistic lock 581 | # p = Post.find('hello-world') 582 | # p.draft = false 583 | # p.save('cas' => p.meta['cas']) 584 | # 585 | def save(options = {}) 586 | return create(options) unless @meta 587 | return false if failed_validations?(options) 588 | 589 | options = model.defaults.merge(options) 590 | value = (options[:format] == :plain) ? @raw : attributes_with_values 591 | @meta['cas'] = model.bucket.replace(@id, value, options) 592 | @previously_changed = changes 593 | @changed_attributes.clear unless @changed_attributes.nil? 594 | self 595 | end 596 | 597 | # Creates an object just like {{Model#save} but raises an exception if 598 | # the record is invalid. 599 | # 600 | # @since 0.5.1 601 | # 602 | # @raise [Couchbase::Error::RecordInvalid] if the instance is invalid 603 | def save!(options = {}) 604 | save(options) || raise(Couchbase::Error::RecordInvalid.new(self)) 605 | end 606 | 607 | # Update this object, optionally accepting new attributes. 608 | # 609 | # @since 0.0.1 610 | # 611 | # @param [Hash] attrs Attribute value pairs to use for the updated 612 | # version 613 | # @param [Hash] options options for operation, see 614 | # {{Couchbase::Bucket#set}} 615 | # @return [Couchbase::Model] The updated object 616 | def update(attrs, options = {}) 617 | assign_attributes(attrs) 618 | save(options) 619 | end 620 | alias :update_attributes :update 621 | 622 | # Delete this object from the bucket 623 | # 624 | # @since 0.0.1 625 | # 626 | # @note This method will reset +id+ attribute 627 | # 628 | # @param [Hash] options options for operation, see 629 | # {{Couchbase::Bucket#delete}} 630 | # @return [Couchbase::Model] Returns a reference of itself. 631 | # 632 | # @example Delete the Post model 633 | # p = Post.find('hello-world') 634 | # p.delete 635 | def delete(options = {}) 636 | raise Couchbase::Error::MissingId, 'missing id attribute' unless @id 637 | model.bucket.delete(@id, options) 638 | @id = nil 639 | @meta = nil 640 | self 641 | end 642 | alias :destroy :delete 643 | 644 | # Check if the record have +id+ attribute 645 | # 646 | # @since 0.0.1 647 | # 648 | # @return [true, false] Whether or not this object has an id. 649 | # 650 | # @note +true+ doesn't mean that record exists in the database 651 | # 652 | # @see Couchbase::Model#exists? 653 | def new? 654 | !@id 655 | end 656 | 657 | # @return [true, false] Where on on this object persisted in the storage 658 | def persisted? 659 | !!@id 660 | end 661 | 662 | # Check if the key exists in the bucket 663 | # 664 | # @since 0.0.1 665 | # 666 | # @param [String, Symbol] id the record identifier 667 | # @return [true, false] Whether or not the object with given +id+ 668 | # presented in the bucket. 669 | def self.exists?(id) 670 | !!bucket.get(id, :quiet => true) 671 | end 672 | 673 | # Check if this model exists in the bucket. 674 | # 675 | # @since 0.0.1 676 | # 677 | # @return [true, false] Whether or not this object presented in the 678 | # bucket. 679 | def exists? 680 | model.exists?(@id) 681 | end 682 | 683 | # All defined attributes within a class. 684 | # 685 | # @since 0.0.1 686 | # 687 | # @see Model.attribute 688 | # 689 | # @return [Hash] 690 | def self.attributes 691 | @attributes ||= if self == Model 692 | @@attributes.dup 693 | else 694 | couchbase_ancestor.attributes.dup 695 | end 696 | end 697 | 698 | # All defined views within a class. 699 | # 700 | # @since 0.1.0 701 | # 702 | # @see Model.view 703 | # 704 | # @return [Array] 705 | def self.views 706 | @views ||= if self == Model 707 | @@views.dup 708 | else 709 | couchbase_ancestor.views.dup 710 | end 711 | end 712 | 713 | # Returns the first ancestor that is also a Couchbase::Model ancestor. 714 | # 715 | # @return Class 716 | def self.couchbase_ancestor 717 | ancestors[1..-1].each do |ancestor| 718 | return ancestor if ancestor.ancestors.include?(Couchbase::Model) 719 | end 720 | end 721 | 722 | # All the attributes of the current instance 723 | # 724 | # @since 0.0.1 725 | # 726 | # @return [Hash] 727 | def attributes 728 | @_attributes || {} 729 | end 730 | 731 | # Update all attributes without persisting the changes. 732 | # 733 | # @since 0.0.1 734 | # 735 | # @param [Hash] attrs attribute-value pairs. 736 | def assign_attributes(attrs) 737 | if id = attrs.delete(:id) 738 | @id = id 739 | end 740 | attrs.each do |key, value| 741 | setter = :"#{key}=" 742 | send(setter, value) if respond_to?(setter) 743 | end 744 | end 745 | 746 | # Reload all the model attributes from the bucket 747 | # 748 | # @since 0.0.1 749 | # 750 | # @return [Model] the latest model state 751 | # 752 | # @raise [Error::MissingId] for records without +id+ 753 | # attribute 754 | def reload 755 | raise Couchbase::Error::MissingId, 'missing id attribute' unless @id 756 | pristine = model.find(@id) 757 | assign_attributes(pristine.attributes) 758 | @meta[:cas] = pristine.meta[:cas] 759 | self 760 | end 761 | 762 | # Format the model for use in a JSON response 763 | # 764 | # @since 0.5.2 765 | # 766 | # @return [Hash] a JSON representation of the model for REST APIs 767 | # 768 | def as_json(options = {}) 769 | attributes.merge({:id => @id}).as_json(options) 770 | end 771 | 772 | # @private The thread local storage for model specific stuff 773 | # 774 | # @since 0.0.1 775 | def self.thread_storage 776 | Couchbase.thread_storage[self] ||= {:uuid_algorithm => :sequential} 777 | end 778 | 779 | # @private Fetch the current connection 780 | # 781 | # @since 0.0.1 782 | def self.bucket 783 | self.thread_storage[:bucket] ||= Couchbase.bucket 784 | end 785 | 786 | # @private Set the current connection 787 | # 788 | # @since 0.0.1 789 | # 790 | # @param [Bucket] connection the connection instance 791 | def self.bucket=(connection) 792 | self.thread_storage[:bucket] = connection 793 | end 794 | 795 | # @private Get model class 796 | # 797 | # @since 0.0.1 798 | def model 799 | self.class 800 | end 801 | 802 | # @private Wrap the hash to the model class. 803 | # 804 | # @since 0.0.1 805 | # 806 | # @param [Bucket] bucket the reference to Bucket instance 807 | # @param [Hash] data the Hash fetched by View, it should have at least 808 | # +"id"+, +"key"+ and +"value"+ keys, also it could have optional 809 | # +"doc"+ key. 810 | # 811 | # @return [Model] 812 | def self.wrap(bucket, data) 813 | doc = { 814 | :id => data['id'], 815 | :key => data['key'], 816 | :value => data['value'] 817 | } 818 | if data['doc'] 819 | doc[:meta] = data['doc']['meta'] 820 | doc[:doc] = data['doc']['value'] || data['doc']['json'] 821 | end 822 | new(doc) 823 | end 824 | 825 | # @private Returns a string containing a human-readable representation 826 | # of the record. 827 | # 828 | # @since 0.0.1 829 | def inspect 830 | attrs = [] 831 | attrs << ['key', @key.inspect] unless @key.nil? 832 | attrs << ['value', @value.inspect] unless @value.nil? 833 | model.attributes.map do |attr, default| 834 | val = read_attribute(attr) 835 | attrs << [attr.to_s, val.inspect] unless val.nil? 836 | end 837 | attrs.sort! 838 | attrs.unshift([:id, id]) unless new? 839 | sprintf('#<%s %s>', model, attrs.map { |a| a.join(': ') }.join(', ')) 840 | end 841 | 842 | def self.inspect 843 | buf = "#{name}" 844 | if self != Couchbase::Model 845 | buf << "(#{['id', attributes.map(&:first)].flatten.join(', ')})" 846 | end 847 | buf 848 | end 849 | 850 | # @private Returns a hash with model attributes 851 | # 852 | # @since 0.1.0 853 | def attributes_with_values 854 | ret = {:type => model.design_document} 855 | model.attributes.keys.each do |attr| 856 | ret[attr] = read_attribute(attr) 857 | end 858 | ret 859 | end 860 | 861 | private :attributes_with_values 862 | 863 | # @private Returns if validations can be and should be performed, 864 | # and they fail 865 | # 866 | # @since 0.5.5 867 | def failed_validations?(options) 868 | options[:validate] != false && respond_to?(:valid?) && !valid? 869 | end 870 | 871 | # Redefine (if exists) #to_key to use #key if #id is missing 872 | def to_key 873 | keys = [id || key] 874 | keys.empty? ? nil : keys 875 | end 876 | 877 | def to_param 878 | keys = to_key 879 | if keys && !keys.empty? 880 | keys.join('-') 881 | end 882 | end 883 | 884 | include Couchbase::ActiveModel 885 | ActiveSupport.run_load_hooks :couchbase_model, self 886 | end 887 | 888 | end 889 | 890 | -------------------------------------------------------------------------------- /lib/couchbase/model/configuration.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | module Couchbase 19 | 20 | class Model 21 | 22 | module Configuration 23 | extend self 24 | 25 | attr_accessor :design_documents_paths 26 | end 27 | 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/couchbase/model/ext/camelize.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | class String 19 | def camelize 20 | res = self.sub(/^[a-z\d]*/) { $&.capitalize } 21 | res.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }.gsub('/', '::') 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/couchbase/model/ext/constantize.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | class String 19 | def constantize 20 | names = self.split('::') 21 | names.shift if names.empty? || names.first.empty? 22 | 23 | constant = Object 24 | names.each do |name| 25 | constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name) 26 | end 27 | constant 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/couchbase/model/ext/singleton_class.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | class Object 19 | def singleton_class 20 | class << self 21 | self 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/couchbase/model/uuid.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'thread' 19 | 20 | module Couchbase 21 | 22 | class Model 23 | 24 | # Generator of CouchDB specfic UUIDs. This is the ruby implementation of 25 | # couch_uuids.erl from couchdb distribution. It is threadsafe. 26 | # 27 | # @since 0.0.1 28 | 29 | class UUID 30 | # Get default UUID generator. You can create your own if you like. 31 | # 32 | # @since 0.0.1 33 | # 34 | # @return [UUID] 35 | def self.generator 36 | @generator ||= UUID.new 37 | end 38 | 39 | # Initialize generator. 40 | # 41 | # @since 0.0.1 42 | # 43 | # @param [Fixnum] seed seed for pseudorandom number generator. 44 | def initialize(seed = nil) 45 | seed ? srand(seed) : srand 46 | @prefix, _ = rand_bytes(13).unpack('H26') 47 | @inc = rand(0xfff) + 1 48 | @lock = Mutex.new 49 | end 50 | 51 | # Generate list of UUIDs. 52 | # 53 | # @since 0.0.1 54 | # 55 | # @param [Fixnum] count number of UUIDs you need 56 | # 57 | # @param [Symbol] algorithm Algorithm to use. Known algorithms: 58 | # [:random] 59 | # 128 bits of random awesome. All awesome, all the time. 60 | # [:sequential] 61 | # Monotonically increasing ids with random increments. First 26 hex 62 | # characters are random. Last 6 increment in random amounts until an 63 | # overflow occurs. On overflow, the random prefix is regenerated and 64 | # the process starts over. 65 | # [:utc_random] 66 | # Time since Jan 1, 1970 UTC with microseconds. First 14 characters 67 | # are the time in hex. Last 18 are random. 68 | # 69 | # @return [String, Array] single string value or array of strings. Where 70 | # each value represents 128-bit number written in hexadecimal format. 71 | def next(count = 1, algorithm = :sequential) 72 | raise ArgumentError, 'count should be a positive number' unless count > 0 73 | uuids = case algorithm 74 | when :random 75 | rand_bytes(16 * count).unpack('H32' * count) 76 | when :utc_random 77 | now = Time.now.utc 78 | prefix = '%014x' % [now.to_i * 1_000_000 + now.usec] 79 | rand_bytes(9 * count).unpack('H18' * count).map do |tail| 80 | "#{prefix}#{tail}" 81 | end 82 | when :sequential 83 | (1..count).map{ next_seq } 84 | else 85 | raise ArgumentError, "Unknown algorithm #{algo}. Should be one :sequential, :random or :utc_random" 86 | end 87 | uuids.size == 1 ? uuids[0] : uuids 88 | end 89 | 90 | private 91 | 92 | def next_seq 93 | @lock.synchronize do 94 | if @inc >= 0xfff000 95 | @prefix, _ = rand_bytes(13).unpack('H26') 96 | @inc = rand(0xfff) + 1 97 | end 98 | @inc += rand(0xfff) + 1 99 | '%s%06x' % [@prefix, @inc] 100 | end 101 | end 102 | 103 | def rand_bytes(count) 104 | bytes = '' 105 | count.times { bytes << rand(256) } 106 | bytes 107 | end 108 | 109 | end 110 | 111 | end 112 | 113 | end 114 | -------------------------------------------------------------------------------- /lib/couchbase/model/version.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | module Couchbase 19 | class Model 20 | VERSION = '0.5.4' 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/couchbase/railtie.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Author:: Couchbase 4 | # Copyright:: 2012 Couchbase, Inc. 5 | # License:: Apache License, Version 2.0 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | require 'couchbase/model' 21 | 22 | module Rails #:nodoc: 23 | module Couchbase #:nodoc: 24 | class Railtie < Rails::Railtie #:nodoc: 25 | 26 | config.couchbase = ActiveSupport::OrderedOptions.new 27 | config.couchbase.ensure_design_documents ||= true 28 | 29 | # Determine which generator to use. app_generators was introduced after 30 | # 3.0.0. 31 | # 32 | # @since 0.1.0 33 | # 34 | # @example Get the generators method. 35 | # railtie.generators 36 | # 37 | # @return [Symbol] The method name to use. 38 | def self.generator 39 | config.respond_to?(:app_generators) ? :app_generators : :generators 40 | end 41 | 42 | # Maping of rescued exceptions to HTTP responses 43 | # 44 | # @since 0.1.0 45 | # 46 | # @example 47 | # railtie.rescue_responses 48 | # 49 | # @return [Hash] rescued responses 50 | def self.rescue_responses 51 | { 52 | 'Couchbase::Error::NotFound' => :not_found, 53 | 'Couchbase::Error::NotStored' => :unprocessable_entity, 54 | 'Couchbase::Error::RecordInvalid' => :unprocessable_entity 55 | } 56 | end 57 | 58 | config.send(generator).orm :couchbase, :migration => false 59 | 60 | if config.action_dispatch.rescue_responses 61 | config.action_dispatch.rescue_responses.merge!(rescue_responses) 62 | end 63 | 64 | # Initialize Couchbase Mode. This will look for a couchbase.yml in the 65 | # config directory and configure Couchbase connection appropriately. 66 | # 67 | # @example couchbase.yml 68 | # 69 | # common: &common 70 | # hostname: localhost 71 | # port: 8091 72 | # username: 73 | # password: 74 | # pool: default 75 | # 76 | # production: 77 | # <<: *common 78 | # bucket: example_production 79 | # 80 | # test: 81 | # <<: *common 82 | # bucket: example_test 83 | # 84 | # development: 85 | # <<: *common 86 | # bucket: example_development 87 | # 88 | initializer 'couchbase.setup_connection' do 89 | config_file = Rails.root.join('config', 'couchbase.yml') 90 | if config_file.file? && 91 | config = YAML.load(ERB.new(File.read(config_file)).result)[Rails.env] 92 | ::Couchbase.connection_options = config.with_indifferent_access 93 | end 94 | end 95 | 96 | # After initialization we will warn the user if we can't find a couchbase.yml and 97 | # alert to create one. 98 | initializer 'couchbase.warn_configuration_missing' do 99 | unless ARGV.include?('couchbase:config') 100 | config.after_initialize do 101 | unless Rails.root.join('config', 'couchbase.yml').file? 102 | puts "\nCouchbase config not found. Create a config file at: config/couchbase.yml" 103 | puts "to generate one run: rails generate couchbase:config\n\n" 104 | end 105 | end 106 | end 107 | end 108 | 109 | # Check (and upgrade if needed) all design documents 110 | initializer 'couchbase.upgrade_design_documents', :after => 'couchbase.setup_connection' do |app| 111 | ::Couchbase::Model::Configuration.design_documents_paths ||= app.config.paths['app/models'] 112 | if config.couchbase.ensure_design_documents 113 | config.to_prepare do 114 | app.config.paths['app/models'].each do |path| 115 | Dir.glob("#{path}/**/*.rb").sort.each do |file| 116 | require_dependency(file.gsub("#{path}/" , '').gsub('.rb', '')) 117 | end 118 | end 119 | begin 120 | ::Couchbase::Model.descendants.each do |model| 121 | model.ensure_design_document! 122 | end 123 | rescue ::Couchbase::Error::Timeout, ::Couchbase::Error::Connect 124 | # skip connection errors for now 125 | end 126 | end 127 | end 128 | end 129 | 130 | # Set the proper error types for Rails. NotFound errors should be 131 | # 404s and not 500s, validation errors are 422s. 132 | initializer 'couchbase.load_http_errors' do |app| 133 | config.after_initialize do 134 | unless config.action_dispatch.rescue_responses 135 | ActionDispatch::ShowExceptions.rescue_responses.update(Railtie.rescue_responses) 136 | end 137 | end 138 | end 139 | 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/rails/generators/couchbase/config/config_generator.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Author:: Couchbase 4 | # Copyright:: 2012 Couchbase, Inc. 5 | # License:: Apache License, Version 2.0 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | require 'rails/generators/couchbase_generator' 21 | 22 | module Couchbase 23 | module Generators 24 | class ConfigGenerator < Rails::Generators::Base 25 | desc 'Creates a Couchbase configuration file at config/couchbase.yml' 26 | 27 | argument :database_name, :type => :string, :optional => true 28 | 29 | def self.source_root 30 | @_couchbase_source_root ||= File.expand_path('../templates', __FILE__) 31 | end 32 | 33 | def app_name 34 | Rails::Application.subclasses.first.parent.to_s.underscore 35 | end 36 | 37 | def create_config_file 38 | template 'couchbase.yml', File.join('config', 'couchbase.yml') 39 | end 40 | 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/rails/generators/couchbase/config/templates/couchbase.yml: -------------------------------------------------------------------------------- 1 | common: &common 2 | hostname: localhost 3 | port: 8091 4 | username: 5 | password: 6 | pool: default 7 | 8 | development: 9 | <<: *common 10 | bucket: <%= database_name || app_name %>_development 11 | 12 | test: 13 | <<: *common 14 | bucket: <%= database_name || app_name %>_test 15 | 16 | # set these environment variables on your production server 17 | production: 18 | hostname: <%%= ENV['COUCHBASE_HOST'] %> 19 | port: <%%= ENV['COUCHBASE_PORT'] %> 20 | username: <%%= ENV['COUCHBASE_USERNAME'] %> 21 | password: <%%= ENV['COUCHBASE_PASSWORD'] %> 22 | pool: <%%= ENV['COUCHBASE_POOL'] %> 23 | bucket: <%%= ENV['COUCHBASE_BUCKET'] %> 24 | -------------------------------------------------------------------------------- /lib/rails/generators/couchbase/view/templates/map.js: -------------------------------------------------------------------------------- 1 | // The map function is the most critical part of any view as it provides the 2 | // logical mapping between the input fields of the individual objects stored 3 | // within Couchbase to the information output when the view is accessed. 4 | // 5 | // Read more about how to write map functions at: 6 | // http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-writing-map.html 7 | 8 | function(doc, meta) { 9 | emit(meta.id, null); 10 | } 11 | 12 | // You can also check out following examples 13 | // 14 | // The simplest example of a map function: 15 | // 16 | // function(doc, meta) { 17 | // emit(meta.id, doc); 18 | // } 19 | // 20 | // Slightly more complex example of a function that defines a view on values 21 | // computed from customer documents: 22 | // 23 | // function(doc, meta) { 24 | // if (doc.type == "customer") { 25 | // emit(meta.id, {last_name: doc.last_name, first_name: doc.first_name}); 26 | // } 27 | // } 28 | // 29 | // To be able to filter or sort the view by some document property, you 30 | // would use that property for the key. For example, the following view 31 | // would allow you to lookup customer documents by the last_name or 32 | // first_name fields (your keys could be compound, e.g. arrays): 33 | // 34 | // function(doc, meta) { 35 | // if (doc.type == "customer") { 36 | // emit(doc.last_name, {first_name: doc.first_name}); 37 | // emit(doc.first_name, {last_name: doc.last_name}); 38 | // } 39 | // } 40 | // 41 | -------------------------------------------------------------------------------- /lib/rails/generators/couchbase/view/templates/reduce.js: -------------------------------------------------------------------------------- 1 | // If a view has a reduce function, it is used to produce aggregate results 2 | // for that view. A reduce function is passed a set of intermediate values 3 | // and combines them to a single value. Reduce functions must accept, as 4 | // input, results emitted by its corresponding map function as well as 5 | // results returned by the reduce function itself. The latter case is 6 | // referred to as a rereduce. 7 | // 8 | // function (key, values, rereduce) { 9 | // return sum(values); 10 | // } 11 | // 12 | // Reduce functions must handle two cases: 13 | // 14 | // 1. When rereduce is false: 15 | // 16 | // reduce([ [key1,id1], [key2,id2], [key3,id3] ], [value1,value2,value3], false) 17 | // 18 | // * key will be an array whose elements are arrays of the form [key,id], 19 | // where key is a key emitted by the map function and id is that of the 20 | // document from which the key was generated. 21 | // * values will be an array of the values emitted for the respective 22 | // elements in keys 23 | // 24 | // 2. When rereduce is true: 25 | // 26 | // reduce(null, [intermediate1,intermediate2,intermediate3], true) 27 | // 28 | // * key will be null 29 | // * values will be an array of values returned by previous calls to the 30 | // reduce function 31 | // 32 | // Reduce functions should return a single value, suitable for both the 33 | // value field of the final view and as a member of the values array passed 34 | // to the reduce function. 35 | // 36 | // NOTE: If this file is empty, reduce part will be skipped in design document 37 | // 38 | // There is number of built-in functions, which could be used instead of 39 | // javascript implementation of reduce function. 40 | // 41 | // The _count function provides a simple count of the input rows from the 42 | // map function, using the keys and group level to provide to provide a 43 | // count of the correlated items. The values generated during the map() 44 | // stage are ignored. 45 | // 46 | // _count 47 | // 48 | // The built-in _sum function collates the output from the map function 49 | // call. The information can either be a single number or an array of numbers. 50 | // 51 | // _sum 52 | // 53 | // The _stats built-in produces statistical calculations for the input data. 54 | // Like the _sum call the source information should be a number. The 55 | // generated statistics include the sum, count, minimum (min), maximum (max) 56 | // and sum squared (sumsqr) of the input rows. 57 | // 58 | // _stats 59 | // 60 | // Read more about how to write reduce functions at: 61 | // http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views-writing-reduce.html 62 | -------------------------------------------------------------------------------- /lib/rails/generators/couchbase/view/view_generator.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Author:: Couchbase 4 | # Copyright:: 2012 Couchbase, Inc. 5 | # License:: Apache License, Version 2.0 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | require 'rails/generators/couchbase_generator' 21 | 22 | module Couchbase 23 | module Generators 24 | class ViewGenerator < Rails::Generators::Base 25 | desc 'Creates a Couchbase views skeletons for map/reduce functions' 26 | 27 | argument :model_name, :type => :string 28 | argument :view_name, :type => :string 29 | 30 | source_root File.expand_path('../templates', __FILE__) 31 | 32 | def app_name 33 | Rails::Application.subclasses.first.parent.to_s.underscore 34 | end 35 | 36 | def create_map_reduce_files 37 | template 'map.js', File.join('app', 'models', model_name, view_name, 'map.js') 38 | template 'reduce.js', File.join('app', 'models', model_name, view_name, 'reduce.js') 39 | end 40 | 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/rails/generators/couchbase_generator.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # 3 | # Author:: Couchbase 4 | # Copyright:: 2012 Couchbase, Inc. 5 | # License:: Apache License, Version 2.0 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | require 'rails/generators/named_base' 21 | require 'rails/generators/active_model' 22 | 23 | module Couchbase #:nodoc: 24 | module Generators #:nodoc: 25 | 26 | class Base < ::Rails::Generators::NamedBase #:nodoc: 27 | 28 | def self.source_root 29 | @_couchbase_source_root ||= 30 | File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__) 31 | end 32 | 33 | unless methods.include?(:module_namespacing) 34 | def module_namespacing(&block) 35 | yield if block 36 | end 37 | end 38 | 39 | end 40 | 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /tasks/package.rake: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'rubygems/package_task' 19 | 20 | def gemspec 21 | @clean_gemspec ||= eval(File.read(File.expand_path('../../couchbase-model.gemspec', __FILE__))) 22 | end 23 | 24 | Gem::PackageTask.new(gemspec) do |pkg| 25 | pkg.need_tar = true 26 | end 27 | 28 | -------------------------------------------------------------------------------- /tasks/test.rake: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'rake/testtask' 19 | require 'rake/clean' 20 | 21 | rule 'test/CouchbaseMock.jar' do |task| 22 | jar_path = "0.8-SNAPSHOT/CouchbaseMock-0.8-20140621.030439-1.jar" 23 | sh %{wget -q -O test/CouchbaseMock.jar http://files.couchbase.com/maven2/org/couchbase/mock/CouchbaseMock/#{jar_path}} 24 | end 25 | 26 | CLOBBER << 'test/CouchbaseMock.jar' 27 | 28 | Rake::TestTask.new do |test| 29 | test.libs << "test" << "." 30 | test.pattern = 'test/test_*.rb' 31 | test.options = '--verbose' 32 | end 33 | 34 | Rake::Task['test'].prerequisites.unshift('test/CouchbaseMock.jar') 35 | -------------------------------------------------------------------------------- /tasks/util.rake: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | desc 'Start an irb session and load the library.' 19 | task :console do 20 | exec "irb -I lib -rcouchbase-model" 21 | end 22 | -------------------------------------------------------------------------------- /test/setup.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require 'minitest/autorun' 19 | require 'couchbase' 20 | require 'couchbase/model' 21 | 22 | require 'socket' 23 | require 'open-uri' 24 | 25 | class CouchbaseServer 26 | attr_accessor :host, :port, :num_nodes, :buckets_spec 27 | 28 | def real? 29 | true 30 | end 31 | 32 | def initialize(params = {}) 33 | @host, @port = ENV['COUCHBASE_SERVER'].split(':') 34 | @port = @port.to_i 35 | 36 | if @host.nil? || @host.empty? || @port == 0 37 | raise ArgumentError, 'Check COUCHBASE_SERVER variable. It should be hostname:port' 38 | end 39 | 40 | @config = Yajl::Parser.parse(open("http://#{@host}:#{@port}/pools/default")) 41 | @num_nodes = @config['nodes'].size 42 | @buckets_spec = params[:buckets_spec] || 'default:' # "default:,protected:secret,cache::memcache" 43 | end 44 | 45 | def start 46 | # flush all buckets 47 | @buckets_spec.split(',') do |bucket| 48 | name, password, _ = bucket.split(':') 49 | connection = Couchbase.new(:hostname => @host, 50 | :port => @port, 51 | :username => name, 52 | :bucket => name, 53 | :password => password) 54 | connection.flush 55 | end 56 | end 57 | def stop; end 58 | end 59 | 60 | class CouchbaseMock 61 | Monitor = Struct.new(:pid, :client, :socket, :port) 62 | 63 | attr_accessor :host, :port, :buckets_spec, :num_nodes, :num_vbuckets 64 | 65 | def real? 66 | false 67 | end 68 | 69 | def initialize(params = {}) 70 | @host = '127.0.0.1' 71 | @port = 0 72 | @num_nodes = 10 73 | @num_vbuckets = 4096 74 | @buckets_spec = 'default:' # "default:,protected:secret,cache::memcache" 75 | params.each do |key, value| 76 | send("#{key}=", value) 77 | end 78 | yield self if block_given? 79 | if @num_vbuckets < 1 || (@num_vbuckets & (@num_vbuckets - 1) != 0) 80 | raise ArgumentError, 'Number of vbuckets should be a power of two and greater than zero' 81 | end 82 | end 83 | 84 | def start 85 | @monitor = Monitor.new 86 | @monitor.socket = TCPServer.new(nil, 0) 87 | @monitor.socket.listen(10) 88 | _, @monitor.port, _, _ = @monitor.socket.addr 89 | trap('CLD') do 90 | puts 'CouchbaseMock.jar died unexpectedly during startup' 91 | exit(1) 92 | end 93 | @monitor.pid = fork 94 | if @monitor.pid.nil? 95 | rc = exec(command_line("--harakiri-monitor=:#{@monitor.port}")) 96 | else 97 | trap('CLD', 'SIG_DFL') 98 | @monitor.client, _ = @monitor.socket.accept 99 | @port = @monitor.client.recv(100).to_i 100 | end 101 | end 102 | 103 | def stop 104 | @monitor.client.close 105 | @monitor.socket.close 106 | Process.kill('TERM', @monitor.pid) 107 | Process.wait(@monitor.pid) 108 | end 109 | 110 | def failover_node(index, bucket = 'default') 111 | @monitor.client.send("failover,#{index},#{bucket}", 0) 112 | end 113 | 114 | def respawn_node(index, bucket = 'default') 115 | @monitor.client.send("respawn,#{index},#{bucket}", 0) 116 | end 117 | 118 | protected 119 | 120 | def command_line(extra = nil) 121 | cmd = "java -jar #{File.dirname(__FILE__)}/CouchbaseMock.jar" 122 | cmd << " --host #{@host}" if @host 123 | cmd << " --port #{@port}" if @port 124 | cmd << " --nodes #{@num_nodes}" if @num_nodes 125 | cmd << " --vbuckets #{@num_vbuckets}" if @num_vbuckets 126 | cmd << " --buckets #{@buckets_spec}" if @buckets_spec 127 | cmd << " #{extra}" 128 | cmd 129 | end 130 | end 131 | 132 | class MiniTest::Unit::TestCase 133 | 134 | def start_mock(params = {}) 135 | mock = nil 136 | if ENV['COUCHBASE_SERVER'] 137 | mock = CouchbaseServer.new(params) 138 | if (params[:port] && mock.port != params[:port]) || 139 | (params[:host] && mock.host != params[:host]) || 140 | mock.buckets_spec != 'default:' 141 | skip("Unable to configure real cluster. Requested config is: #{params.inspect}") 142 | end 143 | else 144 | mock = CouchbaseMock.new(params) 145 | end 146 | mock.start 147 | mock 148 | end 149 | 150 | def stop_mock(mock) 151 | assert(mock) 152 | mock.stop 153 | end 154 | 155 | def with_mock(params = {}) 156 | mock = nil 157 | if block_given? 158 | mock = start_mock(params) 159 | yield mock 160 | end 161 | ensure 162 | stop_mock(mock) if mock 163 | end 164 | 165 | def uniq_id(*suffixes) 166 | test_id = [caller.first[/.*[` ](.*)'/, 1], suffixes].compact.join('_') 167 | @ids ||= {} 168 | @ids[test_id] ||= Time.now.to_f 169 | [test_id, @ids[test_id]].join('_') 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /test/test_active_model_integration.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2011, 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require File.join(File.dirname(__FILE__), 'setup') 19 | 20 | class ActiveUser < Couchbase::Model 21 | attribute :name 22 | attribute :email 23 | attribute :role 24 | attribute :created_at, :updated_at, :default => lambda { Time.now.utc } 25 | 26 | validates_presence_of :email 27 | validates :role, :presence => true, :inclusion => { :in => %w(admin editor) } 28 | 29 | before_create :upcase_name 30 | 31 | private 32 | 33 | def upcase_name 34 | self.name = self.name.upcase unless self.name.nil? 35 | end 36 | end 37 | 38 | class ActiveObj < Couchbase::Model 39 | end 40 | 41 | class TestActiveModelIntegration < MiniTest::Unit::TestCase 42 | 43 | include ActiveModel::Lint::Tests 44 | 45 | def setup 46 | @model = ActiveUser.new # used by ActiveModel::Lint::Tests 47 | @mock = start_mock 48 | bucket = Couchbase.connect(:hostname => @mock.host, :port => @mock.port) 49 | ActiveObj.bucket = ActiveUser.bucket = bucket 50 | end 51 | 52 | def teardown 53 | stop_mock(@mock) 54 | end 55 | 56 | def test_active_model_includes 57 | [ 58 | ActiveModel::Conversion, 59 | ActiveModel::Validations, 60 | ActiveModel::Validations::Callbacks, 61 | ActiveModel::Validations::HelperMethods 62 | ].each do |mod| 63 | assert ActiveUser.ancestors.include?(mod), "Model not including #{mod}" 64 | end 65 | end 66 | 67 | def test_callbacks 68 | [ 69 | :before_validation, :after_validation, 70 | :after_initialize, :before_create, :around_create, 71 | :after_create, :before_delete, :around_delete, 72 | :after_delete, :before_save, :around_save, :after_save, 73 | :before_update, :around_update, :after_update, 74 | :before_destroy, :around_destroy, :after_destroy 75 | ].each do |callback| 76 | assert ActiveObj.respond_to?(callback), "Model doesn't support callback: #{callback}" 77 | end 78 | end 79 | 80 | def test_active_model_validations 81 | no_role = ActiveUser.new(:email => 'joe@example.com', :role => nil) 82 | bad_role = ActiveUser.new(:email => 'joe@example.com', :role => 'bad') 83 | good_role = ActiveUser.new(:email => 'joe@example.com', :role => 'admin') 84 | 85 | refute no_role.valid? 86 | refute bad_role.valid? 87 | assert good_role.valid? 88 | end 89 | 90 | def test_active_model_validation_helpers 91 | valid = ActiveUser.new(:email => 'joe@example.com', :role => 'editor') 92 | invalid = ActiveUser.new(:name => 'Joe', :role => 'editor') 93 | 94 | assert valid.valid? 95 | refute invalid.valid? 96 | end 97 | 98 | def test_before_save_callback 99 | assert user = ActiveUser.create(:name => 'joe', :role => 'admin', :email => 'joe@example.com') 100 | assert_equal 'JOE', user.name 101 | end 102 | 103 | def test_model_name_exposes_singular_and_human_name 104 | assert_equal 'active_user', @model.class.model_name.singular 105 | assert_equal 'Active user', @model.class.model_name.human 106 | end 107 | 108 | def test_model_equality 109 | obj1 = ActiveObj.create 110 | obj2 = ActiveObj.find(obj1.id) 111 | 112 | assert_equal obj1, obj2 113 | end 114 | 115 | def test_to_key 116 | assert_equal ['the-id'], ActiveObj.new(:id => 'the-id').to_key 117 | assert_equal ['the-key'], ActiveObj.new(:key => 'the-key').to_key 118 | end 119 | 120 | def test_to_param 121 | assert_equal 'the-id', ActiveObj.new(:id => 'the-id').to_param 122 | assert_equal 'the-key', ActiveObj.new(:key => ['the', 'key']).to_param 123 | end 124 | 125 | def test_dirty_tracking_on_attribute 126 | tester = ActiveUser.create(:email => 'joe@example.com', :role => 'admin') 127 | tester.email = 'bob@example.com' 128 | assert tester.changed? 129 | assert_equal tester.changed, ["email"] 130 | end 131 | 132 | def test_dirty_tracking_reset_on_save 133 | tester = ActiveUser.create(:email => 'joe@example.com', :role => 'admin') 134 | tester.email = 'bob@example.com' 135 | assert tester.changed? 136 | tester.save 137 | refute tester.changed? 138 | end 139 | 140 | def test_dirty_tracking_previous_values 141 | tester = ActiveUser.create(:email => 'joe@example.com', :role => 'admin') 142 | tester.email = 'bob@example.com' 143 | tester.save 144 | assert_equal tester.previous_changes[:email], ['joe@example.com', 'bob@example.com'] 145 | end 146 | 147 | def test_save_without_validations 148 | tester = ActiveUser.new(:email => 'joe@example.com', :role => nil) 149 | assert tester.save(:validate => false), 'Validations not skipped' 150 | end 151 | 152 | def test_save_bang_without_validations 153 | tester = ActiveUser.new(:email => 'joe@example.com', :role => nil) 154 | begin 155 | tester.save!(:validate => false) 156 | rescue 157 | assert false, 'Validations not skipped' 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /test/test_model.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2011, 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require File.join(File.dirname(__FILE__), 'setup') 19 | 20 | class Post < Couchbase::Model 21 | attribute :title 22 | attribute :body 23 | attribute :author, :default => 'Anonymous' 24 | attribute :created_at, :default => lambda { Time.utc('2010-01-01') } 25 | end 26 | 27 | class ValidPost < Couchbase::Model 28 | attribute :title 29 | 30 | def valid? 31 | title && !title.empty? 32 | end 33 | end 34 | 35 | class Brewery < Couchbase::Model 36 | attribute :name 37 | end 38 | 39 | class Beer < Couchbase::Model 40 | attribute :name 41 | belongs_to :brewery 42 | end 43 | 44 | class Wine < Couchbase::Model 45 | attribute :name 46 | belongs_to :winery, :class_name => :Brewery 47 | end 48 | 49 | class Attachment < Couchbase::Model 50 | defaults :format => :plain 51 | end 52 | 53 | class Comments < Couchbase::Model 54 | include Enumerable 55 | attribute :comments, :default => [] 56 | end 57 | 58 | class User < Couchbase::Model 59 | design_document :people 60 | end 61 | 62 | class TestModel < MiniTest::Unit::TestCase 63 | 64 | def setup 65 | @mock = start_mock 66 | bucket = Couchbase.connect(:hostname => @mock.host, :port => @mock.port) 67 | [Post, ValidPost, Brewery, Beer, Attachment, Wine].each do |model| 68 | model.bucket = bucket 69 | end 70 | end 71 | 72 | def teardown 73 | stop_mock(@mock) 74 | end 75 | 76 | def test_design_document 77 | assert_equal 'people', User.design_document 78 | assert_equal 'new_people', User.design_document('new_people') 79 | assert_equal 'post', Post.design_document 80 | end 81 | 82 | def test_it_supports_value_property 83 | doc = { 84 | 'id' => 'x', 85 | 'key' => 'x', 86 | 'value' => 'x', 87 | 'doc' => { 88 | 'value' => {'title' => 'foo'} 89 | } 90 | } 91 | post = Post.wrap(Post.bucket, doc) 92 | assert_equal 'foo', post.title 93 | end 94 | 95 | def test_it_supports_json_property 96 | doc = { 97 | 'id' => 'x', 98 | 'key' => 'x', 99 | 'value' => 'x', 100 | 'doc' => { 101 | 'json' => {'title' => 'foo'} 102 | } 103 | } 104 | post = Post.wrap(Post.bucket, doc) 105 | assert_equal 'foo', post.title 106 | end 107 | 108 | def test_access_attribute_by_key 109 | post = Post.new(:title => 'Hello, world') 110 | assert_equal 'Hello, world', post[:title] 111 | end 112 | 113 | def test_update_attribute_by_key 114 | post = Post.new(:title => 'Hello, world') 115 | post[:title] = 'world, Hello' 116 | assert_equal 'world, Hello', post.title 117 | end 118 | 119 | def test_assigns_attributes_from_the_hash 120 | post = Post.new(:title => 'Hello, world') 121 | assert_equal 'Hello, world', post.title 122 | refute post.body 123 | refute post.id 124 | end 125 | 126 | def test_uses_default_value_or_nil 127 | post = Post.new(:title => 'Hello, world') 128 | refute post.body 129 | assert_equal 'Anonymous', post.author 130 | assert_equal 'Anonymous', post.attributes[:author] 131 | end 132 | 133 | def test_allows_lambda_as_default_value 134 | post = Post.new(:title => 'Hello, world') 135 | expected = Time.utc('2010-01-01') 136 | assert_equal expected, post.created_at 137 | assert_equal expected, post.attributes[:created_at] 138 | end 139 | 140 | def test_assings_id_and_saves_the_object 141 | post = Post.create(:title => 'Hello, world') 142 | assert post.id 143 | end 144 | 145 | def test_updates_attributes 146 | post = Post.create(:title => 'Hello, world') 147 | post.update(:body => 'This is my first example') 148 | assert_equal 'This is my first example', post.body 149 | end 150 | 151 | def test_update_attributes_saves_record 152 | post = Post.new 153 | 154 | assert !post.persisted?, 'Post already persisted' 155 | post.update_attributes(:title => 'Hello, world', :body => "How's it going?") 156 | assert post.persisted?, 'Post not persisted' 157 | 158 | assert_equal "How's it going?", post.body 159 | assert_equal "Hello, world", post.title 160 | end 161 | 162 | def test_refreshes_the_attributes_with_reload_method 163 | orig = Post.create(:title => 'Hello, world') 164 | double = Post.find(orig.id) 165 | double.update(:title => 'Good bye, world') 166 | orig.reload 167 | assert_equal 'Good bye, world', orig.title 168 | end 169 | 170 | def test_reloads_cas_value_with_reload_method 171 | orig = Post.create(:title => "Hello, world") 172 | double = Post.find(orig.id) 173 | orig.update(:title => "Good bye, world") 174 | double.reload 175 | 176 | assert_equal orig.meta[:cas], double.meta[:cas] 177 | end 178 | 179 | def test_it_raises_not_found_exception 180 | assert_raises Couchbase::Error::NotFound do 181 | Post.find('missing_key') 182 | end 183 | end 184 | 185 | def test_it_raises_not_found_exception_if_id_is_nil 186 | assert_raises Couchbase::Error::NotFound do 187 | Post.find(nil) 188 | end 189 | end 190 | 191 | def test_it_returns_nil_when_key_not_found 192 | refute Post.find_by_id('missing_key') 193 | end 194 | 195 | def test_doesnt_raise_if_the_attribute_redefined 196 | eval <<-EOC 197 | class RefinedPost < Couchbase::Model 198 | attribute :title 199 | attribute :title 200 | end 201 | EOC 202 | end 203 | 204 | def test_allows_to_define_several_attributes_at_once 205 | eval <<-EOC 206 | class Comment < Couchbase::Model 207 | attribute :name, :email, :body 208 | end 209 | EOC 210 | 211 | comment = Comment.new 212 | assert_respond_to comment, :name 213 | assert_respond_to comment, :email 214 | assert_respond_to comment, :body 215 | end 216 | 217 | def test_allows_arbitrary_ids 218 | Post.create(:id => uniq_id, :title => 'Foo') 219 | assert_equal 'Foo', Post.find(uniq_id).title 220 | end 221 | 222 | def test_returns_an_instance_of_post 223 | Post.bucket.set(uniq_id, {:title => 'foo'}) 224 | assert Post.find(uniq_id).kind_of?(Post) 225 | assert_equal uniq_id, Post.find(uniq_id).id 226 | assert_equal 'foo', Post.find(uniq_id).title 227 | end 228 | 229 | def test_changes_its_attributes 230 | post = Post.create(:title => 'Hello, world') 231 | post.title = 'Good bye, world' 232 | post.save.reload 233 | assert_equal 'Good bye, world', post.title 234 | end 235 | 236 | def test_assings_a_new_id_to_each_record 237 | post1 = Post.create 238 | post2 = Post.create 239 | 240 | refute post1.new? 241 | refute post2.new? 242 | refute_equal post1.id, post2.id 243 | end 244 | 245 | def test_deletes_an_existent_model 246 | post = Post.create(:id => uniq_id) 247 | assert post.delete 248 | assert_raises Couchbase::Error::NotFound do 249 | Post.bucket.get(uniq_id) 250 | end 251 | end 252 | 253 | def test_destroy_an_existing_model 254 | post = Post.create(:id => uniq_id) 255 | assert post.destroy 256 | assert_raises Couchbase::Error::NotFound do 257 | Post.bucket.get(uniq_id) 258 | end 259 | end 260 | 261 | def test_belongs_to_with_class_name_assoc 262 | brewery = Brewery.create(:name => "R Wines") 263 | assert_includes Wine.attributes.keys, :winery_id 264 | wine = Wine.create(:name => "Classy", :winery_id => brewery.id) 265 | assert_respond_to wine, :winery 266 | assoc = wine.winery 267 | assert_instance_of Brewery, assoc 268 | assert_equal "R Wines", assoc.name 269 | end 270 | 271 | def test_fails_to_delete_model_without_id 272 | post = Post.new(:title => 'Hello') 273 | refute post.id 274 | assert_raises Couchbase::Error::MissingId do 275 | post.delete 276 | end 277 | end 278 | 279 | def test_belongs_to_assoc 280 | brewery = Brewery.create(:name => 'Anheuser-Busch') 281 | assert_includes Beer.attributes.keys, :brewery_id 282 | beer = Beer.create(:name => 'Budweiser', :brewery_id => brewery.id) 283 | assert_respond_to beer, :brewery 284 | assoc = beer.brewery 285 | assert_instance_of Brewery, assoc 286 | assert_equal 'Anheuser-Busch', assoc.name 287 | end 288 | 289 | def test_belongs_to_assoc_assign 290 | brewery = Brewery.create(:name => 'Anheuser-Busch') 291 | beer = Beer.create(:name => 'Budweiser') 292 | beer.brewery = brewery 293 | 294 | assert_equal brewery.id, beer.brewery_id 295 | assert_equal brewery, beer.brewery 296 | 297 | beer.brewery = nil 298 | assert_nil beer.brewery 299 | assert_nil beer.brewery_id 300 | end 301 | 302 | def test_to_key 303 | assert_equal ['the-id'], Post.new(:id => 'the-id').to_key 304 | assert_equal ['the-key'], Post.new(:key => 'the-key').to_key 305 | end 306 | 307 | def test_to_param 308 | assert_equal 'the-id', Post.new(:id => 'the-id').to_param 309 | assert_equal 'the-key', Post.new(:key => ['the', 'key']).to_param 310 | end 311 | 312 | def test_as_json 313 | require 'active_support/json/encoding' 314 | 315 | response = {'id' => 'the-id'} 316 | assert_equal response, Post.new(:id => 'the-id').as_json 317 | 318 | response = {} 319 | assert_equal response, Post.new(:id => 'the-id').as_json(:except => :id) 320 | end 321 | 322 | def test_validation 323 | post = ValidPost.create(:title => 'Hello, World!') 324 | assert post.valid?, 'post with title should be valid' 325 | post.title = nil 326 | refute post.save 327 | assert_raises(Couchbase::Error::RecordInvalid) do 328 | post.save! 329 | end 330 | refute ValidPost.create(:title => nil) 331 | assert_raises(Couchbase::Error::RecordInvalid) do 332 | ValidPost.create!(:title => nil) 333 | end 334 | end 335 | 336 | def test_blob_documents 337 | contents = File.read(__FILE__) 338 | id = Attachment.create(:raw => contents).id 339 | blob = Attachment.find(id) 340 | assert_equal contents, blob.raw 341 | end 342 | 343 | def test_couchbase_ancestor 344 | assert_equal Couchbase::Model, Comments.couchbase_ancestor 345 | end 346 | 347 | def test_returns_multiple_instances_of_post 348 | Post.create(:id => uniq_id('first'), :title => 'foo') 349 | Post.create(:id => uniq_id('second'), :title => 'bar') 350 | 351 | results = Post.find([uniq_id('first'), uniq_id('second')]) 352 | assert results.kind_of?(Array) 353 | assert results.size == 2 354 | assert results.detect { |post| post.id == uniq_id('first') }.title == 'foo' 355 | assert results.detect { |post| post.id == uniq_id('second') }.title == 'bar' 356 | end 357 | 358 | def test_returns_array_for_array_of_ids 359 | Post.create(:id => uniq_id('first'), :title => 'foo') 360 | 361 | results = Post.find([uniq_id('first')]) 362 | assert results.kind_of?(Array) 363 | assert results.size == 1 364 | assert results[0].title == 'foo' 365 | end 366 | 367 | def test_returns_array_for_array_of_ids_using_find_by_id 368 | Post.create(:id => uniq_id('first'), :title => 'foo') 369 | 370 | results = Post.find_by_id([uniq_id('first')]) 371 | assert results.kind_of?(Array) 372 | assert results.size == 1 373 | assert results[0].title == 'foo' 374 | end 375 | end 376 | -------------------------------------------------------------------------------- /test/test_model_rails_integration.rb: -------------------------------------------------------------------------------- 1 | # Author:: Couchbase 2 | # Copyright:: 2011, 2012 Couchbase, Inc. 3 | # License:: Apache License, Version 2.0 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | require File.join(File.dirname(__FILE__), 'setup') 19 | 20 | class Program < Couchbase::Model 21 | attribute :title 22 | attribute :genres, :default => [] 23 | end 24 | 25 | class Movie < Program 26 | attribute :mpaa_rating 27 | attribute :runtime 28 | end 29 | 30 | class Series < Program 31 | attribute :vchip_rating 32 | end 33 | 34 | class Episode < Series 35 | attribute :runtime 36 | end 37 | 38 | class TestModelRailsIntegration < MiniTest::Unit::TestCase 39 | 40 | def test_class_attributes_are_inheritable 41 | program_attributes = [:title, :genres] 42 | movie_attributes = [:mpaa_rating, :runtime] 43 | series_attributes = [:vchip_rating] 44 | episode_attributes = [:runtime] 45 | 46 | assert_equal program_attributes, Program.attributes.keys 47 | assert_equal program_attributes + movie_attributes, Movie.attributes.keys 48 | assert_equal program_attributes + series_attributes, Series.attributes.keys 49 | assert_equal program_attributes + series_attributes + episode_attributes, Episode.attributes.keys 50 | end 51 | 52 | def test_default_attributes_are_inheritable 53 | assert_equal nil, Movie.attributes[:title] 54 | assert_equal [], Movie.attributes[:genres] 55 | end 56 | 57 | def test_instance_attributes_are_inheritable 58 | episode = Episode.new(:title => 'Family Guy', :genres => ['Comedy'], :vchip_rating => 'TVPG', :runtime => 30) 59 | 60 | assert_equal [:title, :genres, :vchip_rating, :runtime], episode.attributes.keys 61 | assert_equal 'Family Guy', episode.title 62 | assert_equal ['Comedy'], episode.genres 63 | assert_equal 30, episode.runtime 64 | assert_equal 'TVPG', episode.vchip_rating 65 | end 66 | 67 | def test_class_attributes_from_subclasses_do_not_propogate_up_ancestor_chain 68 | assert_equal [:title, :genres, :vchip_rating], Series.attributes.keys 69 | end 70 | 71 | def test_instance_attributes_from_subclasses_do_not_propogate_up_ancestor_chain 72 | series = Series.new(:title => 'Family Guy', :genres => ['Comedy'], :vchip_rating => 'TVPG') 73 | assert_equal [:title, :genres, :vchip_rating], series.attributes.keys 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /test/test_uuid.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'setup') 2 | 3 | class TestUUID < MiniTest::Unit::TestCase 4 | 5 | def test_it_can_generate_10k_unique_ids 6 | random = Couchbase::Model::UUID.new.next(10_000, :random) 7 | assert_equal 10_000, random.uniq.size 8 | 9 | utc_random = Couchbase::Model::UUID.new.next(10_000, :utc_random) 10 | assert_equal 10_000, utc_random.uniq.size 11 | 12 | sequential = Couchbase::Model::UUID.new.next(10_000, :sequential) 13 | assert_equal 10_000, sequential.uniq.size 14 | end 15 | 16 | def test_it_produces_monotonically_increasing_ids 17 | utc_random = Couchbase::Model::UUID.new 18 | assert utc_random.next(1, :utc_random) < utc_random.next(1, :utc_random) 19 | 20 | sequential = Couchbase::Model::UUID.new 21 | assert sequential.next(1, :sequential) < sequential.next(1, :sequential) 22 | end 23 | 24 | def test_it_roll_over 25 | generator = Couchbase::Model::UUID.new 26 | prefix = generator.next[0, 26] 27 | n = 0 28 | n += 1 while prefix == generator.next[0, 26] 29 | assert(n >= 5000 && n <= 11000) 30 | end 31 | 32 | end 33 | --------------------------------------------------------------------------------