├── .gitignore ├── VERSION.yml ├── test ├── fixtures │ └── designs │ │ └── foos.js ├── exegesis_test.rb ├── test_helper.rb ├── design_syncronization_test.rb ├── design_test.rb └── document_test.rb ├── lib ├── exegesis.rb └── exegesis │ ├── design │ └── syncronization.rb │ ├── design.rb │ └── document.rb ├── Rakefile ├── README.rdoc └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 0 3 | :minor: 0 4 | :patch: 5 5 | -------------------------------------------------------------------------------- /test/fixtures/designs/foos.js: -------------------------------------------------------------------------------- 1 | { 2 | views: { 3 | by_bar: { 4 | map: function(doc) { 5 | emit(doc.bar, null) 6 | }, 7 | reduce: function(keys, values, rereduce) { 8 | return(sum(values)) 9 | } 10 | }, 11 | } 12 | } -------------------------------------------------------------------------------- /test/exegesis_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper.rb') 2 | 3 | class ExegesisTest < Test::Unit::TestCase 4 | 5 | context "designs directory" do 6 | context "setting custom" do 7 | before do 8 | @custom_design_dir = File.join(File.dirname(__FILE__), 'fixtures', 'designs') 9 | Exegesis.designs_directory = @custom_design_dir 10 | end 11 | 12 | expect { Exegesis.designs_directory.to_s.will == @custom_design_dir } 13 | end 14 | 15 | 16 | end 17 | 18 | context "database template" do 19 | before do 20 | @template_string = "http://localhost:5984/appname-%s" 21 | @account = "foo" 22 | Exegesis.database_template = @template_string 23 | end 24 | 25 | expect { Exegesis.database_for(@account).will == @template_string % @account } 26 | end 27 | 28 | end -------------------------------------------------------------------------------- /lib/exegesis.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'pathname' 3 | 4 | require 'couchrest' 5 | require 'active_support/inflector' 6 | 7 | $:.unshift File.dirname(__FILE__) 8 | require 'exegesis/document' 9 | require 'exegesis/design' 10 | 11 | module Exegesis 12 | 13 | def self.designs_directory= dir 14 | @designs_directory = Pathname.new(dir) 15 | end 16 | 17 | def self.designs_directory 18 | @designs_directory ||= Pathname.new(ENV["PWD"]) 19 | @designs_directory 20 | end 21 | 22 | def self.design_file name 23 | File.read(designs_directory + name) 24 | end 25 | 26 | def self.database_template= template 27 | @db_template = template 28 | end 29 | 30 | def self.database_template 31 | @db_template ||= "http://localhost:5984/%s" 32 | end 33 | 34 | def self.database_for name 35 | database_template % name 36 | end 37 | 38 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | begin 6 | require 'jeweler' 7 | Jeweler::Tasks.new do |s| 8 | s.name = "exegesis" 9 | s.summary = "TODO" 10 | s.email = "matt@flowerpowered.com" 11 | s.homepage = "http://github.com/mattly/exegesis" 12 | s.description = "TODO" 13 | s.authors = ["Matt Lyon"] 14 | end 15 | rescue LoadError 16 | puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 17 | end 18 | 19 | Rake::TestTask.new do |t| 20 | t.libs << 'lib' 21 | t.pattern = 'test/**/*_test.rb' 22 | t.verbose = false 23 | end 24 | 25 | Rake::RDocTask.new do |rdoc| 26 | rdoc.rdoc_dir = 'rdoc' 27 | rdoc.title = 'test-gem' 28 | rdoc.options << '--line-numbers' << '--inline-source' 29 | rdoc.rdoc_files.include('README*') 30 | rdoc.rdoc_files.include('lib/**/*.rb') 31 | end 32 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | 4 | require 'rr' 5 | require 'context' 6 | require 'matchy' 7 | require 'zebra' 8 | 9 | require 'ruby-debug' 10 | Debugger.start 11 | 12 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 13 | require 'lib/exegesis' 14 | 15 | class Test::Unit::TestCase 16 | include RR::Adapters::TestUnit 17 | 18 | def fixtures_path fixtures 19 | File.join(File.dirname(__FILE__), 'fixtures', fixtures) 20 | end 21 | 22 | # todo: extract to some helper methods to include ala RR, etc 23 | def reset_db(name=nil) 24 | @db = CouchRest.database db(name) rescue nil 25 | @db.delete! rescue nil 26 | @db = CouchRest.database! db(name) 27 | end 28 | 29 | def teardown_db 30 | @db.delete! rescue nil 31 | end 32 | 33 | def db(name) 34 | "http://localhost:5984/exegesis-test#{name.nil? ? '' : "-#{name}"}" 35 | end 36 | 37 | # def with_couch(%blk) 38 | # test_db_name = method_name.downcase.gsub(/[^-$\w]/,'$$') 39 | # 40 | # end 41 | end -------------------------------------------------------------------------------- /lib/exegesis/design/syncronization.rb: -------------------------------------------------------------------------------- 1 | require 'johnson' 2 | 3 | module Exegesis 4 | class Design 5 | module Syncronization 6 | 7 | def self.included(base) 8 | base.extend ClassMethods 9 | end 10 | 11 | module ClassMethods 12 | 13 | def compose_design 14 | js_doc = Johnson.evaluate("v = #{Exegesis.design_file(design_doc + '.js')}"); 15 | views = js_doc['views'].entries.inject({}) do |memo, (name, mapreduce)| 16 | memo[name] = mapreduce.entries.inject({}) do |view, (role, func)| 17 | view.update role => func.toString 18 | end 19 | memo 20 | end 21 | { '_id' => "_design/#{design_doc}", 22 | 'language' => 'javascript', 23 | 'views' => views 24 | } 25 | end 26 | 27 | def push_design!(db) 28 | doc = CouchRest::Document.new 29 | doc.database = db 30 | doc.update compose_design 31 | doc.save 32 | end 33 | 34 | end 35 | 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = exegesis 2 | 3 | * http://github.com/mattly/exegesis 4 | 5 | == DESCRIPTION: 6 | 7 | An ODM (Object/Document Mapper) for Couchdb. Still very much a work in progress 8 | 9 | == FEATURES/PROBLEMS: 10 | 11 | * Encourages Per-"Account" databases. This is both a feature and a problem, since classes cannot know which database to pull from you cannot do f.e. "Article.find('foo')" as Article doesn't know what database to use. 12 | * Does not yet provide it's own validators; hooks to use either ActiveRecord::Validations or the Validatable gem are planned. 13 | * Does not yet provide it's own callbacks; hooks to load either ActiveSupport::Callbacks or Extlib's callbacks are planned. 14 | 15 | == REQUIREMENTS: 16 | 17 | * Johnson (and Spidermonkey, if you have CouchDB you have Spidermonkey) 18 | * ActiveSupport (for now, and only the inflector is loaded) 19 | 20 | For running the tests: 21 | 22 | * Test::Unit (you got it) 23 | * Context (http://github.com/jeremymcanally/context) 24 | * Matchy (http://github.com/jeremymcanally/matchy) 25 | * Zebra (http://github.com/giraffesoft/zerba) 26 | * RR (http://github.com/btakita/rr) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2009 Matthew Lyon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/design_syncronization_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper.rb') 2 | 3 | class Foos < Exegesis::Design; end 4 | 5 | class ExegesisDesignSyncronizationTest < Test::Unit::TestCase 6 | 7 | context "syncronizing with server" do 8 | before do 9 | reset_db "design_doc_sync" 10 | Foos.push_design!(@db) 11 | @get_design = lambda { @db.get('_design/foos') } 12 | @design = @get_design.call rescue nil 13 | end 14 | 15 | expect { @get_design.wont raise_error } 16 | expect { @design.has_key?('_rev').will be(true) } 17 | expect { @design.has_key?('language').will be(true) } 18 | expect { @design['language'].will == 'javascript' } 19 | expect { @design.has_key?('views').will be(true) } 20 | expect { @design['views'].has_key?('by_bar').will be(true) } 21 | end 22 | 23 | context "composing design docs from local sources" do 24 | before do 25 | Exegesis.designs_directory = fixtures_path('designs') 26 | @design = Foos.compose_design 27 | @file = File.read(fixtures_path('designs/foos.js')) 28 | @jsdoc = Johnson.evaluate("v=#{@file}") 29 | end 30 | 31 | expect { @design.has_key?('_id').will be(true) } 32 | expect { @design['_id'].will == '_design/foos' } 33 | 34 | expect { @design.has_key?('views').will be(true) } 35 | expect { @design['views'].has_key?('by_bar').will be(true) } 36 | expect { @design['views']['by_bar'].has_key?('map').will be(true) } 37 | expect { @design['views']['by_bar']['map'].should == @jsdoc['views']['by_bar']['map'].toString } 38 | end 39 | end -------------------------------------------------------------------------------- /lib/exegesis/design.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) 2 | require 'design/syncronization' 3 | 4 | module Exegesis 5 | class Design 6 | 7 | include Exegesis::Design::Syncronization 8 | 9 | attr_accessor :database 10 | 11 | def initialize(db) 12 | @database = db 13 | end 14 | 15 | def self.design_doc 16 | ActiveSupport::Inflector.pluralize(name.to_s.sub(/(Design)$/,'').downcase) 17 | end 18 | 19 | def design_doc 20 | self.class.design_doc 21 | end 22 | 23 | def get(id) 24 | Exegesis::Document.instantiate database.get(id) 25 | end 26 | 27 | def parse_opts(opts={}) 28 | if opts[:key] 29 | case opts[:key] 30 | when Range 31 | range = opts.delete(:key) 32 | opts.update({:startkey => range.first, :endkey => range.last}) 33 | when Array 34 | if opts[:key].any?{|v| v.kind_of?(Range) } 35 | key = opts.delete(:key) 36 | opts[:startkey] = key.map {|v| v.kind_of?(Range) ? v.first : v } 37 | opts[:endkey] = key.map {|v| v.kind_of?(Range) ? v.last : v } 38 | end 39 | end 40 | end 41 | 42 | opts 43 | end 44 | 45 | def view view_name, opts={} 46 | opts = parse_opts opts 47 | database.view "#{design_doc}/#{view_name}", opts 48 | end 49 | 50 | def docs_for view_name, opts={} 51 | response = view view_name, opts.update({:include_docs => true}) 52 | response['rows'].map {|doc| Exegesis::Document.instantiate doc['doc'] } 53 | end 54 | 55 | def values_for view_name, opts={} 56 | response = view view_name, opts 57 | response['rows'].map {|row| row['value'] } 58 | end 59 | 60 | def keys_for view_name, opts={} 61 | response = view view_name, opts 62 | response['rows'].map {|row| row['key'] } 63 | end 64 | 65 | def ids_for view_name, opts={} 66 | response = view view_name, opts 67 | response['rows'].map {|row| row['id'] } 68 | end 69 | 70 | end 71 | end -------------------------------------------------------------------------------- /lib/exegesis/document.rb: -------------------------------------------------------------------------------- 1 | module Exegesis 2 | class Document < CouchRest::Document 3 | 4 | def self.instantiate hash={} 5 | ActiveSupport::Inflector.constantize(hash['.kind'] || 'Exegesis::Document').new(hash) 6 | end 7 | 8 | def self.cast field, opts={} 9 | unless opts.kind_of?(Hash) 10 | raise ArgumentError 11 | end 12 | casts 13 | opts[:with] = :parse if opts[:as] == 'Time' 14 | opts[:with] ||= :new 15 | @casts[field.to_s] = opts 16 | end 17 | 18 | def self.casts 19 | @casts ||= superclass.respond_to?(:casts) ? superclass.casts : {} 20 | end 21 | 22 | def self.default hash=nil 23 | if hash 24 | @default = hash 25 | else 26 | @default ||= superclass.respond_to?(:default) ? superclass.default : {} 27 | end 28 | end 29 | 30 | def self.expose *attrs 31 | show attrs 32 | [attrs].flatten.each do |attrib| 33 | define_method("#{attrib}=") {|val| self["#{attrib}"] = val } 34 | end 35 | end 36 | 37 | def self.show *attrs 38 | [attrs].flatten.each do |attrib| 39 | define_method(attrib) { self["#{attrib}"] } 40 | end 41 | end 42 | 43 | def self.timestamps! 44 | define_method :set_timestamps do 45 | self['updated_at'] = Time.now 46 | self['created_at'] ||= Time.now 47 | end 48 | cast 'updated_at', :as => 'Time' 49 | cast 'created_at', :as => 'Time' 50 | end 51 | 52 | def self.unique_id meth 53 | define_method :set_unique_id do 54 | self['_id'] = self.send(meth) 55 | end 56 | end 57 | 58 | alias_method :document_save, :save 59 | 60 | def save 61 | set_timestamps if respond_to?(:set_timestamps) 62 | if respond_to?(:set_unique_id) && id.nil? 63 | @unique_id_attempt = 0 64 | begin 65 | self['_id'] = set_unique_id 66 | document_save 67 | rescue RestClient::RequestFailed => e 68 | @unique_id_attempt += 1 69 | retry 70 | end 71 | else 72 | document_save 73 | end 74 | end 75 | 76 | def initialize keys={} 77 | apply_default 78 | super keys 79 | cast_keys 80 | self['.kind'] ||= self.class.to_s 81 | end 82 | 83 | def to_param 84 | self['_id'] 85 | end 86 | 87 | private 88 | 89 | def apply_default 90 | self.class.default.each do |key, value| 91 | self[key] = value 92 | end 93 | end 94 | 95 | def cast_keys 96 | return unless self.class.casts 97 | self.class.casts.each do |key, pattern| 98 | next unless self[key] 99 | self[key] = if self[key].is_a?(Array) 100 | self[key].map {|val| class_for(pattern[:as], val['.kind']).send(pattern[:with], val) } 101 | else 102 | class_for(pattern[:as], self[key]['.kind']).send pattern[:with], self[key] 103 | end 104 | end 105 | end 106 | 107 | def class_for(as, kind) 108 | ActiveSupport::Inflector.constantize(as || kind || 'Exegesis::Document') 109 | end 110 | 111 | end 112 | end -------------------------------------------------------------------------------- /test/design_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper.rb') 2 | 3 | class Foos < Exegesis::Design; end 4 | class FooDesign < Exegesis::Design; end 5 | 6 | class TestForDesign < CouchRest::Document; end 7 | 8 | class ExegesisDesignTest < Test::Unit::TestCase 9 | 10 | before do 11 | @db = Object.new 12 | @doc = Foos.new(@db) 13 | end 14 | 15 | expect { @doc.database.will == @db } 16 | expect { @doc.design_doc.will == "foos" } 17 | expect { FooDesign.new(@db).design_doc.will == "foos" } 18 | 19 | context "retrieving documents with #get" do 20 | before do 21 | reset_db('design-views') 22 | @doc = Foos.new(@db) 23 | @db.save '_id' => 'foo', 'foo' => 'bar', '.kind' => 'TestForDesign' 24 | @obj = @doc.get('foo') 25 | end 26 | 27 | expect { @obj.will be_kind_of(TestForDesign) } 28 | expect { @obj['foo'].will == 'bar' } 29 | end 30 | 31 | context "retreiving views" do 32 | before do 33 | reset_db('design-views') 34 | @doc = Foos.new(@db) 35 | @raw_docs = [ 36 | {'_id' => 'foo', 'foo' => 'foo', 'bar' => 'foo', '.kind' => 'TestForDesign'}, 37 | {'_id' => 'bar', 'foo' => 'bar', 'bar' => 'bar', '.kind' => 'TestForDesign'}, 38 | {'_id' => 'baz', 'foo' => 'baz', 'bar' => 'baz', '.kind' => 'TestForDesign'} 39 | ] 40 | @db.bulk_save @raw_docs 41 | @db.save({ 42 | '_id' => '_design/foos', 43 | 'views' => { 44 | 'test' => { 'map'=>'function(doc) {emit(doc.foo, doc.bar);}' }, 45 | } 46 | }) 47 | end 48 | 49 | context "parsing options" do 50 | context "when the key is a range" do 51 | before { @opts = @doc.parse_opts(:key => 'bar'..'baz') } 52 | 53 | expect { @opts[:key].will == nil } 54 | expect { @opts[:startkey].will == 'bar' } 55 | expect { @opts[:endkey].will == 'baz' } 56 | end 57 | 58 | context "when the key is an array with a range in it" do 59 | before { @opts = @doc.parse_opts(:key => ['published', '2008'..'2008/13']) } 60 | 61 | expect { @opts[:key].will be(nil) } 62 | expect { @opts[:startkey].will == ['published', '2008'] } 63 | expect { @opts[:endkey].will == ['published', '2008/13'] } 64 | end 65 | end 66 | 67 | context "with docs" do 68 | before { @response = @doc.docs_for :test, :key => 'foo' } 69 | 70 | expect { @response.will be_kind_of(Array) } 71 | expect { @response.size.will == 1 } 72 | expect { @response.first.will be_kind_of(TestForDesign) } 73 | expect { @response.first['foo'].will == 'foo' } 74 | end 75 | 76 | context "for the view's data" do 77 | before { @response = @doc.values_for :test } 78 | 79 | expect { @response.will == %w(bar baz foo) } 80 | end 81 | 82 | context "for the view's matching keys" do 83 | before { @response = @doc.keys_for :test, :key => 'bar'..'baz' } 84 | 85 | expect { @response.will == %w(bar baz) } 86 | end 87 | 88 | context "for the view's matching ids" do 89 | before { @response = @doc.ids_for :test, :key => 'bar'..'foo'} 90 | 91 | expect { @response.will == %w(bar baz foo) } 92 | end 93 | end 94 | 95 | end -------------------------------------------------------------------------------- /test/document_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper.rb') 2 | 3 | class Foo < Exegesis::Document; end 4 | class Bar < Exegesis::Document; end 5 | 6 | class Caster < Exegesis::Document 7 | cast 'castee' 8 | cast 'castees' 9 | cast 'time', :as => 'Time' 10 | cast 'regex', :as => 'Regexp' 11 | cast 'regexen', :as => 'Regexp' 12 | end 13 | class WithDefault < Exegesis::Document 14 | default :foo => 'bar' 15 | end 16 | class Exposer < Exegesis::Document 17 | expose :foo, :bar 18 | show :baz 19 | end 20 | class Timestamper < Exegesis::Document 21 | timestamps! 22 | end 23 | class UniqueSnowflake < Exegesis::Document 24 | unique_id :set_id 25 | def set_id 26 | @unique_id_attempt.zero? ? "snowflake" : "snowflake-#{@unique_id_attempt}" 27 | end 28 | end 29 | 30 | 31 | class ExegesisDocumentTest < Test::Unit::TestCase 32 | 33 | context "a bare Exegesis::Document" do 34 | before do 35 | reset_db 36 | @obj = Foo.new 37 | @obj.database = @db 38 | @obj.save 39 | end 40 | 41 | expect { @obj['.kind'].will == "Foo" } 42 | expect { @obj['foo'].will == nil } 43 | expect { @obj.will_not respond_to(:foo) } 44 | expect { @obj['created_at'].will == nil } 45 | expect { @obj['updated_at'].will == nil } 46 | end 47 | 48 | context "instantiating" do 49 | expect { Exegesis::Document.instantiate({'.kind' => 'Foo'}).will be_kind_of(Foo) } 50 | 51 | context "transitions" do 52 | before do 53 | @foo = Foo.new 54 | @foo['.kind'] = 'Bar' 55 | @bar = Exegesis::Document.instantiate(@foo) 56 | end 57 | 58 | expect { @bar.will be_kind_of(Bar) } 59 | end 60 | end 61 | 62 | context "casting keys into classes" do 63 | before do 64 | @caster = Caster.new({ 65 | 'castee' => {'foo' => 'bar', '.kind' => 'Foo'}, 66 | 'castees' => [{'foo' => 'bar', '.kind' => 'Foo'}, {'foo' => 'baz', '.kind' => 'Bar'}], 67 | 'time' => Time.now.to_json, 68 | 'regex' => 'foo', 69 | 'regexen' => ['foo', 'bar'] 70 | }) 71 | end 72 | 73 | expect { @caster['castee'].will be_kind_of(Foo) } 74 | expect { @caster['castee']['foo'].will == 'bar' } 75 | 76 | expect { @caster['regex'].will be_kind_of(Regexp) } 77 | expect { @caster['regex'].will == /foo/ } 78 | 79 | expect { @caster['time'].will be_kind_of(Time) } 80 | 81 | expect { @caster['castees'].will be_kind_of(Array) } 82 | expect { @caster['castees'].first.will be_kind_of(Foo) } 83 | expect { @caster['castees'].first['foo'].will == 'bar' } 84 | expect { @caster['castees'].last.will be_kind_of(Bar) } 85 | expect { @caster['castees'].last['foo'].will == 'baz' } 86 | 87 | expect { @caster['regexen'].will be_kind_of(Array) } 88 | expect { @caster['regexen'].first.will be_kind_of(Regexp) } 89 | expect { @caster['regexen'].last.will be_kind_of(Regexp) } 90 | expect { @caster['regexen'].first.will == /foo/ } 91 | expect { @caster['regexen'].last.will == /bar/ } 92 | 93 | context "with bad syntax" do 94 | expect { lambda{Caster.class_eval {cast :foo, 'Time'} }.will raise_error(ArgumentError) } 95 | end 96 | end 97 | 98 | context "default objects" do 99 | expect { WithDefault.new['foo'].will == 'bar' } 100 | expect { WithDefault.new({'foo' => 'baz'})['foo'].will == 'baz' } 101 | end 102 | 103 | context "exposing keys as methods" do 104 | before do 105 | @obj = Exposer.new(:foo => 'bar', :bar => 'foo', :baz => 'bee') 106 | end 107 | 108 | expect { @obj.will respond_to(:foo) } 109 | expect { @obj.foo.will == 'bar' } 110 | expect { @obj.will respond_to(:bar) } 111 | expect { @obj.bar.will == 'foo' } 112 | expect { @obj.will respond_to(:baz) } 113 | expect { @obj.baz.will == 'bee' } 114 | 115 | expect { @obj.will respond_to(:foo=) } 116 | expect { @obj.will respond_to(:bar=) } 117 | expect { @obj.wont respond_to(:baz=) } 118 | 119 | describe "writing methods" do 120 | before do 121 | @obj.foo = "foo" 122 | end 123 | 124 | expect { @obj.foo.will == "foo" } 125 | end 126 | end 127 | 128 | context "with timestamps" do 129 | before do 130 | reset_db 131 | @obj = Timestamper.new 132 | @obj.database = @db 133 | stub(Time).now { Time.utc(2009,1,15) } 134 | @obj.save 135 | @obj = Timestamper.new(@db.get(@obj.id)) 136 | end 137 | 138 | context "initial save" do 139 | expect { @obj['created_at'].will == Time.now } 140 | expect { @obj['updated_at'].will == Time.now } 141 | end 142 | 143 | context "when created_at already exists" do 144 | before do 145 | @obj.database = @db 146 | @obj['created_at'] = Time.now 147 | stub(Time).now { Time.utc(2009,1,16) } 148 | @obj.save 149 | @obj = Timestamper.new(@db.get(@obj.id)) 150 | end 151 | 152 | expect { @obj['created_at'].will == Time.utc(2009,1,15) } 153 | expect { @obj['updated_at'].will == Time.utc(2009,1,16) } 154 | end 155 | 156 | end 157 | 158 | context "with a custom unique_id setter" do 159 | before do 160 | reset_db 161 | @obj = UniqueSnowflake.new 162 | @obj.database = @db 163 | end 164 | 165 | context "when the id isn't in use yet" do 166 | before do 167 | @obj.save 168 | end 169 | 170 | expect { @obj.id.will == "snowflake" } 171 | end 172 | 173 | context "when there is an id in place already" do 174 | before do 175 | @obj['_id'] = 'foo' 176 | @obj.save 177 | end 178 | 179 | expect { @obj.id.will == "foo" } 180 | end 181 | 182 | context "when the desired id is already in use" do 183 | before do 184 | @db.save({'_id' => 'snowflake', 'foo' => 'bar'}) 185 | @obj.save 186 | end 187 | 188 | expect { @obj.id.will == 'snowflake-1' } 189 | end 190 | end 191 | 192 | context "interacting with rails/merb assumptions" do 193 | context "to_param for routes" do 194 | before { @doc = Foo.new({'_id' => 'foo'})} 195 | expect { @doc.to_param.will == "foo" } 196 | end 197 | end 198 | 199 | end 200 | --------------------------------------------------------------------------------