couch_potato-rails2 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/.gitignore +7 -0
  2. data/.travis.yml +5 -0
  3. data/CHANGES.md +148 -0
  4. data/CREDITS +6 -0
  5. data/Gemfile +4 -0
  6. data/MIT-LICENSE.txt +19 -0
  7. data/README.md +450 -0
  8. data/Rakefile +82 -0
  9. data/couch_potato.gemspec +27 -0
  10. data/init.rb +3 -0
  11. data/lib/core_ext/date.rb +14 -0
  12. data/lib/core_ext/object.rb +5 -0
  13. data/lib/core_ext/string.rb +12 -0
  14. data/lib/core_ext/symbol.rb +15 -0
  15. data/lib/core_ext/time.rb +23 -0
  16. data/lib/couch_potato.rb +48 -0
  17. data/lib/couch_potato/database.rb +179 -0
  18. data/lib/couch_potato/persistence.rb +124 -0
  19. data/lib/couch_potato/persistence/active_model_compliance.rb +44 -0
  20. data/lib/couch_potato/persistence/attachments.rb +31 -0
  21. data/lib/couch_potato/persistence/callbacks.rb +29 -0
  22. data/lib/couch_potato/persistence/dirty_attributes.rb +56 -0
  23. data/lib/couch_potato/persistence/ghost_attributes.rb +12 -0
  24. data/lib/couch_potato/persistence/json.rb +47 -0
  25. data/lib/couch_potato/persistence/magic_timestamps.rb +23 -0
  26. data/lib/couch_potato/persistence/properties.rb +79 -0
  27. data/lib/couch_potato/persistence/simple_property.rb +82 -0
  28. data/lib/couch_potato/persistence/type_caster.rb +40 -0
  29. data/lib/couch_potato/railtie.rb +25 -0
  30. data/lib/couch_potato/rspec.rb +2 -0
  31. data/lib/couch_potato/rspec/matchers.rb +39 -0
  32. data/lib/couch_potato/rspec/matchers/json2.js +482 -0
  33. data/lib/couch_potato/rspec/matchers/list_as_matcher.rb +54 -0
  34. data/lib/couch_potato/rspec/matchers/map_to_matcher.rb +49 -0
  35. data/lib/couch_potato/rspec/matchers/print_r.js +60 -0
  36. data/lib/couch_potato/rspec/matchers/reduce_to_matcher.rb +50 -0
  37. data/lib/couch_potato/rspec/stub_db.rb +46 -0
  38. data/lib/couch_potato/validation.rb +16 -0
  39. data/lib/couch_potato/validation/with_active_model.rb +27 -0
  40. data/lib/couch_potato/validation/with_validatable.rb +41 -0
  41. data/lib/couch_potato/version.rb +3 -0
  42. data/lib/couch_potato/view/base_view_spec.rb +84 -0
  43. data/lib/couch_potato/view/custom_view_spec.rb +42 -0
  44. data/lib/couch_potato/view/custom_views.rb +52 -0
  45. data/lib/couch_potato/view/lists.rb +23 -0
  46. data/lib/couch_potato/view/model_view_spec.rb +75 -0
  47. data/lib/couch_potato/view/properties_view_spec.rb +47 -0
  48. data/lib/couch_potato/view/raw_view_spec.rb +25 -0
  49. data/lib/couch_potato/view/view_query.rb +82 -0
  50. data/rails/init.rb +4 -0
  51. data/rails/reload_classes.rb +47 -0
  52. data/spec/attachments_spec.rb +23 -0
  53. data/spec/callbacks_spec.rb +297 -0
  54. data/spec/create_spec.rb +35 -0
  55. data/spec/custom_view_spec.rb +239 -0
  56. data/spec/default_property_spec.rb +38 -0
  57. data/spec/destroy_spec.rb +29 -0
  58. data/spec/fixtures/address.rb +10 -0
  59. data/spec/fixtures/person.rb +6 -0
  60. data/spec/property_spec.rb +323 -0
  61. data/spec/rails_spec.rb +50 -0
  62. data/spec/railtie_spec.rb +65 -0
  63. data/spec/spec.opts +2 -0
  64. data/spec/spec_helper.rb +44 -0
  65. data/spec/unit/active_model_compliance_spec.rb +98 -0
  66. data/spec/unit/attributes_spec.rb +135 -0
  67. data/spec/unit/base_view_spec_spec.rb +106 -0
  68. data/spec/unit/callbacks_spec.rb +46 -0
  69. data/spec/unit/couch_potato_spec.rb +39 -0
  70. data/spec/unit/create_spec.rb +69 -0
  71. data/spec/unit/custom_views_spec.rb +15 -0
  72. data/spec/unit/database_spec.rb +317 -0
  73. data/spec/unit/date_spec.rb +22 -0
  74. data/spec/unit/dirty_attributes_spec.rb +136 -0
  75. data/spec/unit/initialize_spec.rb +38 -0
  76. data/spec/unit/json_spec.rb +30 -0
  77. data/spec/unit/lists_spec.rb +20 -0
  78. data/spec/unit/model_view_spec_spec.rb +13 -0
  79. data/spec/unit/properties_view_spec_spec.rb +31 -0
  80. data/spec/unit/rspec_matchers_spec.rb +124 -0
  81. data/spec/unit/rspec_stub_db_spec.rb +35 -0
  82. data/spec/unit/string_spec.rb +7 -0
  83. data/spec/unit/time_spec.rb +15 -0
  84. data/spec/unit/validation_spec.rb +67 -0
  85. data/spec/unit/view_query_spec.rb +86 -0
  86. data/spec/update_spec.rb +40 -0
  87. data/spec/view_updates_spec.rb +28 -0
  88. metadata +243 -0
