andrewtimberlake-couch_potato 0.2.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/MIT-LICENSE.txt +19 -0
  2. data/README.md +279 -0
  3. data/VERSION.yml +4 -0
  4. data/init.rb +3 -0
  5. data/lib/core_ext/date.rb +10 -0
  6. data/lib/core_ext/object.rb +5 -0
  7. data/lib/core_ext/string.rb +19 -0
  8. data/lib/core_ext/symbol.rb +15 -0
  9. data/lib/core_ext/time.rb +11 -0
  10. data/lib/couch_potato.rb +40 -0
  11. data/lib/couch_potato/database.rb +105 -0
  12. data/lib/couch_potato/persistence.rb +96 -0
  13. data/lib/couch_potato/persistence/belongs_to_property.rb +58 -0
  14. data/lib/couch_potato/persistence/callbacks.rb +60 -0
  15. data/lib/couch_potato/persistence/dirty_attributes.rb +27 -0
  16. data/lib/couch_potato/persistence/json.rb +46 -0
  17. data/lib/couch_potato/persistence/magic_timestamps.rb +13 -0
  18. data/lib/couch_potato/persistence/properties.rb +57 -0
  19. data/lib/couch_potato/persistence/simple_property.rb +83 -0
  20. data/lib/couch_potato/persistence/validation.rb +18 -0
  21. data/lib/couch_potato/view/base_view_spec.rb +24 -0
  22. data/lib/couch_potato/view/custom_view_spec.rb +27 -0
  23. data/lib/couch_potato/view/custom_views.rb +44 -0
  24. data/lib/couch_potato/view/model_view_spec.rb +63 -0
  25. data/lib/couch_potato/view/properties_view_spec.rb +39 -0
  26. data/lib/couch_potato/view/raw_view_spec.rb +25 -0
  27. data/lib/couch_potato/view/view_query.rb +44 -0
  28. data/rails/init.rb +7 -0
  29. data/spec/callbacks_spec.rb +271 -0
  30. data/spec/create_spec.rb +22 -0
  31. data/spec/custom_view_spec.rb +134 -0
  32. data/spec/destroy_spec.rb +29 -0
  33. data/spec/fixtures/address.rb +9 -0
  34. data/spec/fixtures/person.rb +6 -0
  35. data/spec/property_spec.rb +83 -0
  36. data/spec/spec.opts +4 -0
  37. data/spec/spec_helper.rb +31 -0
  38. data/spec/unit/attributes_spec.rb +26 -0
  39. data/spec/unit/callbacks_spec.rb +33 -0
  40. data/spec/unit/create_spec.rb +58 -0
  41. data/spec/unit/customs_views_spec.rb +15 -0
  42. data/spec/unit/database_spec.rb +38 -0
  43. data/spec/unit/dirty_attributes_spec.rb +113 -0
  44. data/spec/unit/string_spec.rb +13 -0
  45. data/spec/unit/view_query_spec.rb +9 -0
  46. data/spec/update_spec.rb +40 -0
  47. metadata +144 -0
