couch_potato 0.2.12
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.md +15 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.md +295 -0
- data/VERSION.yml +4 -0
- data/init.rb +3 -0
- data/lib/core_ext/date.rb +10 -0
- data/lib/core_ext/object.rb +5 -0
- data/lib/core_ext/string.rb +19 -0
- data/lib/core_ext/symbol.rb +15 -0
- data/lib/core_ext/time.rb +11 -0
- data/lib/couch_potato.rb +40 -0
- data/lib/couch_potato/database.rb +106 -0
- data/lib/couch_potato/persistence.rb +98 -0
- data/lib/couch_potato/persistence/attachments.rb +31 -0
- data/lib/couch_potato/persistence/callbacks.rb +60 -0
- data/lib/couch_potato/persistence/dirty_attributes.rb +49 -0
- data/lib/couch_potato/persistence/ghost_attributes.rb +22 -0
- data/lib/couch_potato/persistence/json.rb +46 -0
- data/lib/couch_potato/persistence/magic_timestamps.rb +13 -0
- data/lib/couch_potato/persistence/properties.rb +52 -0
- data/lib/couch_potato/persistence/simple_property.rb +83 -0
- data/lib/couch_potato/persistence/validation.rb +18 -0
- data/lib/couch_potato/view/base_view_spec.rb +24 -0
- data/lib/couch_potato/view/custom_view_spec.rb +27 -0
- data/lib/couch_potato/view/custom_views.rb +44 -0
- data/lib/couch_potato/view/model_view_spec.rb +63 -0
- data/lib/couch_potato/view/properties_view_spec.rb +39 -0
- data/lib/couch_potato/view/raw_view_spec.rb +25 -0
- data/lib/couch_potato/view/view_query.rb +44 -0
- data/rails/init.rb +7 -0
- data/spec/attachments_spec.rb +23 -0
- data/spec/callbacks_spec.rb +271 -0
- data/spec/create_spec.rb +22 -0
- data/spec/custom_view_spec.rb +149 -0
- data/spec/default_property_spec.rb +34 -0
- data/spec/destroy_spec.rb +29 -0
- data/spec/fixtures/address.rb +9 -0
- data/spec/fixtures/person.rb +6 -0
- data/spec/property_spec.rb +101 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/unit/attributes_spec.rb +48 -0
- data/spec/unit/callbacks_spec.rb +33 -0
- data/spec/unit/couch_potato_spec.rb +20 -0
- data/spec/unit/create_spec.rb +58 -0
- data/spec/unit/customs_views_spec.rb +15 -0
- data/spec/unit/database_spec.rb +50 -0
- data/spec/unit/dirty_attributes_spec.rb +131 -0
- data/spec/unit/string_spec.rb +13 -0
- data/spec/unit/view_query_spec.rb +9 -0
- data/spec/update_spec.rb +40 -0
- metadata +153 -0
data/CHANGES.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
## Changes
|
2
|
+
|
3
|
+
### 0.2.10
|
4
|
+
* fixed bug with hardcoded timezone
|
5
|
+
|
6
|
+
### 0.2.9
|
7
|
+
|
8
|
+
* allow to overwrite attribute accessor of properties and use super to call the original accessors
|
9
|
+
* allow read access to attributes that are present in the Couchdb document but not defined as properties
|
10
|
+
* support default values for properties via the :default parameter
|
11
|
+
* support attachments via the _attachments property
|
12
|
+
* support for namespaces models
|
13
|
+
* removed belongs_to macro for now
|
14
|
+
* removed CouchPotato::Config.database_server, just set CouchPotato::Config.database_name to the full url if you are not using localhost:5984
|
15
|
+
* Ruby 1.9 was broken and is now working again
|
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,295 @@
|
|
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:
|
41
|
+
|
42
|
+
CouchPotato::Config.database_name = "http://example.com:5984/name_of_the_db"
|
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 and the [rdocs](http://rdoc.info/projects/langalex/couch_potato).
|
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 [CouchPotato::View::BaseViewSpec](http://rdoc.info/rdoc/langalex/couch_potato/blob/e8f0069e5529ad08a1bd1f02637ea8f1d6d0ab5b/CouchPotato/View/BaseViewSpec.html) and its descendants for more detailed documentation). 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
|
+
#### Attachments
|
249
|
+
|
250
|
+
There is basic attachment support: if you want to store any attachments set that _attachments attribute of a model before saving like this:
|
251
|
+
|
252
|
+
class User
|
253
|
+
include CouchPotato::Persistence
|
254
|
+
end
|
255
|
+
|
256
|
+
user = User.new
|
257
|
+
user._attachments = {'photo' => {'data' => '[image byte data]', 'content_type' => 'image/png'}}
|
258
|
+
|
259
|
+
When saving this object an attachment with the name _photo_ will be uploaded into CouchDB. It will be available under the url of the user object + _/photo_. When loading the user at a later time you still have access to the _content_type_ and additionally to the _length_ of the attachment:
|
260
|
+
|
261
|
+
user_reloaded = CouchPotato.database.load user.id
|
262
|
+
user_reloaded._attachments['photo'] # => {'content_type' => 'image/png', 'length' => 37861}
|
263
|
+
|
264
|
+
#### Testing
|
265
|
+
|
266
|
+
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:
|
267
|
+
|
268
|
+
class User
|
269
|
+
include CouchPotato::Persistence
|
270
|
+
end
|
271
|
+
|
272
|
+
# RSpec
|
273
|
+
describe 'save a user' do
|
274
|
+
it 'should save' do
|
275
|
+
couchrest_db = stub 'couchrest_db',
|
276
|
+
database = CouchPotato::Database.new couchrest_db
|
277
|
+
user = User.new
|
278
|
+
couchrest_db.should_receive(:save_doc).with(...)
|
279
|
+
database.save_document user
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
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.
|
284
|
+
|
285
|
+
### Helping out
|
286
|
+
|
287
|
+
Please fix bugs, add more specs, implement new features by forking the github repo at http://github.com/langalex/couch_potato.
|
288
|
+
|
289
|
+
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
|
290
|
+
|
291
|
+
I will only accept patches that are covered by specs - sorry.
|
292
|
+
|
293
|
+
### Contact
|
294
|
+
|
295
|
+
If you have any questions/suggestions etc. please contact me at alex at upstream-berlin.com or @langalex on twitter.
|
data/VERSION.yml
ADDED
data/init.rb
ADDED
@@ -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
|
data/lib/couch_potato.rb
ADDED
@@ -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 = Struct.new(:database_name).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_name[0,7] == 'http://'
|
27
|
+
CouchPotato::Config.database_name
|
28
|
+
else
|
29
|
+
"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
|
+
|