langalex-couch_potato 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CREDITS +3 -0
  2. data/MIT-LICENSE.txt +19 -0
  3. data/init.rb +5 -0
  4. data/lib/core_ext/object.rb +5 -0
  5. data/lib/core_ext/time.rb +14 -0
  6. data/lib/couch_potato/active_record/compatibility.rb +9 -0
  7. data/lib/couch_potato/ordering.rb +88 -0
  8. data/lib/couch_potato/persistence/belongs_to_property.rb +44 -0
  9. data/lib/couch_potato/persistence/bulk_save_queue.rb +47 -0
  10. data/lib/couch_potato/persistence/callbacks.rb +96 -0
  11. data/lib/couch_potato/persistence/collection.rb +51 -0
  12. data/lib/couch_potato/persistence/external_collection.rb +54 -0
  13. data/lib/couch_potato/persistence/external_has_many_property.rb +68 -0
  14. data/lib/couch_potato/persistence/find.rb +21 -0
  15. data/lib/couch_potato/persistence/finder.rb +109 -0
  16. data/lib/couch_potato/persistence/inline_collection.rb +14 -0
  17. data/lib/couch_potato/persistence/inline_has_many_property.rb +39 -0
  18. data/lib/couch_potato/persistence/json.rb +46 -0
  19. data/lib/couch_potato/persistence/properties.rb +40 -0
  20. data/lib/couch_potato/persistence/simple_property.rb +33 -0
  21. data/lib/couch_potato/persistence.rb +186 -0
  22. data/lib/couch_potato/versioning.rb +46 -0
  23. data/lib/couch_potato.rb +20 -0
  24. data/spec/attributes_spec.rb +22 -0
  25. data/spec/belongs_to_spec.rb +37 -0
  26. data/spec/callbacks_spec.rb +229 -0
  27. data/spec/create_spec.rb +68 -0
  28. data/spec/destroy_spec.rb +24 -0
  29. data/spec/find_spec.rb +88 -0
  30. data/spec/finder_spec.rb +115 -0
  31. data/spec/has_many_spec.rb +178 -0
  32. data/spec/inline_collection_spec.rb +15 -0
  33. data/spec/ordering_spec.rb +94 -0
  34. data/spec/property_spec.rb +46 -0
  35. data/spec/reload_spec.rb +46 -0
  36. data/spec/spec.opts +4 -0
  37. data/spec/spec_helper.rb +31 -0
  38. data/spec/update_spec.rb +33 -0
  39. data/spec/versioning_spec.rb +149 -0
  40. metadata +132 -0