@@ -0,0 +1,82 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake'
5
+ require "rspec/core/rake_task"
6
+ require 'rake/rdoctask'
7
+
8
+ def with_validatable(&block)
9
+ begin
10
+ require 'validatable'
11
+
12
+ ENV['VALIDATION_FRAMEWORK'] = 'validatable'
13
+ puts "Running task with Validatable validation framework."
14
+ yield block
15
+ rescue LoadError
16
+ STDERR.puts "WARNING: Validatable not available, skipping task."
17
+ end
18
+ end
19
+
20
+ def with_active_model(&block)
21
+ begin
22
+ require 'active_model'
23
+
24
+ ENV['VALIDATION_FRAMEWORK'] = 'active_model'
25
+ puts "Running task with ActiveModel validation framework."
26
+ yield block
27
+ rescue LoadError
28
+ STDERR.puts "WARNING: ActiveModel not available, skipping task."
29
+ end
30
+ end
31
+
32
+ task :default => :spec
33
+
34
+ task :spec_functional_validatable do
35
+ with_validatable { Rake::Task['spec_functional_default'].execute }
36
+ end
37
+
38
+ task :spec_functional_active_model do
39
+ with_active_model { Rake::Task['spec_functional_default'].execute }
40
+ end
41
+
42
+ task :spec_unit_validatable do
43
+ with_validatable { Rake::Task['spec_unit_default'].execute }
44
+ end
45
+
46
+ task :spec_unit_active_model do
47
+ with_active_model { Rake::Task['spec_unit_default'].execute }
48
+ end
49
+
50
+ desc "Run functional specs with default validation framework, override with VALIDATION_FRAMEWORK"
51
+ RSpec::Core::RakeTask.new(:spec_functional_default) do |spec|
52
+ spec.pattern = 'spec/*_spec.rb'
53
+ spec.rspec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
54
+ end
55
+
56
+ desc "Run unit specs with default validation framework, override with VALIDATION_FRAMEWORK"
57
+ RSpec::Core::RakeTask.new(:spec_unit_default) do |spec|
58
+ spec.pattern = 'spec/unit/*_spec.rb'
59
+ spec.rspec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
60
+ end
61
+
62
+ desc "Run functional specs with all validation frameworks"
63
+ task :spec_functional => [:spec_functional_validatable, :spec_functional_active_model] do
64
+ end
65
+
66
+ desc "Run unit specs with all validation frameworks"
67
+ task :spec_unit => [:spec_unit_validatable, :spec_unit_active_model] do
68
+ end
69
+
70
+ desc "Run all specs"
71
+ task :spec => [:spec_unit, :spec_functional] do
72
+ end
73
+
74
+ desc 'Generate documentation'
75
+ Rake::RDocTask.new(:rdoc) do |rdoc|
76
+ rdoc.rdoc_dir = 'rdoc'
77
+ rdoc.title = 'Couch Potato'
78
+ rdoc.options << '--line-numbers' << '--inline-source'
79
+ rdoc.rdoc_files.include('README.md')
80
+ rdoc.rdoc_files.include('lib/couch_potato.rb')
81
+ rdoc.rdoc_files.include('lib/couch_potato/**/*.rb')
82
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "couch_potato/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "couch_potato-rails2"
7
+ s.summary = %Q{Ruby persistence layer for CouchDB}
8
+ s.email = "alex@upstre.am"
9
+ s.homepage = "http://github.com/langalex/couch_potato"
10
+ s.description = "Ruby persistence layer for CouchDB"
11
+ s.authors = ["Alexander Lang"]
12
+ s.version = CouchPotato::VERSION
13
+ s.platform = Gem::Platform::RUBY
14
+
15
+ s.add_dependency 'json'
16
+ s.add_dependency 'couchrest', '>=1.0.1'
17
+
18
+ s.add_development_dependency 'rspec', '>=2.0'
19
+ s.add_development_dependency 'timecop'
20
+ s.add_development_dependency 'tzinfo'
21
+ s.add_development_dependency 'rake'
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
26
+ s.require_paths = ["lib"]
27
+ end
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ # this is for rails only
2
+
3
+ require File.dirname(__FILE__) + '/rails/init'
@@ -0,0 +1,14 @@
1
+ class Date
2
+ def to_json(*a)
3
+ %("#{as_json}")
4
+ end
5
+
6
+ def as_json(*args)
7
+ strftime("%Y/%m/%d")
8
+ end
9
+
10
+ def self.json_create string
11
+ return nil if string.nil?
12
+ Date.parse(string)
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ Object.class_eval do
2
+ def try(method, *args)
3
+ self.send method, *args if self.respond_to?(method)
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveSupportMethods
2
+ def camelize
3
+ sub(/^([a-z])/) {$1.upcase}.gsub(/_([a-z])/) do
4
+ $1.upcase
5
+ end
6
+ end
7
+
8
+ def blank?
9
+ empty?
10
+ end
11
+ end
12
+ String.send :include, ActiveSupportMethods unless String.new.respond_to?(:underscore)
@@ -0,0 +1,15 @@
1
+ # taken from ActiveSupport 2.3.2
2
+ unless :to_proc.respond_to?(:to_proc)
3
+ class Symbol
4
+ # Turns the symbol into a simple proc, which is especially useful for enumerations. Examples:
5
+ #
6
+ # # The same as people.collect { |p| p.name }
7
+ # people.collect(&:name)
8
+ #
9
+ # # The same as people.select { |p| p.manager? }.collect { |p| p.salary }
10
+ # people.select(&:manager?).collect(&:salary)
11
+ def to_proc
12
+ Proc.new { |*args| args.shift.__send__(self, *args) }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ require 'active_support/time'
2
+
3
+ class Time
4
+ def to_json(*a)
5
+ %("#{as_json}")
6
+ end
7
+
8
+ def as_json(*args)
9
+ getutc.strftime("%Y/%m/%d %H:%M:%S +0000")
10
+ end
11
+
12
+ def self.json_create string
13
+ return nil if string.nil?
14
+ d = DateTime.parse(string.to_s).new_offset
15
+ self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec).in_time_zone
16
+ end
17
+ end
18
+
19
+ ActiveSupport::TimeWithZone.class_eval do
20
+ def as_json(*args)
21
+ utc.as_json
22
+ end
23
+ end
@@ -0,0 +1,48 @@
1
+ require 'couchrest'
2
+ require 'json'
3
+ require 'json/add/core'
4
+ require 'json/add/rails'
5
+
6
+ require 'ostruct'
7
+
8
+ JSON.create_id = 'ruby_class'
9
+
10
+ module CouchPotato
11
+ Config = Struct.new(:database_name, :validation_framework, :split_design_documents_per_view).new
12
+ Config.validation_framework = :active_model
13
+ Config.split_design_documents_per_view = false
14
+
15
+ class NotFound < StandardError; end
16
+
17
+ # Returns a database instance which you can then use to create objects and query views. You have to set the CouchPotato::Config.database_name before this works.
18
+ def self.database
19
+ @@__database ||= Database.new(self.couchrest_database)
20
+ end
21
+
22
+ # Returns the underlying CouchRest database object if you want low level access to your CouchDB. You have to set the CouchPotato::Config.database_name before this works.
23
+ def self.couchrest_database
24
+ @@__couchrest_database ||= CouchRest.database(full_url_to_database)
25
+ end
26
+
27
+ private
28
+
29
+ def self.full_url_to_database
30
+ raise('No Database configured. Set CouchPotato::Config.database_name') unless CouchPotato::Config.database_name
31
+ if CouchPotato::Config.database_name.match(%r{https?://})
32
+ CouchPotato::Config.database_name
33
+ else
34
+ "http://127.0.0.1:5984/#{CouchPotato::Config.database_name}"
35
+ end
36
+ end
37
+ end
38
+
39
+ $LOAD_PATH << File.dirname(__FILE__)
40
+
41
+ require 'core_ext/object'
42
+ require 'core_ext/time'
43
+ require 'core_ext/date'
44
+ require 'core_ext/string'
45
+ require 'core_ext/symbol'
46
+ require 'couch_potato/validation'
47
+ require 'couch_potato/persistence'
48
+ require 'couch_potato/railtie' if defined?(Rails)
@@ -0,0 +1,179 @@
1
+ module CouchPotato
2
+ class Database
3
+
4
+ class ValidationsFailedError < ::StandardError; end
5
+
6
+ def initialize(couchrest_database)
7
+ @couchrest_database = couchrest_database
8
+ begin
9
+ couchrest_database.info
10
+ rescue RestClient::ResourceNotFound
11
+ raise "Database '#{couchrest_database.name}' does not exist."
12
+ end
13
+ end
14
+
15
+ # executes a view and return the results. you pass in a view spec
16
+ # which is usually a result of a SomePersistentClass.some_view call.
17
+ # also return the total_rows returned by CouchDB as an accessor on the results.
18
+ #
19
+ # Example:
20
+ #
21
+ # class User
22
+ # include CouchPotato::Persistence
23
+ # property :age
24
+ # view :all, key: :age
25
+ # end
26
+ # db = CouchPotato.database
27
+ #
28
+ # db.view(User.all) # => [user1, user2]
29
+ # db.view(User.all).total_rows # => 2
30
+ #
31
+ # You can pass the usual parameters you can pass to a couchdb view to the view:
32
+ #
33
+ # db.view(User.all(limit: 5, startkey: 2, reduce: false))
34
+ #
35
+ # For your convenience when passing a hash with only a key parameter you can just pass in the value
36
+ #
37
+ # db.view(User.all(key: 1)) == db.view(User.all(1))
38
+ #
39
+ # Instead of passing a startkey and endkey you can pass in a key with a range:
40
+ #
41
+ # db.view(User.all(key: 1..20)) == db.view(startkey: 1, endkey: 20) == db.view(User.all(1..20))
42
+ #
43
+ # You can also pass in multiple keys:
44
+ #
45
+ # db.view(User.all(keys: [1, 2, 3]))
46
+ def view(spec)
47
+ results = CouchPotato::View::ViewQuery.new(
48
+ couchrest_database,
49
+ spec.design_document,
50
+ {spec.view_name => {
51
+ :map => spec.map_function,
52
+ :reduce => spec.reduce_function}
53
+ },
54
+ ({spec.list_name => spec.list_function} unless spec.list_name.nil?)
55
+ ).query_view!(spec.view_parameters)
56
+ processed_results = spec.process_results results
57
+ processed_results.instance_eval "def total_rows; #{results['total_rows']}; end" if results['total_rows']
58
+ processed_results.each do |document|
59
+ document.database = self if document.respond_to?(:database=)
60
+ end if processed_results.respond_to?(:each)
61
+ processed_results
62
+ end
63
+
64
+ # returns the first result from a #view query or nil
65
+ def first(spec)
66
+ view(spec).first
67
+ end
68
+
69
+ # returns th first result from a #view or raises CouchPotato::NotFound
70
+ def first!(spec)
71
+ first(spec) || raise(CouchPotato::NotFound)
72
+ end
73
+
74
+ # saves a document. returns true on success, false on failure
75
+ def save_document(document, validate = true)
76
+ return true unless document.dirty? || document.new?
77
+ if document.new?
78
+ create_document(document, validate)
79
+ else
80
+ update_document(document, validate)
81
+ end
82
+ end
83
+ alias_method :save, :save_document
84
+
85
+ # saves a document, raises a CouchPotato::Database::ValidationsFailedError on failure
86
+ def save_document!(document)
87
+ save_document(document) || raise(ValidationsFailedError.new(document.errors.full_messages))
88
+ end
89
+ alias_method :save!, :save_document!
90
+
91
+ def destroy_document(document)
92
+ document.run_callbacks :destroy do
93
+ document._deleted = true
94
+ couchrest_database.delete_doc document.to_hash
95
+ end
96
+ document._id = nil
97
+ document._rev = nil
98
+ end
99
+ alias_method :destroy, :destroy_document
100
+
101
+ # loads a document by its id
102
+ def load_document(id)
103
+ raise "Can't load a document without an id (got nil)" if id.nil?
104
+ begin
105
+ instance = couchrest_database.get(id)
106
+ instance.database = self
107
+ instance
108
+ rescue(RestClient::ResourceNotFound)
109
+ nil
110
+ end
111
+ end
112
+ alias_method :load, :load_document
113
+
114
+ def load!(id)
115
+ load(id) || raise(CouchPotato::NotFound)
116
+ end
117
+
118
+ def inspect #:nodoc:
119
+ "#<CouchPotato::Database @root=\"#{couchrest_database.root}\">"
120
+ end
121
+
122
+ # returns the underlying CouchRest::Database instance
123
+ def couchrest_database
124
+ @couchrest_database
125
+ end
126
+
127
+ private
128
+
129
+ def create_document(document, validate)
130
+ document.database = self
131
+
132
+ if validate
133
+ document.errors.clear
134
+ document.run_callbacks :validation_on_save do
135
+ document.run_callbacks :validation_on_create do
136
+ return false unless valid_document?(document)
137
+ end
138
+ end
139
+ end
140
+
141
+ document.run_callbacks :save do
142
+ document.run_callbacks :create do
143
+ res = couchrest_database.save_doc document.to_hash
144
+ document._rev = res['rev']
145
+ document._id = res['id']
146
+ end
147
+ end
148
+ true
149
+ end
150
+
151
+ def update_document(document, validate)
152
+ if validate
153
+ document.errors.clear
154
+ document.run_callbacks :validation_on_save do
155
+ document.run_callbacks :validation_on_update do
156
+ return false unless valid_document?(document)
157
+ end
158
+ end
159
+ end
160
+
161
+ document.run_callbacks :save do
162
+ document.run_callbacks :update do
163
+ res = couchrest_database.save_doc document.to_hash
164
+ document._rev = res['rev']
165
+ end
166
+ end
167
+ true
168
+ end
169
+
170
+ def valid_document?(document)
171
+ errors = document.errors.errors.dup
172
+ document.valid?
173
+ errors.each_pair do |k, v|
174
+ v.each {|message| document.errors.add(k, message)}
175
+ end
176
+ document.errors.empty?
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,124 @@
1
+ require 'digest/md5'
2
+ require File.dirname(__FILE__) + '/database'
3
+ require File.dirname(__FILE__) + '/persistence/active_model_compliance'
4
+ require File.dirname(__FILE__) + '/persistence/properties'
5
+ require File.dirname(__FILE__) + '/persistence/magic_timestamps'
6
+ require File.dirname(__FILE__) + '/persistence/callbacks'
7
+ require File.dirname(__FILE__) + '/persistence/json'
8
+ require File.dirname(__FILE__) + '/persistence/dirty_attributes'
9
+ require File.dirname(__FILE__) + '/persistence/ghost_attributes'
10
+ require File.dirname(__FILE__) + '/persistence/attachments'
11
+ require File.dirname(__FILE__) + '/persistence/type_caster'
12
+ require File.dirname(__FILE__) + '/view/custom_views'
13
+ require File.dirname(__FILE__) + '/view/lists'
14
+ require File.dirname(__FILE__) + '/view/view_query'
15
+
16
+
17
+ module CouchPotato
18
+ module Persistence
19
+
20
+ def self.included(base) #:nodoc:
21
+ base.send :include, Properties, Callbacks, Json, CouchPotato::View::CustomViews, CouchPotato::View::Lists
22
+ base.send :include, DirtyAttributes, GhostAttributes, Attachments
23
+ base.send :include, MagicTimestamps, ActiveModelCompliance
24
+ base.send :include, Validation
25
+ base.class_eval do
26
+ attr_accessor :_id, :_rev, :_deleted, :database
27
+ alias_method :id, :_id
28
+ alias_method :id=, :_id=
29
+ end
30
+ end
31
+
32
+ # initialize a new instance of the model optionally passing it a hash of attributes.
33
+ # the attributes have to be declared using the #property method.
34
+ # the new model will be yielded to an optionally given block.
35
+ #
36
+ # example:
37
+ # class Book
38
+ # include CouchPotato::Persistence
39
+ # property :title
40
+ # end
41
+ # book = Book.new :title => 'Time to Relax'
42
+ #
43
+ # OR
44
+ #
45
+ # book = Book.new do |b|
46
+ # b.title = 'Time to Relax'
47
+ # end
48
+ # book.title # => 'Time to Relax'
49
+ def initialize(attributes = {})
50
+ if attributes
51
+ attributes.each do |name, value|
52
+ self.send("#{name}=", value)
53
+ end
54
+ end
55
+ yield self if block_given?
56
+ end
57
+
58
+ # assign multiple attributes at once.
59
+ # the attributes have to be declared using the #property method
60
+ #
61
+ # example:
62
+ # class Book
63
+ # include CouchPotato::Persistence
64
+ # property :title
65
+ # property :year
66
+ # end
67
+ # book = Book.new
68
+ # book.attributes = {:title => 'Time to Relax', :year => 2009}
69
+ # book.title # => 'Time to Relax'
70
+ # book.year # => 2009
71
+ def attributes=(hash)
72
+ hash.each do |attribute, value|
73
+ self.send "#{attribute}=", value
74
+ end
75
+ end
76
+
77
+ # returns all of a model's attributes that have been defined using the #property method as a Hash
78
+ #
79
+ # example:
80
+ # class Book
81
+ # include CouchPotato::Persistence
82
+ # property :title
83
+ # property :year
84
+ # end
85
+ # book = Book.new :year => 2009
86
+ # book.attributes # => {:title => nil, :year => 2009}
87
+ def attributes
88
+ self.class.properties.inject({}) do |res, property|
89
+ property.value(res, self)
90
+ res
91
+ end
92
+ end
93
+
94
+ # returns true if a model hasn't been saved yet, false otherwise
95
+ def new?
96
+ _rev.nil?
97
+ end
98
+ alias_method :new_record?, :new?
99
+
100
+ # returns the document id
101
+ # this is used by rails to construct URLs
102
+ # can be overridden to for example use slugs for URLs instead if ids
103
+ def to_param
104
+ _id
105
+ end
106
+
107
+ def ==(other) #:nodoc:
108
+ other.class == self.class && self.to_json == other.to_json
109
+ end
110
+
111
+ def eql?(other)
112
+ self == other
113
+ end
114
+
115
+ def hash
116
+ _id.hash * (_id.hash.to_s.size ** 10) + _rev.hash
117
+ end
118
+
119
+ def inspect
120
+ attributes_as_string = attributes.map {|attribute, value| "#{attribute}: #{value.inspect}"}.join(", ")
121
+ %Q{#<#{self.class} _id: "#{_id}", _rev: "#{_rev}", #{attributes_as_string}>}
122
+ end
123
+ end
124
+ end