jchris-couchrest 0.9.9 → 0.9.10
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +19 -8
- data/Rakefile +1 -1
- data/lib/couchrest/core/database.rb +13 -0
- data/lib/couchrest/core/model.rb +295 -178
- data/spec/{couchrest_spec.rb → couchrest/core/couchrest_spec.rb} +1 -1
- data/spec/{database_spec.rb → couchrest/core/database_spec.rb} +17 -1
- data/spec/couchrest/core/model_spec.rb +213 -16
- data/spec/fixtures/attachments/couchdb.png +0 -0
- metadata +6 -9
data/README.rdoc
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
== CouchRest - CouchDB, close to the metal
|
2
2
|
|
3
|
-
CouchRest is based on [CouchDB's couch.js test
|
3
|
+
CouchRest is based on [CouchDB's couch.js test
|
4
|
+
library](http://svn.apache.org/repos/asf/incubator/couchdb/trunk/share/www/script/couch.js),
|
5
|
+
which I find to be concise, clear, and well designed. CouchRest lightly wraps
|
6
|
+
CouchDB's HTTP API, managing JSON serialization, and remembering the URI-paths
|
7
|
+
to CouchDB's API endpoints so you don't have to.
|
4
8
|
|
5
|
-
CouchRest's lighweight is designed to make a simple base for application and
|
9
|
+
CouchRest's lighweight is designed to make a simple base for application and
|
10
|
+
framework-specific object oriented APIs.
|
6
11
|
|
7
12
|
=== Easy Install
|
8
13
|
|
@@ -10,11 +15,16 @@ CouchRest's lighweight is designed to make a simple base for application and fra
|
|
10
15
|
|
11
16
|
=== Relax, it's RESTful
|
12
17
|
|
13
|
-
The core of Couchrest is Heroku’s excellent REST Client Ruby HTTP wrapper.
|
18
|
+
The core of Couchrest is Heroku’s excellent REST Client Ruby HTTP wrapper.
|
19
|
+
REST Client takes all the nastyness of Net::HTTP and gives is a pretty face,
|
20
|
+
while still giving you more control than Open-URI. I recommend it anytime
|
21
|
+
you’re interfacing with a well-defined web service.
|
14
22
|
|
15
23
|
=== Running the Specs
|
16
24
|
|
17
|
-
The most complete documentation is the spec/ directory. To validate your
|
25
|
+
The most complete documentation is the spec/ directory. To validate your
|
26
|
+
CouchRest install, from the project root directory run `rake`, or `autotest`
|
27
|
+
(requires RSpec and optionally ZenTest for autotest support).
|
18
28
|
|
19
29
|
=== Examples
|
20
30
|
|
@@ -50,7 +60,8 @@ Creating and Querying Views:
|
|
50
60
|
|
51
61
|
== CouchRest::Model
|
52
62
|
|
53
|
-
CouchRest::Model is a module designed along the lines of DataMapper::Resource.
|
54
|
-
|
55
|
-
|
56
|
-
standard SQL alternatives. See the CouchRest::Model documentation for
|
63
|
+
CouchRest::Model is a module designed along the lines of DataMapper::Resource.
|
64
|
+
By subclassing, suddenly you get all sorts of powerful sugar, so that working
|
65
|
+
with CouchDB in your Rails or Merb app is no harder than working with the
|
66
|
+
standard SQL alternatives. See the CouchRest::Model documentation for an
|
67
|
+
example article class that illustrates usage.
|
data/Rakefile
CHANGED
@@ -59,6 +59,19 @@ module CouchRest
|
|
59
59
|
RestClient.get "#{@root}/#{doc}/#{name}"
|
60
60
|
end
|
61
61
|
|
62
|
+
# PUT an attachment directly to CouchDB
|
63
|
+
def put_attachment doc, name, file, options = {}
|
64
|
+
docid = CGI.escape(doc['_id'])
|
65
|
+
name = CGI.escape(name)
|
66
|
+
uri = if doc['_rev']
|
67
|
+
"#{@root}/#{docid}/#{name}?rev=#{doc['_rev']}"
|
68
|
+
else
|
69
|
+
"#{@root}/#{docid}/#{name}"
|
70
|
+
end
|
71
|
+
|
72
|
+
JSON.parse(RestClient.put(uri, file, options))
|
73
|
+
end
|
74
|
+
|
62
75
|
# Save a document to CouchDB. This will use the <tt>_id</tt> field from the document as the id for PUT, or request a new UUID from CouchDB, if no <tt>_id</tt> is present on the document. IDs are attached to documents on the client side because POST has the curious property of being automatically retried by proxies in the event of network segmentation and lost responses.
|
63
76
|
def save doc
|
64
77
|
if doc['_attachments']
|
data/lib/couchrest/core/model.rb
CHANGED
@@ -1,15 +1,23 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'extlib'
|
3
|
+
require 'digest/md5'
|
4
|
+
|
1
5
|
# = CouchRest::Model - ORM, the CouchDB way
|
2
6
|
module CouchRest
|
3
7
|
# = CouchRest::Model - ORM, the CouchDB way
|
4
|
-
#
|
5
|
-
# CouchRest::Model provides an ORM-like interface for CouchDB documents. It
|
6
|
-
#
|
8
|
+
#
|
9
|
+
# CouchRest::Model provides an ORM-like interface for CouchDB documents. It
|
10
|
+
# avoids all usage of <tt>method_missing</tt>, and tries to strike a balance
|
11
|
+
# between usability and magic. See CouchRest::Model#view_by for
|
12
|
+
# documentation about the view-generation system.
|
13
|
+
#
|
7
14
|
# ==== Example
|
8
|
-
#
|
9
|
-
# This is an example class using CouchRest::Model. It is taken from the
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
15
|
+
#
|
16
|
+
# This is an example class using CouchRest::Model. It is taken from the
|
17
|
+
# spec/couchrest/core/model_spec.rb file, which may be even more up to date
|
18
|
+
# than this example.
|
19
|
+
#
|
20
|
+
# class Article < CouchRest::Model
|
13
21
|
# use_database CouchRest.database!('http://localhost:5984/couchrest-model-test')
|
14
22
|
# unique_id :slug
|
15
23
|
#
|
@@ -19,7 +27,7 @@ module CouchRest
|
|
19
27
|
# view_by :tags,
|
20
28
|
# :map =>
|
21
29
|
# "function(doc) {
|
22
|
-
# if (doc
|
30
|
+
# if (doc['couchrest-type'] == 'Article' && doc.tags) {
|
23
31
|
# doc.tags.forEach(function(tag){
|
24
32
|
# emit(tag, 1);
|
25
33
|
# });
|
@@ -38,169 +46,163 @@ module CouchRest
|
|
38
46
|
#
|
39
47
|
# before(:create, :generate_slug_from_title)
|
40
48
|
# def generate_slug_from_title
|
41
|
-
#
|
49
|
+
# self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
|
42
50
|
# end
|
43
51
|
# end
|
44
|
-
|
45
|
-
class << self
|
46
|
-
# this is the CouchRest::Database that model classes will use unless they override it with <tt>use_database</tt>
|
47
|
-
attr_accessor :default_database
|
48
|
-
end
|
49
|
-
|
50
|
-
# instance methods on the model classes
|
51
|
-
module InstanceMethods
|
52
|
-
attr_accessor :doc
|
53
|
-
|
54
|
-
def initialize keys = {}
|
55
|
-
self.doc = {}
|
56
|
-
keys.each do |k,v|
|
57
|
-
doc[k.to_s] = v
|
58
|
-
end
|
59
|
-
unless doc['_id'] && doc['_rev']
|
60
|
-
init_doc
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# returns the database used by this model's class
|
65
|
-
def database
|
66
|
-
self.class.database
|
67
|
-
end
|
68
|
-
|
69
|
-
# alias for doc['_id']
|
70
|
-
def id
|
71
|
-
doc['_id']
|
72
|
-
end
|
52
|
+
class Model < Hash
|
73
53
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
def new_record?
|
81
|
-
!doc['_rev']
|
54
|
+
# instantiates the hash by converting all the keys to strings.
|
55
|
+
def initialize keys = {}
|
56
|
+
super()
|
57
|
+
apply_defaults
|
58
|
+
keys.each do |k,v|
|
59
|
+
self[k.to_s] = v
|
82
60
|
end
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
if new_record?
|
87
|
-
create
|
88
|
-
else
|
89
|
-
update
|
90
|
-
end
|
61
|
+
cast_keys
|
62
|
+
unless self['_id'] && self['_rev']
|
63
|
+
self['couchrest-type'] = self.class.to_s
|
91
64
|
end
|
65
|
+
end
|
92
66
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
def save_doc
|
107
|
-
result = database.save doc
|
108
|
-
if result['ok']
|
109
|
-
doc['_id'] = result['id']
|
110
|
-
doc['_rev'] = result['rev']
|
111
|
-
end
|
112
|
-
result['ok']
|
113
|
-
end
|
114
|
-
|
115
|
-
def init_doc
|
116
|
-
doc['type'] = self.class.to_s
|
117
|
-
end
|
118
|
-
end # module InstanceMethods
|
119
|
-
|
120
|
-
# Class methods for models that include CouchRest::Model
|
121
|
-
module ClassMethods
|
67
|
+
# this is the CouchRest::Database that model classes will use unless
|
68
|
+
# they override it with <tt>use_database</tt>
|
69
|
+
cattr_accessor :default_database
|
70
|
+
|
71
|
+
class_inheritable_accessor :casts
|
72
|
+
class_inheritable_accessor :default_obj
|
73
|
+
class_inheritable_accessor :class_database
|
74
|
+
class_inheritable_accessor :generated_design_doc
|
75
|
+
class_inheritable_accessor :design_doc_slug_cache
|
76
|
+
class_inheritable_accessor :design_doc_fresh
|
77
|
+
|
78
|
+
class << self
|
122
79
|
# override the CouchRest::Model-wide default_database
|
123
80
|
def use_database db
|
124
|
-
|
81
|
+
self.class_database = db
|
125
82
|
end
|
126
|
-
|
83
|
+
|
127
84
|
# returns the CouchRest::Database instance that this class uses
|
128
85
|
def database
|
129
|
-
|
86
|
+
self.class_database || CouchRest::Model.default_database
|
130
87
|
end
|
131
|
-
|
88
|
+
|
132
89
|
# load a document from the database
|
133
90
|
def get id
|
134
91
|
doc = database.get id
|
135
92
|
new(doc)
|
136
93
|
end
|
94
|
+
|
95
|
+
def all opts = {}
|
96
|
+
self.generated_design_doc ||= default_design_doc
|
97
|
+
unless design_doc_fresh
|
98
|
+
refresh_design_doc
|
99
|
+
end
|
100
|
+
view_name = "#{design_doc_slug}/all"
|
101
|
+
raw = opts.delete(:raw)
|
102
|
+
view = fetch_view(view_name, opts)
|
103
|
+
process_view_results view, raw
|
104
|
+
end
|
137
105
|
|
138
|
-
#
|
106
|
+
# Cast a field as another class. The class must be happy to have the
|
107
|
+
# field's primitive type as the argument to it's constucture. Classes
|
108
|
+
# which inherit from CouchRest::Model are happy to act as sub-objects
|
109
|
+
# for any fields that are stored in JSON as object (and therefore are
|
110
|
+
# parsed from the JSON as Ruby Hashes).
|
111
|
+
def cast field, opts = {}
|
112
|
+
self.casts ||= {}
|
113
|
+
self.casts[field.to_s] = opts
|
114
|
+
end
|
115
|
+
|
116
|
+
# Defines methods for reading and writing from fields in the document.
|
117
|
+
# Uses key_writer and key_reader internally.
|
139
118
|
def key_accessor *keys
|
140
119
|
key_writer *keys
|
141
120
|
key_reader *keys
|
142
121
|
end
|
143
|
-
|
144
|
-
# For each argument key, define a method <tt>key=</tt> that sets the
|
122
|
+
|
123
|
+
# For each argument key, define a method <tt>key=</tt> that sets the
|
124
|
+
# corresponding field on the CouchDB document.
|
145
125
|
def key_writer *keys
|
146
126
|
keys.each do |method|
|
147
127
|
key = method.to_s
|
148
128
|
define_method "#{method}=" do |value|
|
149
|
-
|
129
|
+
self[key] = value
|
150
130
|
end
|
151
131
|
end
|
152
132
|
end
|
153
133
|
|
154
|
-
# For each argument key, define a method <tt>key</tt> that reads the
|
134
|
+
# For each argument key, define a method <tt>key</tt> that reads the
|
135
|
+
# corresponding field on the CouchDB document.
|
155
136
|
def key_reader *keys
|
156
137
|
keys.each do |method|
|
157
138
|
key = method.to_s
|
158
139
|
define_method method do
|
159
|
-
|
140
|
+
self[key]
|
160
141
|
end
|
161
142
|
end
|
162
143
|
end
|
144
|
+
|
145
|
+
def default
|
146
|
+
self.default_obj
|
147
|
+
end
|
163
148
|
|
164
|
-
|
149
|
+
def set_default hash
|
150
|
+
self.default_obj = hash
|
151
|
+
end
|
152
|
+
|
153
|
+
# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
|
154
|
+
# on the document whenever saving occurs. CouchRest uses a pretty
|
155
|
+
# decent time format by default. See Time#to_json
|
165
156
|
def timestamps!
|
166
157
|
before(:create) do
|
167
|
-
|
158
|
+
self['updated_at'] = self['created_at'] = Time.now
|
168
159
|
end
|
169
160
|
before(:update) do
|
170
|
-
|
161
|
+
self['updated_at'] = Time.now
|
171
162
|
end
|
172
163
|
end
|
173
|
-
|
174
|
-
# Name a method that will be called before the document is first saved,
|
175
|
-
|
176
|
-
|
177
|
-
|
164
|
+
|
165
|
+
# Name a method that will be called before the document is first saved,
|
166
|
+
# which returns a string to be used for the document's <tt>_id</tt>.
|
167
|
+
# Because CouchDB enforces a constraint that each id must be unique,
|
168
|
+
# this can be used to enforce eg: uniq usernames. Note that this id
|
169
|
+
# must be globally unique across all document types which share a
|
170
|
+
# database, so if you'd like to scope uniqueness to this class, you
|
171
|
+
# should use the class name as part of the unique id.
|
172
|
+
def unique_id method = nil, &block
|
173
|
+
if method
|
174
|
+
define_method :set_unique_id do
|
175
|
+
self['_id'] ||= self.send(method)
|
176
|
+
end
|
177
|
+
elsif block
|
178
|
+
define_method :set_unique_id do
|
179
|
+
uniqid = block.call(self)
|
180
|
+
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
|
181
|
+
self['_id'] ||= uniqid
|
182
|
+
end
|
178
183
|
end
|
179
184
|
end
|
180
|
-
|
181
|
-
end # module ClassMethods
|
182
185
|
|
183
|
-
|
184
|
-
|
185
|
-
#
|
186
|
-
#
|
186
|
+
# Define a CouchDB view. The name of the view will be the concatenation
|
187
|
+
# of <tt>by</tt> and the keys joined by <tt>_and_</tt>
|
188
|
+
#
|
187
189
|
# ==== Example views:
|
188
|
-
#
|
190
|
+
#
|
189
191
|
# class Post
|
190
192
|
# # view with default options
|
191
193
|
# # query with Post.by_date
|
192
194
|
# view_by :date, :descending => true
|
193
|
-
#
|
195
|
+
#
|
194
196
|
# # view with compound sort-keys
|
195
197
|
# # query with Post.by_user_id_and_date
|
196
198
|
# view_by :user_id, :date
|
197
|
-
#
|
199
|
+
#
|
198
200
|
# # view with custom map/reduce functions
|
199
201
|
# # query with Post.by_tags :reduce => true
|
200
202
|
# view_by :tags,
|
201
203
|
# :map =>
|
202
204
|
# "function(doc) {
|
203
|
-
# if (doc
|
205
|
+
# if (doc['couchrest-type'] == 'Post' && doc.tags) {
|
204
206
|
# doc.tags.forEach(function(tag){
|
205
207
|
# emit(doc.tag, 1);
|
206
208
|
# });
|
@@ -211,32 +213,46 @@ module CouchRest
|
|
211
213
|
# return sum(values);
|
212
214
|
# }"
|
213
215
|
# end
|
214
|
-
#
|
215
|
-
# <tt>view_by :date</tt> will create a view defined by this Javascript
|
216
|
-
#
|
216
|
+
#
|
217
|
+
# <tt>view_by :date</tt> will create a view defined by this Javascript
|
218
|
+
# function:
|
219
|
+
#
|
217
220
|
# function(doc) {
|
218
|
-
# if (doc
|
221
|
+
# if (doc['couchrest-type'] == 'Post' && doc.date) {
|
219
222
|
# emit(doc.date, null);
|
220
223
|
# }
|
221
224
|
# }
|
222
|
-
#
|
223
|
-
# It can be queried by calling <tt>Post.by_date</tt> which accepts all
|
224
|
-
#
|
225
|
-
#
|
226
|
-
#
|
227
|
-
#
|
228
|
-
#
|
229
|
-
#
|
230
|
-
#
|
231
|
-
#
|
225
|
+
#
|
226
|
+
# It can be queried by calling <tt>Post.by_date</tt> which accepts all
|
227
|
+
# valid options for CouchRest::Database#view. In addition, calling with
|
228
|
+
# the <tt>:raw => true</tt> option will return the view rows
|
229
|
+
# themselves. By default <tt>Post.by_date</tt> will return the
|
230
|
+
# documents included in the generated view.
|
231
|
+
#
|
232
|
+
# CouchRest::Database#view options can be applied at view definition
|
233
|
+
# time as defaults, and they will be curried and used at view query
|
234
|
+
# time. Or they can be overridden at query time.
|
235
|
+
#
|
236
|
+
# Custom views can be queried with <tt>:reduce => true</tt> to return
|
237
|
+
# reduce results. The default for custom views is to query with
|
238
|
+
# <tt>:reduce => false</tt>.
|
239
|
+
#
|
240
|
+
# Views are generated (on a per-model basis) lazily on first-access.
|
241
|
+
# This means that if you are deploying changes to a view, the views for
|
242
|
+
# that model won't be available until generation is complete. This can
|
243
|
+
# take some time with large databases. Strategies are in the works.
|
244
|
+
#
|
245
|
+
# To understand the capabilities of this view system more compeletly,
|
246
|
+
# it is recommended that you read the RSpec file at
|
247
|
+
# <tt>spec/core/model.rb</tt>.
|
232
248
|
def view_by *keys
|
233
249
|
opts = keys.pop if keys.last.is_a?(Hash)
|
234
250
|
opts ||= {}
|
235
251
|
type = self.to_s
|
236
252
|
|
237
253
|
method_name = "by_#{keys.join('_and_')}"
|
238
|
-
|
239
|
-
|
254
|
+
self.generated_design_doc ||= default_design_doc
|
255
|
+
|
240
256
|
if opts[:map]
|
241
257
|
view = {}
|
242
258
|
view['map'] = opts.delete(:map)
|
@@ -244,53 +260,61 @@ module CouchRest
|
|
244
260
|
view['reduce'] = opts.delete(:reduce)
|
245
261
|
opts[:reduce] = false
|
246
262
|
end
|
247
|
-
|
263
|
+
generated_design_doc['views'][method_name] = view
|
248
264
|
else
|
249
265
|
doc_keys = keys.collect{|k|"doc['#{k}']"}
|
250
266
|
key_protection = doc_keys.join(' && ')
|
251
267
|
key_emit = doc_keys.length == 1 ? "#{doc_keys.first}" : "[#{doc_keys.join(', ')}]"
|
252
268
|
map_function = <<-JAVASCRIPT
|
253
269
|
function(doc) {
|
254
|
-
if (doc
|
270
|
+
if (doc['couchrest-type'] == '#{type}' && #{key_protection}) {
|
255
271
|
emit(#{key_emit}, null);
|
256
272
|
}
|
257
273
|
}
|
258
274
|
JAVASCRIPT
|
259
|
-
|
275
|
+
generated_design_doc['views'][method_name] = {
|
260
276
|
'map' => map_function
|
261
277
|
}
|
262
278
|
end
|
263
|
-
|
264
|
-
|
265
|
-
|
279
|
+
|
280
|
+
self.design_doc_fresh = false
|
281
|
+
|
266
282
|
self.meta_class.instance_eval do
|
267
283
|
define_method method_name do |*args|
|
268
284
|
query = opts.merge(args[0] || {})
|
269
285
|
query[:raw] = true if query[:reduce]
|
270
|
-
unless
|
286
|
+
unless design_doc_fresh
|
271
287
|
refresh_design_doc
|
272
288
|
end
|
273
289
|
raw = query.delete(:raw)
|
274
|
-
view_name = "#{
|
275
|
-
|
290
|
+
view_name = "#{design_doc_slug}/#{method_name}"
|
276
291
|
view = fetch_view(view_name, query)
|
277
|
-
|
278
|
-
view
|
279
|
-
else
|
280
|
-
# TODO this can be optimized once the include-docs patch is applied
|
281
|
-
view['rows'].collect{|r|new(database.get(r['id']))}
|
282
|
-
end
|
292
|
+
process_view_results view, raw
|
283
293
|
end
|
284
294
|
end
|
285
295
|
end
|
286
|
-
|
296
|
+
|
297
|
+
# Fetch the generated design doc. Could raise an error if the generated views have not been queried yet.
|
298
|
+
def design_doc
|
299
|
+
database.get("_design/#{design_doc_slug}")
|
300
|
+
end
|
301
|
+
|
287
302
|
private
|
288
|
-
|
303
|
+
|
304
|
+
def process_view_results view, raw=false
|
305
|
+
if raw
|
306
|
+
view
|
307
|
+
else
|
308
|
+
# TODO this can be optimized once the include-docs patch is applied
|
309
|
+
view['rows'].collect{|r|new(database.get(r['id']))}
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
289
313
|
def fetch_view view_name, opts
|
290
314
|
retryable = true
|
291
315
|
begin
|
292
316
|
database.view(view_name, opts)
|
293
|
-
|
317
|
+
# the design doc could have been deleted by a rouge process
|
294
318
|
rescue RestClient::ResourceNotFound => e
|
295
319
|
if retryable
|
296
320
|
refresh_design_doc
|
@@ -301,52 +325,145 @@ module CouchRest
|
|
301
325
|
end
|
302
326
|
end
|
303
327
|
end
|
304
|
-
|
305
|
-
def
|
306
|
-
|
328
|
+
|
329
|
+
def design_doc_slug
|
330
|
+
return design_doc_slug_cache if design_doc_slug_cache && design_doc_fresh
|
331
|
+
funcs = []
|
332
|
+
generated_design_doc['views'].each do |name, view|
|
333
|
+
funcs << "#{name}/#{view['map']}#{view['reduce']}"
|
334
|
+
end
|
335
|
+
md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
|
336
|
+
self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
|
307
337
|
end
|
308
|
-
|
338
|
+
|
309
339
|
def default_design_doc
|
310
340
|
{
|
311
|
-
"_id" => design_doc_id,
|
312
341
|
"language" => "javascript",
|
313
|
-
"views" => {
|
342
|
+
"views" => {
|
343
|
+
'all' => {
|
344
|
+
'map' => "function(doc) {
|
345
|
+
if (doc['couchrest-type'] == '#{self.to_s}') {
|
346
|
+
emit(null,null);
|
347
|
+
}
|
348
|
+
}"
|
349
|
+
}
|
350
|
+
}
|
314
351
|
}
|
315
352
|
end
|
316
|
-
|
353
|
+
|
317
354
|
def refresh_design_doc
|
318
|
-
|
355
|
+
did = "_design/#{design_doc_slug}"
|
356
|
+
saved = database.get(did) rescue nil
|
319
357
|
if saved
|
320
|
-
|
358
|
+
generated_design_doc['views'].each do |name, view|
|
321
359
|
saved['views'][name] = view
|
322
360
|
end
|
323
361
|
database.save(saved)
|
324
362
|
else
|
325
|
-
|
363
|
+
generated_design_doc['_id'] = did
|
364
|
+
database.save(generated_design_doc)
|
326
365
|
end
|
327
|
-
|
366
|
+
self.design_doc_fresh = true
|
328
367
|
end
|
329
|
-
|
330
|
-
end #
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
368
|
+
|
369
|
+
end # class << self
|
370
|
+
|
371
|
+
# returns the database used by this model's class
|
372
|
+
def database
|
373
|
+
self.class.database
|
374
|
+
end
|
375
|
+
|
376
|
+
# alias for self['_id']
|
377
|
+
def id
|
378
|
+
self['_id']
|
379
|
+
end
|
380
|
+
|
381
|
+
# alias for self['_rev']
|
382
|
+
def rev
|
383
|
+
self['_rev']
|
384
|
+
end
|
385
|
+
|
386
|
+
# returns true if the document has never been saved
|
387
|
+
def new_record?
|
388
|
+
!rev
|
389
|
+
end
|
390
|
+
|
391
|
+
# Saves the document to the db using create or update. Also runs the :save
|
392
|
+
# callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on
|
393
|
+
# CouchDB's response.
|
394
|
+
def save
|
395
|
+
if new_record?
|
396
|
+
create
|
397
|
+
else
|
398
|
+
update
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Deletes the document from the database. Runs the :delete callbacks.
|
403
|
+
# Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
|
404
|
+
# document to be saved to a new <tt>_id</tt>.
|
405
|
+
def destroy
|
406
|
+
result = database.delete self
|
407
|
+
if result['ok']
|
408
|
+
self['_rev'] = nil
|
409
|
+
self['_id'] = nil
|
410
|
+
end
|
411
|
+
result['ok']
|
412
|
+
end
|
413
|
+
|
414
|
+
protected
|
415
|
+
|
416
|
+
# Saves a document for the first time, after running the before(:create)
|
417
|
+
# callbacks, and applying the unique_id.
|
418
|
+
def create
|
419
|
+
set_unique_id if respond_to?(:set_unique_id) # hack
|
420
|
+
save_doc
|
421
|
+
end
|
422
|
+
|
423
|
+
# Saves the document and runs the :update callbacks.
|
424
|
+
def update
|
425
|
+
save_doc
|
426
|
+
end
|
427
|
+
|
428
|
+
private
|
429
|
+
|
430
|
+
def save_doc
|
431
|
+
result = database.save self
|
432
|
+
if result['ok']
|
433
|
+
self['_id'] = result['id']
|
434
|
+
self['_rev'] = result['rev']
|
435
|
+
end
|
436
|
+
result['ok']
|
437
|
+
end
|
438
|
+
|
439
|
+
def apply_defaults
|
440
|
+
if self.class.default
|
441
|
+
self.class.default.each do |k,v|
|
442
|
+
self[k.to_s] = v
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
def cast_keys
|
448
|
+
return unless self.class.casts
|
449
|
+
# TODO move the argument checking to the cast method for early crashes
|
450
|
+
self.class.casts.each do |k,v|
|
451
|
+
next unless self[k]
|
452
|
+
target = v[:as]
|
453
|
+
if target.is_a?(Array) && target[0].is_a?(Class)
|
454
|
+
self[k] = self[k].collect do |value|
|
455
|
+
target[0].new(value)
|
456
|
+
end
|
457
|
+
elsif target.is_a?(Class)
|
458
|
+
self[k] = target.new(self[k])
|
459
|
+
else
|
460
|
+
raise ArgumentError, "Call like - cast :field, :as => MyClass - or - :as => [MyClass] if the field is an array."
|
461
|
+
end
|
338
462
|
end
|
339
|
-
end # module Callbacks
|
340
|
-
|
341
|
-
# bookkeeping section
|
342
|
-
|
343
|
-
# load the code into the model class
|
344
|
-
def self.included(model)
|
345
|
-
model.send(:include, InstanceMethods)
|
346
|
-
model.extend ClassMethods
|
347
|
-
model.extend MagicViews
|
348
|
-
model.send(:include, Callbacks)
|
349
463
|
end
|
350
|
-
|
351
|
-
|
464
|
+
|
465
|
+
include ::Extlib::Hook
|
466
|
+
register_instance_hooks :save, :create, :update, :destroy
|
467
|
+
|
468
|
+
end # class Model
|
352
469
|
end # module CouchRest
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require File.dirname(__FILE__) + '
|
1
|
+
require File.dirname(__FILE__) + '/../../spec_helper'
|
2
2
|
|
3
3
|
describe CouchRest::Database do
|
4
4
|
before(:each) do
|
@@ -205,6 +205,22 @@ describe CouchRest::Database do
|
|
205
205
|
|
206
206
|
end
|
207
207
|
|
208
|
+
describe "PUT attachment from file" do
|
209
|
+
before(:each) do
|
210
|
+
filename = File.dirname(__FILE__) + '/../../fixtures/attachments/couchdb.png'
|
211
|
+
@file = File.open(filename)
|
212
|
+
end
|
213
|
+
after(:each) do
|
214
|
+
@file.close
|
215
|
+
end
|
216
|
+
it "should save the attachment to a new doc" do
|
217
|
+
r = @db.put_attachment({'_id' => 'attach-this'}, 'couchdb.png', image = @file.read, {:content_type => 'image/png'})
|
218
|
+
r['ok'].should == true
|
219
|
+
attachment = @db.fetch_attachment("attach-this","couchdb.png")
|
220
|
+
attachment.should == image
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
208
224
|
describe "PUT document with attachment" do
|
209
225
|
before(:each) do
|
210
226
|
@attach = "<html><head><title>My Doc</title></head><body><p>Has words.</p></body></html>"
|
@@ -1,11 +1,38 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/../../spec_helper'
|
2
2
|
|
3
|
-
class Basic
|
4
|
-
include CouchRest::Model
|
3
|
+
class Basic < CouchRest::Model
|
5
4
|
end
|
6
5
|
|
7
|
-
class
|
8
|
-
|
6
|
+
class WithTemplate < CouchRest::Model
|
7
|
+
unique_id do |model|
|
8
|
+
model['important-field']
|
9
|
+
end
|
10
|
+
set_default({
|
11
|
+
:preset => 'value',
|
12
|
+
'more-template' => [1,2,3]
|
13
|
+
})
|
14
|
+
key_accessor :preset
|
15
|
+
end
|
16
|
+
|
17
|
+
class Question < CouchRest::Model
|
18
|
+
key_accessor :q, :a
|
19
|
+
end
|
20
|
+
|
21
|
+
class Person < CouchRest::Model
|
22
|
+
key_accessor :name
|
23
|
+
def last_name
|
24
|
+
name.last
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Course < CouchRest::Model
|
29
|
+
key_accessor :title
|
30
|
+
cast :questions, :as => [Question]
|
31
|
+
cast :professor, :as => Person
|
32
|
+
view_by :title
|
33
|
+
end
|
34
|
+
|
35
|
+
class Article < CouchRest::Model
|
9
36
|
use_database CouchRest.database!('http://localhost:5984/couchrest-model-test')
|
10
37
|
unique_id :slug
|
11
38
|
|
@@ -15,7 +42,7 @@ class Article
|
|
15
42
|
view_by :tags,
|
16
43
|
:map =>
|
17
44
|
"function(doc) {
|
18
|
-
if (doc
|
45
|
+
if (doc['couchrest-type'] == 'Article' && doc.tags) {
|
19
46
|
doc.tags.forEach(function(tag){
|
20
47
|
emit(tag, 1);
|
21
48
|
});
|
@@ -34,7 +61,7 @@ class Article
|
|
34
61
|
|
35
62
|
before(:create, :generate_slug_from_title)
|
36
63
|
def generate_slug_from_title
|
37
|
-
|
64
|
+
self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
|
38
65
|
end
|
39
66
|
end
|
40
67
|
|
@@ -61,6 +88,7 @@ describe CouchRest::Model do
|
|
61
88
|
describe "a new model" do
|
62
89
|
it "should be a new_record" do
|
63
90
|
@obj = Basic.new
|
91
|
+
@obj.rev.should be_nil
|
64
92
|
@obj.should be_a_new_record
|
65
93
|
end
|
66
94
|
end
|
@@ -68,13 +96,13 @@ describe CouchRest::Model do
|
|
68
96
|
describe "a model with key_accessors" do
|
69
97
|
it "should allow reading keys" do
|
70
98
|
@art = Article.new
|
71
|
-
@art
|
99
|
+
@art['title'] = 'My Article Title'
|
72
100
|
@art.title.should == 'My Article Title'
|
73
101
|
end
|
74
102
|
it "should allow setting keys" do
|
75
103
|
@art = Article.new
|
76
104
|
@art.title = 'My Article Title'
|
77
|
-
@art
|
105
|
+
@art['title'].should == 'My Article Title'
|
78
106
|
end
|
79
107
|
end
|
80
108
|
|
@@ -83,7 +111,7 @@ describe CouchRest::Model do
|
|
83
111
|
@art = Article.new
|
84
112
|
t = Time.now
|
85
113
|
@art.date = t
|
86
|
-
@art
|
114
|
+
@art['date'].should == t
|
87
115
|
end
|
88
116
|
it "should not allow reading keys" do
|
89
117
|
@art = Article.new
|
@@ -96,7 +124,7 @@ describe CouchRest::Model do
|
|
96
124
|
describe "a model with key_readers" do
|
97
125
|
it "should allow reading keys" do
|
98
126
|
@art = Article.new
|
99
|
-
@art
|
127
|
+
@art['slug'] = 'my-slug'
|
100
128
|
@art.slug.should == 'my-slug'
|
101
129
|
end
|
102
130
|
it "should not allow setting keys" do
|
@@ -105,6 +133,15 @@ describe CouchRest::Model do
|
|
105
133
|
end
|
106
134
|
end
|
107
135
|
|
136
|
+
describe "a model with template values" do
|
137
|
+
before(:all) do
|
138
|
+
@tmpl = WithTemplate.new
|
139
|
+
end
|
140
|
+
it "should have fields set when new" do
|
141
|
+
@tmpl.preset.should == 'value'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
108
145
|
describe "getting a model" do
|
109
146
|
before(:all) do
|
110
147
|
@art = Article.new(:title => 'All About Getting')
|
@@ -116,6 +153,68 @@ describe CouchRest::Model do
|
|
116
153
|
end
|
117
154
|
end
|
118
155
|
|
156
|
+
describe "getting a model with a subobjects array" do
|
157
|
+
before(:all) do
|
158
|
+
course_doc = {
|
159
|
+
"title" => "Metaphysics 200",
|
160
|
+
"questions" => [
|
161
|
+
{
|
162
|
+
"q" => "Carve the ___ of reality at the ___.",
|
163
|
+
"a" => ["beast","joints"]
|
164
|
+
},{
|
165
|
+
"q" => "Who layed the smack down on Leibniz's Law?",
|
166
|
+
"a" => "Willard Van Orman Quine"
|
167
|
+
}
|
168
|
+
]
|
169
|
+
}
|
170
|
+
r = Course.database.save course_doc
|
171
|
+
@course = Course.get r['id']
|
172
|
+
end
|
173
|
+
it "should load the course" do
|
174
|
+
@course.title.should == "Metaphysics 200"
|
175
|
+
end
|
176
|
+
it "should instantiate them as such" do
|
177
|
+
@course["questions"][0].a[0].should == "beast"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe "finding all instances of a model" do
|
182
|
+
before(:all) do
|
183
|
+
WithTemplate.new('important-field' => '1').save
|
184
|
+
WithTemplate.new('important-field' => '2').save
|
185
|
+
WithTemplate.new('important-field' => '3').save
|
186
|
+
WithTemplate.new('important-field' => '4').save
|
187
|
+
end
|
188
|
+
it "should make the design doc" do
|
189
|
+
WithTemplate.all
|
190
|
+
d = WithTemplate.design_doc
|
191
|
+
d['views']['all']['map'].should include('WithTemplate')
|
192
|
+
end
|
193
|
+
it "should find all" do
|
194
|
+
rs = WithTemplate.all
|
195
|
+
rs.length.should == 4
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
describe "getting a model with a subobject field" do
|
200
|
+
before(:all) do
|
201
|
+
course_doc = {
|
202
|
+
"title" => "Metaphysics 410",
|
203
|
+
"professor" => {
|
204
|
+
"name" => ["Mark", "Hinchliff"]
|
205
|
+
}
|
206
|
+
}
|
207
|
+
r = Course.database.save course_doc
|
208
|
+
@course = Course.get r['id']
|
209
|
+
end
|
210
|
+
it "should load the course" do
|
211
|
+
@course["professor"]["name"][1].should == "Hinchliff"
|
212
|
+
end
|
213
|
+
it "should instantiate the professor as a person" do
|
214
|
+
@course['professor'].last_name.should == "Hinchliff"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
119
218
|
describe "saving a model" do
|
120
219
|
before(:all) do
|
121
220
|
@obj = Basic.new
|
@@ -129,17 +228,17 @@ describe CouchRest::Model do
|
|
129
228
|
|
130
229
|
it "should be set for resaving" do
|
131
230
|
rev = @obj.rev
|
132
|
-
@obj
|
231
|
+
@obj['another-key'] = "some value"
|
133
232
|
@obj.save
|
134
233
|
@obj.rev.should_not == rev
|
135
234
|
end
|
136
235
|
|
137
236
|
it "should set the id" do
|
138
|
-
@obj.id.should be_an_instance_of
|
237
|
+
@obj.id.should be_an_instance_of(String)
|
139
238
|
end
|
140
239
|
|
141
240
|
it "should set the type" do
|
142
|
-
@obj
|
241
|
+
@obj['couchrest-type'].should == 'Basic'
|
143
242
|
end
|
144
243
|
end
|
145
244
|
|
@@ -184,6 +283,48 @@ describe CouchRest::Model do
|
|
184
283
|
end
|
185
284
|
end
|
186
285
|
|
286
|
+
describe "saving a model with a unique_id lambda" do
|
287
|
+
before(:each) do
|
288
|
+
@templated = WithTemplate.new
|
289
|
+
@old = WithTemplate.get('very-important') rescue nil
|
290
|
+
@old.destroy if @old
|
291
|
+
end
|
292
|
+
|
293
|
+
it "should require the field" do
|
294
|
+
lambda{@templated.save}.should raise_error
|
295
|
+
@templated['important-field'] = 'very-important'
|
296
|
+
@templated.save.should == true
|
297
|
+
end
|
298
|
+
|
299
|
+
it "should save with the id" do
|
300
|
+
@templated['important-field'] = 'very-important'
|
301
|
+
@templated.save.should == true
|
302
|
+
t = WithTemplate.get('very-important')
|
303
|
+
t.should == @templated
|
304
|
+
end
|
305
|
+
|
306
|
+
it "should not change the id on update" do
|
307
|
+
@templated['important-field'] = 'very-important'
|
308
|
+
@templated.save.should == true
|
309
|
+
@templated['important-field'] = 'not-important'
|
310
|
+
@templated.save.should == true
|
311
|
+
t = WithTemplate.get('very-important')
|
312
|
+
t.should == @templated
|
313
|
+
end
|
314
|
+
|
315
|
+
it "should raise an error when the id is taken" do
|
316
|
+
@templated['important-field'] = 'very-important'
|
317
|
+
@templated.save.should == true
|
318
|
+
lambda{WithTemplate.new('important-field' => 'very-important').save}.should raise_error
|
319
|
+
end
|
320
|
+
|
321
|
+
it "should set the id" do
|
322
|
+
@templated['important-field'] = 'very-important'
|
323
|
+
@templated.save.should == true
|
324
|
+
@templated.id.should == 'very-important'
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
187
328
|
describe "a model with timestamps" do
|
188
329
|
before(:all) do
|
189
330
|
@art = Article.new(:title => "Saving this")
|
@@ -214,7 +355,7 @@ describe CouchRest::Model do
|
|
214
355
|
|
215
356
|
it "should create the design doc" do
|
216
357
|
Article.by_date rescue nil
|
217
|
-
doc = Article.
|
358
|
+
doc = Article.design_doc
|
218
359
|
doc['views']['by_date'].should_not be_nil
|
219
360
|
end
|
220
361
|
|
@@ -234,6 +375,23 @@ describe CouchRest::Model do
|
|
234
375
|
end
|
235
376
|
end
|
236
377
|
|
378
|
+
describe "another model with a simple view" do
|
379
|
+
before(:all) do
|
380
|
+
Course.database.delete! rescue nil
|
381
|
+
@db = @cr.create_db(TESTDB) rescue nil
|
382
|
+
Course.new(:title => 'aaa').save
|
383
|
+
Course.new(:title => 'bbb').save
|
384
|
+
end
|
385
|
+
it "should make the design doc" do
|
386
|
+
doc = Course.design_doc
|
387
|
+
doc['views']['all']['map'].should include('Course')
|
388
|
+
end
|
389
|
+
it "should get them" do
|
390
|
+
rs = Course.by_title
|
391
|
+
rs.length.should == 2
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
237
395
|
describe "a model with a compound key view" do
|
238
396
|
before(:all) do
|
239
397
|
written_at = Time.now - 24 * 3600 * 7
|
@@ -249,12 +407,12 @@ describe CouchRest::Model do
|
|
249
407
|
end
|
250
408
|
it "should create the design doc" do
|
251
409
|
Article.by_user_id_and_date rescue nil
|
252
|
-
doc = Article.
|
410
|
+
doc = Article.design_doc
|
253
411
|
doc['views']['by_date'].should_not be_nil
|
254
412
|
end
|
255
413
|
it "should sort correctly" do
|
256
414
|
articles = Article.by_user_id_and_date
|
257
|
-
articles.collect{|a|a
|
415
|
+
articles.collect{|a|a['user_id']}.should == ['aaron', 'aaron', 'quentin', 'quentin']
|
258
416
|
articles[1].title.should == 'not junk'
|
259
417
|
end
|
260
418
|
it "should be queryable with couchrest options" do
|
@@ -289,4 +447,43 @@ describe CouchRest::Model do
|
|
289
447
|
view['rows'].find{|r|r['key'] == 'cool'}['value'].should == 3
|
290
448
|
end
|
291
449
|
end
|
450
|
+
|
451
|
+
describe "adding a view" do
|
452
|
+
before(:each) do
|
453
|
+
Article.by_date
|
454
|
+
@design_docs = Article.database.documents :startkey => "_design/", :endkey => "_design/\u9999"
|
455
|
+
end
|
456
|
+
it "should not create a design doc on view definition" do
|
457
|
+
Article.view_by :created_at
|
458
|
+
newdocs = Article.database.documents :startkey => "_design/", :endkey => "_design/\u9999"
|
459
|
+
newdocs["rows"].length.should == @design_docs["rows"].length
|
460
|
+
end
|
461
|
+
it "should create a new design document on view access" do
|
462
|
+
Article.view_by :created_at
|
463
|
+
Article.by_created_at
|
464
|
+
newdocs = Article.database.documents :startkey => "_design/", :endkey => "_design/\u9999"
|
465
|
+
newdocs["rows"].length.should == @design_docs["rows"].length + 1
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
describe "destroying an instance" do
|
470
|
+
before(:each) do
|
471
|
+
@obj = Basic.new
|
472
|
+
@obj.save.should == true
|
473
|
+
end
|
474
|
+
it "should return true" do
|
475
|
+
result = @obj.destroy
|
476
|
+
result.should == true
|
477
|
+
end
|
478
|
+
it "should be resavable" do
|
479
|
+
@obj.destroy
|
480
|
+
@obj.rev.should be_nil
|
481
|
+
@obj.id.should be_nil
|
482
|
+
@obj.save.should == true
|
483
|
+
end
|
484
|
+
it "should make it go away" do
|
485
|
+
@obj.destroy
|
486
|
+
lambda{Basic.get(@obj.id)}.should raise_error
|
487
|
+
end
|
488
|
+
end
|
292
489
|
end
|
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jchris-couchrest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- J. Chris Anderson
|
@@ -59,6 +59,8 @@ files:
|
|
59
59
|
- bin/couchapp
|
60
60
|
- bin/couchdir
|
61
61
|
- bin/couchview
|
62
|
+
- examples/model
|
63
|
+
- examples/model/example.rb
|
62
64
|
- examples/word_count
|
63
65
|
- examples/word_count/markov
|
64
66
|
- examples/word_count/views
|
@@ -94,12 +96,13 @@ files:
|
|
94
96
|
- spec/couchapp_spec.rb
|
95
97
|
- spec/couchrest
|
96
98
|
- spec/couchrest/core
|
99
|
+
- spec/couchrest/core/couchrest_spec.rb
|
100
|
+
- spec/couchrest/core/database_spec.rb
|
97
101
|
- spec/couchrest/core/model_spec.rb
|
98
|
-
- spec/couchrest_spec.rb
|
99
|
-
- spec/database_spec.rb
|
100
102
|
- spec/file_manager_spec.rb
|
101
103
|
- spec/fixtures
|
102
104
|
- spec/fixtures/attachments
|
105
|
+
- spec/fixtures/attachments/couchdb.png
|
103
106
|
- spec/fixtures/attachments/test.html
|
104
107
|
- spec/fixtures/couchapp
|
105
108
|
- spec/fixtures/couchapp/attachments
|
@@ -108,12 +111,6 @@ files:
|
|
108
111
|
- spec/fixtures/couchapp/views/example-map.js
|
109
112
|
- spec/fixtures/couchapp/views/example-reduce.js
|
110
113
|
- spec/fixtures/couchapp-test
|
111
|
-
- spec/fixtures/couchapp-test/my-app
|
112
|
-
- spec/fixtures/couchapp-test/my-app/attachments
|
113
|
-
- spec/fixtures/couchapp-test/my-app/attachments/index.html
|
114
|
-
- spec/fixtures/couchapp-test/my-app/views
|
115
|
-
- spec/fixtures/couchapp-test/my-app/views/example-map.js
|
116
|
-
- spec/fixtures/couchapp-test/my-app/views/example-reduce.js
|
117
114
|
- spec/fixtures/views
|
118
115
|
- spec/fixtures/views/lib.js
|
119
116
|
- spec/fixtures/views/test_view
|