@@ -0,0 +1,39 @@
1
+ class InlineHasManyProperty
2
+ attr_accessor :name
3
+
4
+ def initialize(owner_clazz, name, options = {})
5
+ @name = name
6
+ getter = <<-GETTER
7
+ def #{name}
8
+ @#{name} ||= CouchPotato::Persistence::InlineCollection.new(#{item_class_name})
9
+ end
10
+ GETTER
11
+ owner_clazz.class_eval getter
12
+ end
13
+
14
+ def build(object, json)
15
+ object.send("#{name}").clear
16
+ json[name.to_s].each do |item|
17
+ item.delete 'ruby_class'
18
+ object.send("#{name}").build item
19
+ end
20
+ end
21
+
22
+ def save(object)
23
+
24
+ end
25
+
26
+ def serialize(json, object)
27
+ json[name.to_s] = object.send(name)
28
+ end
29
+
30
+ def destroy(object)
31
+
32
+ end
33
+
34
+ private
35
+
36
+ def item_class_name
37
+ @name.to_s.singularize.camelcase
38
+ end
39
+ end
@@ -0,0 +1,46 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ module Json
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ def to_json(*args)
9
+ (self.class.properties).inject({}) do |props, property|
10
+ property.serialize(props, self)
11
+ props
12
+ end.merge('ruby_class' => self.class.name).merge(id_and_rev_json).merge(timestamps_json).to_json(*args)
13
+ end
14
+
15
+ private
16
+
17
+ def id_and_rev_json
18
+ [:_id, :_rev, :_deleted].inject({}) do |hash, key|
19
+ hash[key] = self.send(key) unless self.send(key).nil?
20
+ hash
21
+ end
22
+ end
23
+
24
+ def timestamps_json
25
+ [:created_at, :updated_at].inject({}) do |hash, key|
26
+ hash[key] = self.send(key).to_s unless self.send(key).nil?
27
+ hash
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ def json_create(json)
33
+ instance = self.new
34
+ instance.created_at = Time.parse(json['created_at'])
35
+ instance.updated_at = Time.parse(json['updated_at'])
36
+ instance._id = json['_id']
37
+ instance._rev = json['_rev']
38
+ properties.each do |property|
39
+ property.build(instance, json)
40
+ end
41
+ instance
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,40 @@
1
+ require File.dirname(__FILE__) + '/simple_property'
2
+ require File.dirname(__FILE__) + '/belongs_to_property'
3
+ require File.dirname(__FILE__) + '/inline_has_many_property'
4
+ require File.dirname(__FILE__) + '/external_has_many_property'
5
+
6
+ module CouchPotato
7
+ module Persistence
8
+ module Properties
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ base.class_eval do
12
+ def self.properties
13
+ @@properties ||= {}
14
+ @@properties[self.name] ||= []
15
+ end
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ def property_names
21
+ properties.map(&:name)
22
+ end
23
+
24
+ def property(name, options = {})
25
+ clazz = options.delete(:class)
26
+ properties << (clazz || SimpleProperty).new(self, name, options)
27
+ end
28
+
29
+ def belongs_to(name)
30
+ property name, :class => BelongsToProperty
31
+ end
32
+
33
+ def has_many(name, options = {})
34
+ stored = options.delete(:stored)
35
+ property name, options.merge(:class => (stored == :inline ? InlineHasManyProperty : ExternalHasManyProperty))
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ class SimpleProperty
4
+ attr_accessor :name
5
+
6
+ def initialize(owner_clazz, name, options = {})
7
+ self.name = name
8
+ owner_clazz.class_eval do
9
+ attr_accessor name
10
+ define_method "#{name}?" do
11
+ !self.send(name).nil? && !self.send(name).try(:blank?)
12
+ end
13
+ end
14
+ end
15
+
16
+ def build(object, json)
17
+ object.send "#{name}=", json.stringify_keys[name.to_s]
18
+ end
19
+
20
+ def save(object)
21
+
22
+ end
23
+
24
+ def destroy(object)
25
+
26
+ end
27
+
28
+ def serialize(json, object)
29
+ json[name] = object.send name
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,186 @@
1
+ require 'digest/md5'
2
+ require 'couchrest'
3
+ require 'validatable'
4
+ require File.dirname(__FILE__) + '/persistence/inline_collection'
5
+ require File.dirname(__FILE__) + '/persistence/external_collection'
6
+ require File.dirname(__FILE__) + '/persistence/properties'
7
+ require File.dirname(__FILE__) + '/persistence/callbacks'
8
+ require File.dirname(__FILE__) + '/persistence/json'
9
+ require File.dirname(__FILE__) + '/persistence/bulk_save_queue'
10
+ require File.dirname(__FILE__) + '/persistence/find'
11
+
12
+ module CouchPotato
13
+ module Persistence
14
+
15
+ class ValidationsFailedError < ::Exception; end
16
+ class UnsavedRecordError < ::Exception; end
17
+
18
+ def self.included(base)
19
+ base.send :extend, ClassMethods, Find
20
+ base.send :include, Callbacks, Properties, Validatable, Json
21
+ base.class_eval do
22
+ attr_accessor :_id, :_rev, :_attachments, :_deleted, :created_at, :updated_at
23
+ attr_reader :bulk_save_queue
24
+ alias_method :id, :_id
25
+ end
26
+ end
27
+
28
+ def initialize(attributes = {})
29
+ @bulk_save_queue = BulkSaveQueue.new
30
+ attributes.each do |name, value|
31
+ self.send("#{name}=", value)
32
+ end if attributes
33
+ end
34
+
35
+ def attributes=(hash)
36
+ hash.each do |attribute, value|
37
+ self.send "#{attribute}=", value
38
+ end
39
+ end
40
+
41
+ def attributes
42
+ self.class.properties.inject({}) do |res, property|
43
+ property.serialize(res, self)
44
+ res
45
+ end
46
+ end
47
+
48
+ def save!
49
+ save || raise(ValidationsFailedError.new(self.errors.full_messages))
50
+ end
51
+
52
+ def save
53
+ if new_document?
54
+ create_document
55
+ else
56
+ update_document
57
+ end
58
+ end
59
+
60
+ def destroy
61
+ run_callbacks(:before_destroy)
62
+ self._deleted = true
63
+ bulk_save_queue << self
64
+ destroy_dependent_objects
65
+ bulk_save_queue.save do |res|
66
+ self._id = nil
67
+ self._rev = nil
68
+ end
69
+ run_callbacks(:after_destroy)
70
+ end
71
+
72
+ def reload
73
+ raise(UnsavedRecordError.new) unless _id
74
+ json = self.class.db.get _id
75
+ self.class.properties.each do |property|
76
+ property.build self, json
77
+ end
78
+ end
79
+
80
+ def new_document?
81
+ _id.nil?
82
+ end
83
+
84
+ def to_param
85
+ _id
86
+ end
87
+
88
+ def [](name)
89
+ self.send name
90
+ end
91
+
92
+ def ==(other)
93
+ other.class == self.class && self.class.property_names.map{|name| self.send(name)} == self.class.property_names.map{|name| other.send(name)}
94
+ end
95
+
96
+ private
97
+
98
+ def create_document
99
+ run_callbacks :before_validation_on_save
100
+ run_callbacks :before_validation_on_create
101
+ return unless valid?
102
+ run_callbacks :before_save
103
+ run_callbacks :before_create
104
+ self.created_at = Time.now
105
+ self.updated_at = Time.now
106
+ self._id = self.class.db.server.next_uuid rescue Digest::MD5.hexdigest(rand(1000000000000).to_s) # only works with couchdb 0.9
107
+ bulk_save_queue << self
108
+ save_dependent_objects
109
+ bulk_save_queue.save do |res|
110
+ self._rev = extract_rev(res)
111
+ end
112
+ run_callbacks :after_save
113
+ run_callbacks :after_create
114
+ true
115
+ end
116
+
117
+ def extract_rev(res)
118
+ res['new_revs'].select{|hash| hash['id'] == self.id}.first['rev']
119
+ end
120
+
121
+ def update_document
122
+ run_callbacks(:before_validation_on_save)
123
+ run_callbacks(:before_validation_on_update)
124
+ return unless valid?
125
+ run_callbacks :before_save
126
+ run_callbacks :before_update
127
+ self.updated_at = Time.now
128
+ bulk_save_queue << self
129
+ save_dependent_objects
130
+ bulk_save_queue.save do |res|
131
+ self._rev = extract_rev(res)
132
+ end
133
+ run_callbacks :after_save
134
+ run_callbacks :after_update
135
+ true
136
+ end
137
+
138
+ def save_dependent_objects
139
+ self.class.properties.each do |property|
140
+ property.save(self)
141
+ end
142
+ end
143
+
144
+ def destroy_dependent_objects
145
+ self.class.properties.each do |property|
146
+ property.destroy(self)
147
+ end
148
+ end
149
+
150
+ module ClassMethods
151
+
152
+ def create!(attributes = {})
153
+ instance = self.new attributes
154
+ instance.save!
155
+ instance
156
+ end
157
+
158
+ def create(attributes = {})
159
+ instance = self.new attributes
160
+ instance.save
161
+ instance
162
+ end
163
+
164
+ def get(id)
165
+ begin
166
+ self.json_create db.get(id)
167
+ rescue(RestClient::ResourceNotFound)
168
+ nil
169
+ end
170
+ end
171
+
172
+ def db(name = nil)
173
+ ::CouchPotato::Persistence.Db(name)
174
+ end
175
+ end
176
+
177
+ def self.Db(database_name = nil)
178
+ database_name ||= CouchPotato::Config.database_name || raise('No Database configured. Set CouchPotato::Config.database_name')
179
+ full_url_to_database = database_name
180
+ if full_url_to_database !~ /^http:\/\//
181
+ full_url_to_database = "http://localhost:5984/#{database_name}"
182
+ end
183
+ CouchRest.database!(full_url_to_database)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,46 @@
1
+ module CouchPotato
2
+ module Versioning
3
+ def self.included(base)
4
+ base.class_eval do
5
+ property :version
6
+ property :master_version_id
7
+ cattr_accessor :new_version_condition
8
+ before_create :set_version_default
9
+ before_update :prepare_for_new_version
10
+
11
+ def self.set_version_condition(lambda)
12
+ self.new_version_condition = lambda
13
+ end
14
+ end
15
+ end
16
+
17
+ def versions(version_no = nil)
18
+ if version_no
19
+ CouchPotato::Persistence::Finder.new.find(self.class, :version => version_no).first
20
+ elsif version == 1
21
+ [self]
22
+ else
23
+ CouchPotato::Persistence::Finder.new.find(self.class, :master_version_id => _id).sort_by(&:version)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def set_version_default
30
+ self.version ||= 1
31
+ end
32
+
33
+ def prepare_for_new_version
34
+ if new_version_condition.nil? || new_version_condition.call(self)
35
+ copy = self.class.get self._id
36
+ copy._id = nil
37
+ copy._rev = nil
38
+ copy.master_version_id = self._id
39
+ copy.save
40
+ self.master_version_id ||= self.id
41
+ self.version += 1
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'json'
4
+ require 'json/add/core'
5
+ require 'json/add/rails'
6
+
7
+ require 'ostruct'
8
+
9
+ module CouchPotato
10
+ Config = OpenStruct.new
11
+ end
12
+
13
+ require File.dirname(__FILE__) + '/core_ext/object'
14
+ require File.dirname(__FILE__) + '/core_ext/time'
15
+
16
+ require File.dirname(__FILE__) + '/couch_potato/persistence'
17
+ require File.dirname(__FILE__) + '/couch_potato/versioning'
18
+ require File.dirname(__FILE__) + '/couch_potato/ordering'
19
+ require File.dirname(__FILE__) + '/couch_potato/active_record/compatibility'
20
+
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe 'attributes=' do
4
+
5
+ class Plant
6
+ include CouchPotato::Persistence
7
+ property :leaf_count
8
+ end
9
+
10
+ it "should assign the attributes" do
11
+ plant = Plant.new
12
+ plant.attributes = {:leaf_count => 1}
13
+ plant.leaf_count.should == 1
14
+ end
15
+ end
16
+
17
+ describe "attributes" do
18
+ it "should return the attributes" do
19
+ plant = Plant.new :leaf_count => 1
20
+ plant.attributes .should == {:leaf_count => 1}
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe 'belongs_to' do
4
+ before(:each) do
5
+ @commenter = Commenter.create!
6
+ end
7
+
8
+ it "should return nil by default" do
9
+ c = Comment.new :title => 'title'
10
+ c.commenter.should be_nil
11
+ c.commenter_id.should be_nil
12
+ end
13
+
14
+ it "should assign the parent object" do
15
+ c = Comment.new :title => 'title'
16
+ c.commenter = @commenter
17
+ c.commenter.should == @commenter
18
+ c.commenter_id.should == @commenter.id
19
+ end
20
+
21
+ it "should assign the parent object id" do
22
+ c = Comment.new :title => 'title'
23
+ c.commenter_id = @commenter.id
24
+ c.commenter.should == @commenter
25
+ c.commenter_id.should == @commenter.id
26
+ end
27
+
28
+ it "should persist the link to the parent object" do
29
+ c = Comment.new :title => 'title'
30
+ c.commenter_id = @commenter.id
31
+ c.save!
32
+ c = Comment.find c.id
33
+ c.commenter._id.should == @commenter.id
34
+ c.commenter_id.should == @commenter._id
35
+ end
36
+
37
+ end
@@ -0,0 +1,229 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class CallbackRecorder
4
+ include CouchPotato::Persistence
5
+
6
+ property :required_property
7
+
8
+ validates_presence_of :required_property
9
+
10
+ [:before_validation_on_create,
11
+ :before_validation_on_save, :before_validation_on_update,
12
+ :before_save, :before_create, :before_create,
13
+ :after_save, :after_create, :after_create,
14
+ :before_update, :after_update,
15
+ :before_destroy, :after_destroy
16
+ ].each do |callback|
17
+ define_method callback do
18
+ callbacks << callback
19
+ end
20
+ self.send callback, callback
21
+ end
22
+
23
+ def callbacks
24
+ @callbacks ||= []
25
+ end
26
+
27
+ end
28
+
29
+ describe "multiple callbacks at once" do
30
+ class Monkey
31
+ include CouchPotato::Persistence
32
+ attr_accessor :eaten_banana, :eaten_apple
33
+
34
+ before_create :eat_apple, :eat_banana
35
+
36
+ private
37
+
38
+ def eat_banana
39
+ self.eaten_banana = true
40
+ end
41
+
42
+ def eat_apple
43
+ self.eaten_apple = true
44
+ end
45
+ end
46
+ it "should run all callback methods given to the callback method call" do
47
+ monkey = Monkey.create!
48
+ monkey.eaten_banana.should be_true
49
+ monkey.eaten_apple.should be_true
50
+ end
51
+ end
52
+
53
+ describe 'create callbacks' do
54
+
55
+ before(:each) do
56
+ @recorder = CallbackRecorder.new
57
+ end
58
+
59
+ describe "successful create" do
60
+ before(:each) do
61
+ @recorder.required_property = 1
62
+ end
63
+
64
+ it "should call before_validation_on_create" do
65
+ @recorder.save!
66
+ @recorder.callbacks.should include(:before_validation_on_create)
67
+ end
68
+
69
+ it "should call before_validation_on_save" do
70
+ @recorder.save!
71
+ @recorder.callbacks.should include(:before_validation_on_save)
72
+ end
73
+
74
+ it "should call before_save" do
75
+ @recorder.save!
76
+ @recorder.callbacks.should include(:before_save)
77
+ end
78
+
79
+ it "should call after_save" do
80
+ @recorder.save!
81
+ @recorder.callbacks.should include(:after_save)
82
+ end
83
+
84
+ it "should call before_create" do
85
+ @recorder.save!
86
+ @recorder.callbacks.should include(:before_create)
87
+ end
88
+
89
+ it "should call after_create" do
90
+ @recorder.save!
91
+ @recorder.callbacks.should include(:after_create)
92
+ end
93
+
94
+ end
95
+
96
+ describe "failed create" do
97
+
98
+ it "should call before_validation_on_create" do
99
+ @recorder.save
100
+ @recorder.callbacks.should include(:before_validation_on_create)
101
+ end
102
+
103
+ it "should call before_validation_on_save" do
104
+ @recorder.save
105
+ @recorder.callbacks.should include(:before_validation_on_save)
106
+ end
107
+
108
+ it "should not call before_save" do
109
+ @recorder.save
110
+ @recorder.callbacks.should_not include(:before_save)
111
+ end
112
+
113
+ it "should not call after_save" do
114
+ @recorder.save
115
+ @recorder.callbacks.should_not include(:after_save)
116
+ end
117
+
118
+ it "should not call before_create" do
119
+ @recorder.save
120
+ @recorder.callbacks.should_not include(:before_create)
121
+ end
122
+
123
+ it "should not call after_create" do
124
+ @recorder.save
125
+ @recorder.callbacks.should_not include(:after_create)
126
+ end
127
+
128
+ end
129
+
130
+
131
+ end
132
+
133
+ describe "update callbacks" do
134
+
135
+ before(:each) do
136
+ @recorder = CallbackRecorder.create! :required_property => 1
137
+ @recorder.callbacks.clear
138
+ end
139
+
140
+ describe "successful update" do
141
+
142
+ it "should call before_validation_on_update" do
143
+ @recorder.save!
144
+ @recorder.callbacks.should include(:before_validation_on_update)
145
+ end
146
+
147
+ it "should call before_validation_on_save" do
148
+ @recorder.save!
149
+ @recorder.callbacks.should include(:before_validation_on_save)
150
+ end
151
+
152
+ it "should call before_save" do
153
+ @recorder.save!
154
+ @recorder.callbacks.should include(:before_save)
155
+ end
156
+
157
+ it "should call after_save" do
158
+ @recorder.save!
159
+ @recorder.callbacks.should include(:after_save)
160
+ end
161
+
162
+ it "should call before_update" do
163
+ @recorder.save!
164
+ @recorder.callbacks.should include(:before_update)
165
+ end
166
+
167
+ it "should call after_update" do
168
+ @recorder.save!
169
+ @recorder.callbacks.should include(:after_update)
170
+ end
171
+
172
+ end
173
+
174
+ describe "failed update" do
175
+
176
+ before(:each) do
177
+ @recorder.required_property = nil
178
+ end
179
+
180
+ it "should call before_validation_on_update" do
181
+ @recorder.save
182
+ @recorder.callbacks.should include(:before_validation_on_update)
183
+ end
184
+
185
+ it "should call before_validation_on_save" do
186
+ @recorder.save
187
+ @recorder.callbacks.should include(:before_validation_on_save)
188
+ end
189
+
190
+ it "should not call before_save" do
191
+ @recorder.save
192
+ @recorder.callbacks.should_not include(:before_save)
193
+ end
194
+
195
+ it "should not call after_save" do
196
+ @recorder.save
197
+ @recorder.callbacks.should_not include(:after_save)
198
+ end
199
+
200
+ it "should not call before_update" do
201
+ @recorder.save
202
+ @recorder.callbacks.should_not include(:before_update)
203
+ end
204
+
205
+ it "should not call after_update" do
206
+ @recorder.save
207
+ @recorder.callbacks.should_not include(:after_update)
208
+ end
209
+
210
+ end
211
+
212
+ end
213
+
214
+ describe "destroy callbacks" do
215
+ before(:each) do
216
+ @recorder = CallbackRecorder.create! :required_property => 1
217
+ @recorder.callbacks.clear
218
+ end
219
+
220
+ it "should call before_destroy" do
221
+ @recorder.destroy
222
+ @recorder.callbacks.should include(:before_destroy)
223
+ end
224
+
225
+ it "should call after_destroy" do
226
+ @recorder.destroy
227
+ @recorder.callbacks.should include(:after_destroy)
228
+ end
229
+ end