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 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 library](http://svn.apache.org/repos/asf/incubator/couchdb/trunk/share/www/script/couch.js), which I find to be concise, clear, and well designed. CouchRest lightly wraps CouchDB's HTTP API, managing JSON serialization, and remembering the URI-paths to CouchDB's API endpoints so you don't have to.
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 framework-specific object oriented APIs.
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. REST Client takes all the nastyness of Net::HTTP and gives is a pretty face, while still giving you more control than Open-URI. I recommend it anytime you’re interfacing with a well-defined web service.
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 CouchRest install, from the project root directory run `rake`, or `autotest` (requires RSpec and optionally ZenTest for autotest support).
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. By
54
- including it in your class, suddenly you get all sorts of magic sugar, so that
55
- working with CouchDB in your Rails or Merb app is no harder than working with the
56
- standard SQL alternatives. See the CouchRest::Model documentation for and example article class that illustrates usage.
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
@@ -5,7 +5,7 @@ require 'spec/rake/spectask'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = "couchrest"
8
- s.version = "0.9.9"
8
+ s.version = "0.9.10"
9
9
  s.date = "2008-09-11"
10
10
  s.summary = "Lean and RESTful interface to CouchDB."
11
11
  s.email = "jchris@grabb.it"
@@ -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']
@@ -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 avoids all usage of <tt>method_missing</tt>, and tries to strike a balance between usability and magic. See CouchRest::Model::MagicViews#view_by for documentation about the view-generation system. For the other class methods, inspiried by DataMapper and ActiveRecord, see CouchRest::Model::ClassMethods. The InstanceMethods are pretty basic.
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 spec/couchrest/core/model_spec.rb file, which may be even more up to date than this example.
10
- #
11
- # class Article
12
- # include CouchRest::Model
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.type == 'Article' && doc.tags) {
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
- # doc['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
49
+ # self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
42
50
  # end
43
51
  # end
44
- module Model
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
- # alias for doc['_rev']
75
- def rev
76
- doc['_rev']
77
- end
78
-
79
- # returns true if the doc has never been saved
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
- # save the doc to the db using create or update
85
- def save
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
- protected
94
-
95
- def create
96
- set_unique_id if respond_to?(:set_unique_id) # hack
97
- save_doc
98
- end
99
-
100
- def update
101
- save_doc
102
- end
103
-
104
- private
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
- @database = db
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
- @database || CouchRest::Model.default_database
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
- # Defines methods for reading and writing from fields in the document. Uses key_writer and key_reader internally.
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 corresponding field on the CouchDB document.
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
- doc[key] = value
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 corresponding field on the CouchDB document.
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
- doc[key]
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
- # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields on the document whenever saving occurs. CouchRest uses a pretty decent time format by default. See Time#to_json
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
- doc['updated_at'] = doc['created_at'] = Time.now
158
+ self['updated_at'] = self['created_at'] = Time.now
168
159
  end
169
160
  before(:update) do
170
- doc['updated_at'] = Time.now
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, which returns a string to be used for the document's <tt>_id</tt>. Because CouchDB enforces a constraint that each id must be unique, this can be used to enforce eg: uniq usernames. Note that this id must be globally unique across all document types which share a database, so if you'd like to scope uniqueness to this class, you should use the class name as part of the unique id.
175
- def unique_id method
176
- define_method :set_unique_id do
177
- doc['_id'] ||= self.send(method)
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
- module MagicViews
184
-
185
- # Define a CouchDB view. The name of the view will be the concatenation of <tt>by</tt> and the keys joined by <tt>_and_</tt>
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.type == 'Post' && doc.tags) {
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 function:
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.type == 'Post' && doc.date) {
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 valid options for CouchRest::Database#view. In addition, calling with the <tt>:raw => true</tt> option will return the view rows themselves. By default <tt>Post.by_date</tt> will return the documents included in the generated view.
224
- #
225
- # CouchRest::Database#view options can be applied at view definition time as defaults, and they will be curried and used at view query time. Or they can be overridden at query time.
226
- #
227
- # Custom views can be queried with <tt>:reduce => true</tt> to return reduce results. The default for custom views is to query with <tt>:reduce => false</tt>.
228
- #
229
- # Views are generated (on a per-model basis) lazily on first-access. This means that if you are deploying changes to a view, the views for that model won't be available until generation is complete. This can take some time with large databases. Strategies are in the works.
230
- #
231
- # To understand the capabilities of this view system more compeletly, it is recommended that you read the RSpec file at <tt>spec/core/model.rb</tt>.
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
- @@design_doc ||= default_design_doc
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
- @@design_doc['views'][method_name] = view
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.type == '#{type}' && #{key_protection}) {
270
+ if (doc['couchrest-type'] == '#{type}' && #{key_protection}) {
255
271
  emit(#{key_emit}, null);
256
272
  }
257
273
  }
258
274
  JAVASCRIPT
259
- @@design_doc['views'][method_name] = {
275
+ generated_design_doc['views'][method_name] = {
260
276
  'map' => map_function
261
277
  }
262
278
  end
263
-
264
- @@design_doc_fresh = false
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 @@design_doc_fresh
286
+ unless design_doc_fresh
271
287
  refresh_design_doc
272
288
  end
273
289
  raw = query.delete(:raw)
274
- view_name = "#{type}/#{method_name}"
275
-
290
+ view_name = "#{design_doc_slug}/#{method_name}"
276
291
  view = fetch_view(view_name, query)
277
- if raw
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
- # the design doc could have been deleted by a rouge process
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 design_doc_id
306
- "_design/#{self.to_s}"
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
- saved = database.get(design_doc_id) rescue nil
355
+ did = "_design/#{design_doc_slug}"
356
+ saved = database.get(did) rescue nil
319
357
  if saved
320
- @@design_doc['views'].each do |name, view|
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
- database.save(@@design_doc)
363
+ generated_design_doc['_id'] = did
364
+ database.save(generated_design_doc)
326
365
  end
327
- @@design_doc_fresh = true
366
+ self.design_doc_fresh = true
328
367
  end
329
-
330
- end # module MagicViews
331
-
332
- module Callbacks
333
- def self.included(model)
334
- model.class_eval <<-EOS, __FILE__, __LINE__
335
- include Extlib::Hook
336
- register_instance_hooks :save, :create, :update #, :destroy
337
- EOS
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
- end # module Model
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__) + '/spec_helper'
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
2
 
3
3
  describe CouchRest do
4
4
 
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
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 Article
8
- include CouchRest::Model
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.type == 'Article' && doc.tags) {
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
- doc['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
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.doc['title'] = 'My Article Title'
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.doc['title'].should == 'My Article Title'
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.doc['date'].should == t
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.doc['slug'] = 'my-slug'
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.doc['another-key'] = "some value"
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 String
237
+ @obj.id.should be_an_instance_of(String)
139
238
  end
140
239
 
141
240
  it "should set the type" do
142
- @obj.doc['type'].should == 'Basic'
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.database.get("_design/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.database.get("_design/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.doc['user_id']}.should == ['aaron', 'aaron', 'quentin', 'quentin']
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
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.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