data/MIT-LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2007 Bryan Helmkamp, Seth Fitzsimmons
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,279 @@
1
+ ## Couch Potato
2
+
3
+ ... is a persistence layer written in ruby for CouchDB.
4
+
5
+ ### Mission
6
+
7
+ The goal of Couch Potato is to create a minimal framework in order to store and retrieve Ruby objects to/from CouchDB and create and query views.
8
+
9
+ It follows the document/view/querying semantics established by CouchDB and won't try to mimic ActiveRecord behavior in any way as that IS BAD.
10
+
11
+ Code that uses Couch Potato should be easy to test.
12
+
13
+ Lastly Couch Potato aims to provide a seamless integration with Ruby on Rails, e.g. routing, form helpers etc.
14
+
15
+ ### Core Features
16
+
17
+ * persisting objects by including the CouchPotato::Persistence module
18
+ * declarative views with either custom or generated map/reduce functions
19
+ * extensive spec suite
20
+
21
+ ### Installation
22
+
23
+ Couch Potato is hosted as a gem on github which you can install like this:
24
+
25
+ sudo gem source --add http://gems.github.com # if you haven't already
26
+ sudo gem install langalex-couch_potato
27
+
28
+ #### Using with your ruby application:
29
+
30
+ require 'rubygems'
31
+ gem 'langalex-couch_potato'
32
+ require 'couch_potato'
33
+
34
+ Alternatively you can download or clone the source repository and then require lib/couch_potato.rb.
35
+
36
+ You MUST specificy the name of the database:
37
+
38
+ CouchPotato::Config.database_name = 'name of the db'
39
+
40
+ The server URL will default to http://localhost:5984/ unless specified with:
41
+
42
+ CouchPotato::Config.database_server = "http://example.com:5984/"
43
+
44
+ #### Using with Rails
45
+
46
+ Add to your config/environment.rb:
47
+
48
+ config.gem 'langalex-couch_potato', :lib => 'couch_potato', :source => 'http://gems.github.com'
49
+
50
+ Then create a config/couchdb.yml:
51
+
52
+ development: development_db_name
53
+ test: test_db_name
54
+ production: http://db.server/production_db_name
55
+
56
+ Alternatively you can also install Couch Potato directly as a plugin.
57
+
58
+ ### Introduction
59
+
60
+ This is a basic tutorial on how to use Couch Potato. If you want to know all the details feel free to read the specs.
61
+
62
+ #### Save, load objects
63
+
64
+ First you need a class.
65
+
66
+ class User
67
+ end
68
+
69
+ To make instances of this class persistent include the persistence module:
70
+
71
+ class User
72
+ include CouchPotato::Persistence
73
+ end
74
+
75
+ If you want to store any properties you have to declare them:
76
+
77
+ class User
78
+ include CouchPotato::Persistence
79
+
80
+ property :name
81
+ end
82
+
83
+ Properties can be of any type:
84
+
85
+ class User
86
+ include CouchPotato::Persistence
87
+
88
+ property :address, :type => Address
89
+ end
90
+
91
+ Properties can have a default value
92
+
93
+ class User
94
+ include CouchPotato::Persistence
95
+
96
+ property :active, :default => true
97
+ end
98
+
99
+ Now you can save your objects. All database operations are encapsulated in the CouchPotato::Database class. This separates your domain logic from the database access logic which makes it easier to write tests and also keeps you models smaller and cleaner.
100
+
101
+ user = User.new :name => 'joe'
102
+ CouchPotato.database.save_document user # or save_document!
103
+
104
+ You can of course also retrieve your instance:
105
+
106
+ CouchPotato.database.load_document "id_of_the_user_document" # => <#User 0x3075>
107
+
108
+
109
+ #### Properties
110
+
111
+ You can access the properties you declared above through normal attribute accessors.
112
+
113
+ user.name # => 'joe'
114
+ user.name = {:first => ['joe', 'joey'], :last => 'doe', :middle => 'J'} # you can set any ruby object that responds_to :to_json (includes all core objects)
115
+ user._id # => "02097f33a0046123f1ebc0ebb6937269"
116
+ user._rev # => "2769180384"
117
+ user.created_at # => Fri Oct 24 19:05:54 +0200 2008
118
+ user.updated_at # => Fri Oct 24 19:05:54 +0200 2008
119
+ user.new? # => false
120
+
121
+ If you want to have properties that don't map to any JSON type, i.e. other than String, Number, Boolean, Hash or Array you have to define the type like this:
122
+
123
+ class User
124
+ property :date_of_birth, :type => Date
125
+ end
126
+
127
+ The date_of_birth property is now automatically serialized to JSON and back when storing/retrieving objects.
128
+
129
+ #### Dirty tracking
130
+
131
+ CouchPotato tracks the dirty state of attributes in the same way ActiveRecord does:
132
+
133
+ user = User.create :name => 'joe'
134
+ user.name # => 'joe'
135
+ user.name_changed? # => false
136
+ user.name_was # => nil
137
+
138
+ You can also force a dirty state:
139
+
140
+ user.name = 'jane'
141
+ user.name_changed? # => true
142
+ user.name_not_changed
143
+ user.name_changed? # => false
144
+ CouchPotato.database.save_document user # does nothing as no attributes are dirty
145
+
146
+
147
+ #### Object validations
148
+
149
+ Couch Potato uses the validatable library for vaidation (http://validatable.rubyforge.org/)\
150
+
151
+ class User
152
+ property :name
153
+ validates_presence_of :name
154
+ end
155
+
156
+ user = User.new
157
+ user.valid? # => false
158
+ user.errors.on(:name) # => [:name, 'can't be blank']
159
+
160
+ #### Finding stuff
161
+
162
+ In order to find data in your CouchDB you have to create a view first. Couch Potato offers you to create and manage those views for you. All you have to do is declare them in your classes:
163
+
164
+ class User
165
+ include CouchPotato::Persistence
166
+ property :name
167
+
168
+ view :all, :key => :created_at
169
+ end
170
+
171
+ This will create a view called "all" in the "user" design document with a map function that emits "created_at" for every user document.
172
+
173
+ CouchPotato.database.view User.all
174
+
175
+ This will load all user documents in your database sorted by created_at.
176
+
177
+ CouchPotato.database.view User.all(:key => (Time.now- 10)..(Time.now), :descending => true)
178
+
179
+ Any options you pass in will be passed onto CouchDB.
180
+
181
+ Composite keys are also possible:
182
+
183
+ class User
184
+ property :name
185
+
186
+ view :all, :key => [:created_at, :name]
187
+ end
188
+
189
+ The creation of views is based on view specification classes (see the CouchPotato::View module). The above code uses the ModelViewSpec class which is used to find models by their properties. For more sophisticated searches you can use other view specifications (either use the built-in or provide your own) by passing a type parameter:
190
+
191
+ If you have larger structures and you only want to load some attributes you can use the PropertiesViewSpec (the full class name is automatically derived):
192
+
193
+ class User
194
+ property :name
195
+ property :bio
196
+
197
+ view :all, :key => :created_at, :properties => [:name], :type => :properties
198
+ end
199
+
200
+ CouchPotato.database.view(User.everyone).first.name # => "joe"
201
+ CouchPotato.database.view(User.everyone).first.bio # => nil
202
+
203
+ You can also pass in custom map/reduce functions with the custom view spec:
204
+
205
+ class User
206
+ view :all, :map => "function(doc) { emit(doc.created_at, null)}", :include_docs => true, :type => :custom
207
+ end
208
+
209
+ If you don't want the results to be converted into models the raw view is your friend:
210
+
211
+ class User
212
+ view :all, :map => "function(doc) { emit(doc.created_at, doc.name)}", :type => :raw
213
+ end
214
+
215
+ When querying this view you will get the raw data returned by CouchDB which looks something like this: {'total_entries': 2, 'rows': [{'value': 'alex', 'key': '2009-01-03 00:02:34 +000', 'id': '75976rgi7546gi02a'}]}
216
+
217
+ To process this raw data you can also pass in a results filter:
218
+
219
+ class User
220
+ view :all, :map => "function(doc) { emit(doc.created_at, doc.name)}", :type => :raw, :results_filter => lambda {|results| results['rows'].map{|row| row['value']}}
221
+ end
222
+
223
+ In this case querying the view would only return the emitted value for each row.
224
+
225
+ You can pass in your own view specifications by passing in :type => MyViewSpecClass. Take a look at the CouchPotato::View::*ViewSpec classes to get an idea of how this works.
226
+
227
+ #### Associations
228
+
229
+ Not supported. Not sure if they ever will be. You can implement those yourself using views and custom methods on your models.
230
+
231
+ #### Callbacks
232
+
233
+ Couch Potato supports the usual lifecycle callbacks known from ActiveRecord:
234
+
235
+ class User
236
+ include CouchPotato::Persistence
237
+
238
+ before_create :do_something_before_create
239
+ before_update {|user| user.do_something_on_update}
240
+ end
241
+
242
+ This will call the method do_something_before_create before creating an object and run the given lambda before updating one. Lambda callbacks get passed the model as their first argument. Method callbacks don't receive any arguments.
243
+
244
+ Supported callbacks are: :before_validation, :before_validation_on_create, :before_validation_on_update, :before_validation_on_save, :before_create, :after_create, :before_update, :after_update, :before_save, :after_save, :before_destroy, :after_destroy.
245
+
246
+ If you need access to the database in a callback: Couch Potato automatically assigns a database instance to the model before saving and when loading. It is available as _database_ accessor from within your model instance.
247
+
248
+ #### Testing
249
+
250
+ To make testing easier and faster database logic has been put into its own class, which you can replace and stub out in whatever way you want:
251
+
252
+ class User
253
+ include CouchPotato::Persistence
254
+ end
255
+
256
+ # RSpec
257
+ describe 'save a user' do
258
+ it 'should save' do
259
+ couchrest_db = stub 'couchrest_db',
260
+ database = CouchPotato::Database.new couchrest_db
261
+ user = User.new
262
+ couchrest_db.should_receive(:save_doc).with(...)
263
+ database.save_document user
264
+ end
265
+ end
266
+
267
+ By creating you own instances of CouchPotato::Database and passing them a fake CouchRest database instance you can completely disconnect your unit tests/spec from the database.
268
+
269
+ ### Helping out
270
+
271
+ Please fix bugs, add more specs, implement new features by forking the github repo at http://github.com/langalex/couch_potato.
272
+
273
+ You can run all the specs by calling 'rake spec_unit' and 'rake spec_functional' in the root folder of Couch Potato. The specs require a running CouchDB instance at http://localhost:5984
274
+
275
+ I will only accept patches that are covered by specs - sorry.
276
+
277
+ ### Contact
278
+
279
+ If you have any questions/suggestions etc. please contact me at alex at upstream-berlin.com or @langalex on twitter.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 2
3
+ :patch: 8
4
+ :major: 0
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,10 @@
1
+ class Date
2
+ def to_json(*a)
3
+ %("#{strftime("%Y/%m/%d")}")
4
+ end
5
+
6
+ def self.json_create string
7
+ return nil if string.nil?
8
+ Date.parse(string)
9
+ end
10
+ 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,19 @@
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
+ # Source
9
+ # http://github.com/rails/rails/blob/b600bf2cd728c90d50cc34456c944b2dfefe8c8d/activesupport/lib/active_support/inflector.rb
10
+ def underscore
11
+ gsub(/::/, '/').
12
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
13
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
14
+ tr("-", "_").
15
+ downcase
16
+ end
17
+ end
18
+
19
+ 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,11 @@
1
+ class Time
2
+ def to_json(*a)
3
+ %("#{strftime("%Y/%m/%d %H:%M:%S +0000")}")
4
+ end
5
+
6
+ def self.json_create string
7
+ return nil if string.nil?
8
+ d = DateTime.parse(string).new_offset
9
+ self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec)
10
+ end
11
+ end
@@ -0,0 +1,40 @@
1
+ require 'couchrest'
2
+ require 'json'
3
+ require 'json/add/core'
4
+ require 'json/add/rails'
5
+
6
+ require 'ostruct'
7
+
8
+
9
+ module CouchPotato
10
+ Config = OpenStruct.new
11
+
12
+ # 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.
13
+ def self.database
14
+ @@__database ||= Database.new(self.couchrest_database)
15
+ end
16
+
17
+ # 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.
18
+ def self.couchrest_database
19
+ @@__couchrest_database ||= CouchRest.database(full_url_to_database)
20
+ end
21
+
22
+ private
23
+
24
+ def self.full_url_to_database
25
+ raise('No Database configured. Set CouchPotato::Config.database_name') unless CouchPotato::Config.database_name
26
+ if CouchPotato::Config.database_server
27
+ return "#{CouchPotato::Config.database_server}#{CouchPotato::Config.database_name}"
28
+ else
29
+ return "http://127.0.0.1:5984/#{CouchPotato::Config.database_name}"
30
+ end
31
+ end
32
+ end
33
+
34
+ require File.dirname(__FILE__) + '/core_ext/object'
35
+ require File.dirname(__FILE__) + '/core_ext/time'
36
+ require File.dirname(__FILE__) + '/core_ext/date'
37
+ require File.dirname(__FILE__) + '/core_ext/string'
38
+ require File.dirname(__FILE__) + '/core_ext/symbol'
39
+ require File.dirname(__FILE__) + '/couch_potato/persistence'
40
+
@@ -0,0 +1,105 @@
1
+ module CouchPotato
2
+ class Database
3
+
4
+ class ValidationsFailedError < ::StandardError; end
5
+
6
+ def initialize(couchrest_database)
7
+ @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
+ def view(spec)
16
+ results = CouchPotato::View::ViewQuery.new(database,
17
+ spec.design_document, spec.view_name, spec.map_function,
18
+ spec.reduce_function).query_view!(spec.view_parameters)
19
+ spec.process_results results
20
+ end
21
+
22
+ def save_document(document)
23
+ return true unless document.dirty?
24
+ if document.new?
25
+ create_document document
26
+ else
27
+ update_document document
28
+ end
29
+ end
30
+ alias_method :save, :save_document
31
+
32
+ def save_document!(document)
33
+ save_document(document) || raise(ValidationsFailedError.new(document.errors.full_messages))
34
+ end
35
+ alias_method :save!, :save_document!
36
+
37
+ def destroy_document(document)
38
+ document.run_callbacks :before_destroy
39
+ document._deleted = true
40
+ database.delete_doc document.to_hash
41
+ document.run_callbacks :after_destroy
42
+ document._id = nil
43
+ document._rev = nil
44
+ end
45
+ alias_method :destroy, :destroy_document
46
+
47
+ def load_document(id)
48
+ raise "Can't load a document without an id (got nil)" if id.nil?
49
+ begin
50
+ json = database.get(id)
51
+ instance = Class.const_get(json['ruby_class']).json_create json
52
+ instance.database = self
53
+ instance
54
+ rescue(RestClient::ResourceNotFound)
55
+ nil
56
+ end
57
+ end
58
+ alias_method :load, :load_document
59
+
60
+ def inspect
61
+ "#<CouchPotato::Database>"
62
+ end
63
+
64
+ private
65
+
66
+ def clean_hash(hash)
67
+ hash.each do |k,v|
68
+ hash.delete k unless v
69
+ end
70
+ end
71
+
72
+ def create_document(document)
73
+ document.database = self
74
+ document.run_callbacks :before_validation_on_save
75
+ document.run_callbacks :before_validation_on_create
76
+ return unless document.valid?
77
+ document.run_callbacks :before_save
78
+ document.run_callbacks :before_create
79
+ res = database.save_doc clean_hash(document.to_hash)
80
+ document._rev = res['rev']
81
+ document._id = res['id']
82
+ document.run_callbacks :after_save
83
+ document.run_callbacks :after_create
84
+ true
85
+ end
86
+
87
+ def update_document(document)
88
+ document.run_callbacks :before_validation_on_save
89
+ document.run_callbacks :before_validation_on_update
90
+ return unless document.valid?
91
+ document.run_callbacks :before_save
92
+ document.run_callbacks :before_update
93
+ res = database.save_doc clean_hash(document.to_hash)
94
+ document._rev = res['rev']
95
+ document.run_callbacks :after_save
96
+ document.run_callbacks :after_update
97
+ true
98
+ end
99
+
100
+ def database
101
+ @database
102
+ end
103
+
104
+ end
105
+ end