juozasg-couchrest 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. data/LICENSE +176 -0
  2. data/README.rdoc +67 -0
  3. data/Rakefile +86 -0
  4. data/THANKS +15 -0
  5. data/bin/couchapp +58 -0
  6. data/bin/couchdir +20 -0
  7. data/bin/couchview +48 -0
  8. data/examples/model/example.rb +138 -0
  9. data/examples/word_count/markov +38 -0
  10. data/examples/word_count/views/books/chunked-map.js +3 -0
  11. data/examples/word_count/views/books/united-map.js +1 -0
  12. data/examples/word_count/views/markov/chain-map.js +6 -0
  13. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  14. data/examples/word_count/views/word_count/count-map.js +6 -0
  15. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  16. data/examples/word_count/word_count.rb +67 -0
  17. data/examples/word_count/word_count_query.rb +39 -0
  18. data/lib/couchrest/commands/generate.rb +71 -0
  19. data/lib/couchrest/commands/push.rb +103 -0
  20. data/lib/couchrest/core/database.rb +173 -0
  21. data/lib/couchrest/core/design.rb +89 -0
  22. data/lib/couchrest/core/document.rb +60 -0
  23. data/lib/couchrest/core/model.rb +557 -0
  24. data/lib/couchrest/core/server.rb +51 -0
  25. data/lib/couchrest/core/view.rb +4 -0
  26. data/lib/couchrest/helper/file_manager.rb +317 -0
  27. data/lib/couchrest/helper/pager.rb +103 -0
  28. data/lib/couchrest/helper/streamer.rb +44 -0
  29. data/lib/couchrest/helper/templates/bar.txt +11 -0
  30. data/lib/couchrest/helper/templates/example-map.js +8 -0
  31. data/lib/couchrest/helper/templates/example-reduce.js +10 -0
  32. data/lib/couchrest/helper/templates/index.html +26 -0
  33. data/lib/couchrest/monkeypatches.rb +24 -0
  34. data/lib/couchrest.rb +125 -0
  35. data/spec/couchapp_spec.rb +87 -0
  36. data/spec/couchrest/core/couchrest_spec.rb +191 -0
  37. data/spec/couchrest/core/database_spec.rb +478 -0
  38. data/spec/couchrest/core/design_spec.rb +131 -0
  39. data/spec/couchrest/core/document_spec.rb +96 -0
  40. data/spec/couchrest/core/model_spec.rb +660 -0
  41. data/spec/couchrest/helpers/file_manager_spec.rb +203 -0
  42. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  43. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  44. data/spec/fixtures/attachments/couchdb.png +0 -0
  45. data/spec/fixtures/attachments/test.html +11 -0
  46. data/spec/fixtures/views/lib.js +3 -0
  47. data/spec/fixtures/views/test_view/lib.js +3 -0
  48. data/spec/fixtures/views/test_view/only-map.js +4 -0
  49. data/spec/fixtures/views/test_view/test-map.js +3 -0
  50. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  51. data/spec/spec.opts +6 -0
  52. data/spec/spec_helper.rb +14 -0
  53. data/utils/remap.rb +27 -0
  54. data/utils/subset.rb +30 -0
  55. metadata +154 -0
@@ -0,0 +1,557 @@
1
+ require 'rubygems'
2
+ require 'extlib'
3
+ require 'digest/md5'
4
+ require File.dirname(__FILE__) + '/document'
5
+ # = CouchRest::Model - ORM, the CouchDB way
6
+ module CouchRest
7
+ # = CouchRest::Model - ORM, the CouchDB way
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
+ #
14
+ # ==== Example
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
21
+ # use_database CouchRest.database!('http://localhost:5984/couchrest-model-test')
22
+ # unique_id :slug
23
+ #
24
+ # view_by :date, :descending => true
25
+ # view_by :user_id, :date
26
+ #
27
+ # view_by :tags,
28
+ # :map =>
29
+ # "function(doc) {
30
+ # if (doc['couchrest-type'] == 'Article' && doc.tags) {
31
+ # doc.tags.forEach(function(tag){
32
+ # emit(tag, 1);
33
+ # });
34
+ # }
35
+ # }",
36
+ # :reduce =>
37
+ # "function(keys, values, rereduce) {
38
+ # return sum(values);
39
+ # }"
40
+ #
41
+ # key_writer :date
42
+ # key_reader :slug, :created_at, :updated_at
43
+ # key_accessor :title, :tags
44
+ #
45
+ # timestamps!
46
+ #
47
+ # before(:create, :generate_slug_from_title)
48
+ # def generate_slug_from_title
49
+ # self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
50
+ # end
51
+ # end
52
+ #
53
+ # ==== Examples of finding articles with these views:
54
+ #
55
+ # * All the articles by Barney published in the last 24 hours. Note that we
56
+ # use <tt>{}</tt> as a special value that sorts after all strings,
57
+ # numbers, and arrays.
58
+ #
59
+ # Article.by_user_id_and_date :startkey => ["barney", Time.now - 24 * 3600], :endkey => ["barney", {}]
60
+ #
61
+ # * The most recent 20 articles. Remember that the <tt>view_by :date</tt>
62
+ # has the default option <tt>:descending => true</tt>.
63
+ #
64
+ # Article.by_date :count => 20
65
+ #
66
+ # * The raw CouchDB view reduce result for the custom <tt>:tags</tt> view.
67
+ # In this case we'll get a count of the number of articles tagged "ruby".
68
+ #
69
+ # Article.by_tags :key => "ruby", :reduce => true
70
+ #
71
+ class Model < Document
72
+
73
+ # instantiates the hash by converting all the keys to strings.
74
+ def initialize keys = {}
75
+ super(keys)
76
+ apply_defaults
77
+ cast_keys
78
+ unless self['_id'] && self['_rev']
79
+ self['couchrest-type'] = self.class.to_s
80
+ end
81
+ end
82
+
83
+ # this is the CouchRest::Database that model classes will use unless
84
+ # they override it with <tt>use_database</tt>
85
+ cattr_accessor :default_database
86
+
87
+ class_inheritable_accessor :casts
88
+ class_inheritable_accessor :default_obj
89
+ class_inheritable_accessor :class_database
90
+ class_inheritable_accessor :design_doc
91
+ class_inheritable_accessor :design_doc_slug_cache
92
+ class_inheritable_accessor :design_doc_fresh
93
+
94
+ class << self
95
+ # override the CouchRest::Model-wide default_database
96
+ def use_database db
97
+ self.class_database = db
98
+ end
99
+
100
+ # returns the CouchRest::Database instance that this class uses
101
+ def database
102
+ self.class_database || CouchRest::Model.default_database
103
+ end
104
+
105
+ # Load a document from the database by id
106
+ def get id
107
+ doc = database.get id
108
+ new(doc)
109
+ end
110
+
111
+ # Load all documents that have the "couchrest-type" field equal to the
112
+ # name of the current class. Take the standard set of
113
+ # CouchRest::Database#view options.
114
+ def all opts = {}, &block
115
+ self.design_doc ||= Design.new(default_design_doc)
116
+ unless design_doc_fresh
117
+ refresh_design_doc
118
+ end
119
+ view :all, opts, &block
120
+ end
121
+
122
+ # Load the first document that have the "couchrest-type" field equal to
123
+ # the name of the current class.
124
+ #
125
+ # ==== Returns
126
+ # Object:: The first object instance available
127
+ # or
128
+ # Nil:: if no instances available
129
+ #
130
+ # ==== Parameters
131
+ # opts<Hash>::
132
+ # View options, see <tt>CouchRest::Database#view</tt> options for more info.
133
+ def first opts = {}
134
+ first_instance = self.all(opts.merge!(:count => 1))
135
+ first_instance.empty? ? nil : first_instance.first
136
+ end
137
+
138
+ # Cast a field as another class. The class must be happy to have the
139
+ # field's primitive type as the argument to it's constuctur. Classes
140
+ # which inherit from CouchRest::Model are happy to act as sub-objects
141
+ # for any fields that are stored in JSON as object (and therefore are
142
+ # parsed from the JSON as Ruby Hashes).
143
+ #
144
+ # Example:
145
+ #
146
+ # class Post < CouchRest::Model
147
+ #
148
+ # key_accessor :title, :body, :author
149
+ #
150
+ # cast :author, :as => 'Author'
151
+ #
152
+ # end
153
+ #
154
+ # post.author.class #=> Author
155
+ #
156
+ # Using the same example, if a Post should have many Comments, we
157
+ # would declare it like this:
158
+ #
159
+ # class Post < CouchRest::Model
160
+ #
161
+ # key_accessor :title, :body, :author, comments
162
+ #
163
+ # cast :author, :as => 'Author'
164
+ # cast :comments, :as => ['Comment']
165
+ #
166
+ # end
167
+ #
168
+ # post.author.class #=> Author
169
+ # post.comments.class #=> Array
170
+ # post.comments.first #=> Comment
171
+ #
172
+ def cast field, opts = {}
173
+ self.casts ||= {}
174
+ self.casts[field.to_s] = opts
175
+ end
176
+
177
+ # Defines methods for reading and writing from fields in the document.
178
+ # Uses key_writer and key_reader internally.
179
+ def key_accessor *keys
180
+ key_writer *keys
181
+ key_reader *keys
182
+ end
183
+
184
+ # For each argument key, define a method <tt>key=</tt> that sets the
185
+ # corresponding field on the CouchDB document.
186
+ def key_writer *keys
187
+ keys.each do |method|
188
+ key = method.to_s
189
+ define_method "#{method}=" do |value|
190
+ self[key] = value
191
+ end
192
+ end
193
+ end
194
+
195
+ # For each argument key, define a method <tt>key</tt> that reads the
196
+ # corresponding field on the CouchDB document.
197
+ def key_reader *keys
198
+ keys.each do |method|
199
+ key = method.to_s
200
+ define_method method do
201
+ self[key]
202
+ end
203
+ end
204
+ end
205
+
206
+ def default
207
+ self.default_obj
208
+ end
209
+
210
+ def set_default hash
211
+ self.default_obj = hash
212
+ end
213
+
214
+ # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
215
+ # on the document whenever saving occurs. CouchRest uses a pretty
216
+ # decent time format by default. See Time#to_json
217
+ def timestamps!
218
+ before(:create) do
219
+ self['updated_at'] = self['created_at'] = Time.now
220
+ end
221
+ before(:update) do
222
+ self['updated_at'] = Time.now
223
+ end
224
+ end
225
+
226
+ # Name a method that will be called before the document is first saved,
227
+ # which returns a string to be used for the document's <tt>_id</tt>.
228
+ # Because CouchDB enforces a constraint that each id must be unique,
229
+ # this can be used to enforce eg: uniq usernames. Note that this id
230
+ # must be globally unique across all document types which share a
231
+ # database, so if you'd like to scope uniqueness to this class, you
232
+ # should use the class name as part of the unique id.
233
+ def unique_id method = nil, &block
234
+ if method
235
+ define_method :set_unique_id do
236
+ self['_id'] ||= self.send(method)
237
+ end
238
+ elsif block
239
+ define_method :set_unique_id do
240
+ uniqid = block.call(self)
241
+ raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
242
+ self['_id'] ||= uniqid
243
+ end
244
+ end
245
+ end
246
+
247
+ # Define a CouchDB view. The name of the view will be the concatenation
248
+ # of <tt>by</tt> and the keys joined by <tt>_and_</tt>
249
+ #
250
+ # ==== Example views:
251
+ #
252
+ # class Post
253
+ # # view with default options
254
+ # # query with Post.by_date
255
+ # view_by :date, :descending => true
256
+ #
257
+ # # view with compound sort-keys
258
+ # # query with Post.by_user_id_and_date
259
+ # view_by :user_id, :date
260
+ #
261
+ # # view with custom map/reduce functions
262
+ # # query with Post.by_tags :reduce => true
263
+ # view_by :tags,
264
+ # :map =>
265
+ # "function(doc) {
266
+ # if (doc['couchrest-type'] == 'Post' && doc.tags) {
267
+ # doc.tags.forEach(function(tag){
268
+ # emit(doc.tag, 1);
269
+ # });
270
+ # }
271
+ # }",
272
+ # :reduce =>
273
+ # "function(keys, values, rereduce) {
274
+ # return sum(values);
275
+ # }"
276
+ # end
277
+ #
278
+ # <tt>view_by :date</tt> will create a view defined by this Javascript
279
+ # function:
280
+ #
281
+ # function(doc) {
282
+ # if (doc['couchrest-type'] == 'Post' && doc.date) {
283
+ # emit(doc.date, null);
284
+ # }
285
+ # }
286
+ #
287
+ # It can be queried by calling <tt>Post.by_date</tt> which accepts all
288
+ # valid options for CouchRest::Database#view. In addition, calling with
289
+ # the <tt>:raw => true</tt> option will return the view rows
290
+ # themselves. By default <tt>Post.by_date</tt> will return the
291
+ # documents included in the generated view.
292
+ #
293
+ # CouchRest::Database#view options can be applied at view definition
294
+ # time as defaults, and they will be curried and used at view query
295
+ # time. Or they can be overridden at query time.
296
+ #
297
+ # Custom views can be queried with <tt>:reduce => true</tt> to return
298
+ # reduce results. The default for custom views is to query with
299
+ # <tt>:reduce => false</tt>.
300
+ #
301
+ # Views are generated (on a per-model basis) lazily on first-access.
302
+ # This means that if you are deploying changes to a view, the views for
303
+ # that model won't be available until generation is complete. This can
304
+ # take some time with large databases. Strategies are in the works.
305
+ #
306
+ # To understand the capabilities of this view system more compeletly,
307
+ # it is recommended that you read the RSpec file at
308
+ # <tt>spec/core/model_spec.rb</tt>.
309
+
310
+ def view_by *keys
311
+ self.design_doc ||= Design.new(default_design_doc)
312
+ opts = keys.pop if keys.last.is_a?(Hash)
313
+ opts ||= {}
314
+ ducktype = opts.delete(:ducktype)
315
+ unless ducktype || opts[:map]
316
+ opts[:guards] ||= []
317
+ opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
318
+ end
319
+ keys.push opts
320
+ self.design_doc.view_by(*keys)
321
+ self.design_doc_fresh = false
322
+ end
323
+
324
+ def method_missing m, *args
325
+ if has_view?(m)
326
+ query = args.shift || {}
327
+ view(m, query, *args)
328
+ else
329
+ super
330
+ end
331
+ end
332
+
333
+ # returns stored defaults if the there is a view named this in the design doc
334
+ def has_view?(view)
335
+ view = view.to_s
336
+ design_doc && design_doc['views'] && design_doc['views'][view]
337
+ end
338
+
339
+ # Dispatches to any named view.
340
+ def view name, query={}, &block
341
+ unless design_doc_fresh
342
+ refresh_design_doc
343
+ end
344
+ query[:raw] = true if query[:reduce]
345
+ raw = query.delete(:raw)
346
+ fetch_view_with_docs(name, query, raw, &block)
347
+ end
348
+
349
+ def all_design_doc_versions
350
+ database.documents :startkey => "_design/#{self.to_s}-",
351
+ :endkey => "_design/#{self.to_s}-\u9999"
352
+ end
353
+
354
+ # Deletes any non-current design docs that were created by this class.
355
+ # Running this when you're deployed version of your application is steadily
356
+ # and consistently using the latest code, is the way to clear out old design
357
+ # docs. Running it to early could mean that live code has to regenerate
358
+ # potentially large indexes.
359
+ def cleanup_design_docs!
360
+ ddocs = all_design_doc_versions
361
+ ddocs["rows"].each do |row|
362
+ if (row['id'] != design_doc_id)
363
+ database.delete({
364
+ "_id" => row['id'],
365
+ "_rev" => row['value']['rev']
366
+ })
367
+ end
368
+ end
369
+ end
370
+
371
+ private
372
+
373
+ def fetch_view_with_docs name, opts, raw=false, &block
374
+ if raw
375
+ fetch_view name, opts, &block
376
+ else
377
+ begin
378
+ view = fetch_view name, opts.merge({:include_docs => true}), &block
379
+ view['rows'].collect{|r|new(r['doc'])} if view['rows']
380
+ rescue
381
+ # fallback for old versions of couchdb that don't
382
+ # have include_docs support
383
+ view = fetch_view name, opts, &block
384
+ view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
385
+ end
386
+ end
387
+ end
388
+
389
+ def fetch_view view_name, opts, &block
390
+ retryable = true
391
+ begin
392
+ design_doc.view(view_name, opts, &block)
393
+ # the design doc could have been deleted by a rouge process
394
+ rescue RestClient::ResourceNotFound => e
395
+ if retryable
396
+ refresh_design_doc
397
+ retryable = false
398
+ retry
399
+ else
400
+ raise e
401
+ end
402
+ end
403
+ end
404
+
405
+ def design_doc_id
406
+ "_design/#{design_doc_slug}"
407
+ end
408
+
409
+ def design_doc_slug
410
+ return design_doc_slug_cache if design_doc_slug_cache && design_doc_fresh
411
+ funcs = []
412
+ design_doc['views'].each do |name, view|
413
+ funcs << "#{name}/#{view['map']}#{view['reduce']}"
414
+ end
415
+ md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
416
+ self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
417
+ end
418
+
419
+ def default_design_doc
420
+ {
421
+ "language" => "javascript",
422
+ "views" => {
423
+ 'all' => {
424
+ 'map' => "function(doc) {
425
+ if (doc['couchrest-type'] == '#{self.to_s}') {
426
+ emit(null,null);
427
+ }
428
+ }"
429
+ }
430
+ }
431
+ }
432
+ end
433
+
434
+ def refresh_design_doc
435
+ did = design_doc_id
436
+ saved = database.get(did) rescue nil
437
+ if saved
438
+ design_doc['views'].each do |name, view|
439
+ saved['views'][name] = view
440
+ end
441
+ database.save(saved)
442
+ else
443
+ design_doc['_id'] = did
444
+ design_doc.delete('_rev')
445
+ design_doc.database = database
446
+ design_doc.save
447
+ end
448
+ self.design_doc_fresh = true
449
+ end
450
+
451
+ end # class << self
452
+
453
+ # returns the database used by this model's class
454
+ def database
455
+ self.class.database
456
+ end
457
+
458
+ # Takes a hash as argument, and applies the values by using writer methods
459
+ # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
460
+ # missing. In case of error, no attributes are changed.
461
+ def update_attributes_without_saving hash
462
+ hash.each do |k, v|
463
+ raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=")
464
+ end
465
+ hash.each do |k, v|
466
+ self.send("#{k}=",v)
467
+ end
468
+ end
469
+
470
+ # Takes a hash as argument, and applies the values by using writer methods
471
+ # for each key. Raises a NoMethodError if the corresponding methods are
472
+ # missing. In case of error, no attributes are changed.
473
+ def update_attributes hash
474
+ update_attributes_without_saving hash
475
+ save
476
+ end
477
+
478
+ # for compatibility with old-school frameworks
479
+ alias :new_record? :new_document?
480
+
481
+ # We override this to create the create and update callback opportunities.
482
+ # I think we should drop those and just have save. If you care, in your callback,
483
+ # check new_document?
484
+ def save actually=false
485
+ if actually
486
+ super()
487
+ else
488
+ if new_document?
489
+ create
490
+ else
491
+ update
492
+ end
493
+ end
494
+ end
495
+
496
+ # Saves the document to the db using create or update. Raises an exception
497
+ # if the document is not saved properly.
498
+ def save!
499
+ raise "#{self.inspect} failed to save" unless self.save
500
+ end
501
+
502
+ def update
503
+ save :actually
504
+ end
505
+
506
+ # Deletes the document from the database. Runs the :delete callbacks.
507
+ # Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
508
+ # document to be saved to a new <tt>_id</tt>.
509
+ def destroy
510
+ result = database.delete self
511
+ if result['ok']
512
+ self['_rev'] = nil
513
+ self['_id'] = nil
514
+ end
515
+ result['ok']
516
+ end
517
+
518
+ def create
519
+ # can we use the callbacks for this?
520
+ set_unique_id if self.respond_to?(:set_unique_id)
521
+ save :actually
522
+ end
523
+
524
+ private
525
+
526
+ def apply_defaults
527
+ if self.class.default
528
+ self.class.default.each do |k,v|
529
+ self[k] = v
530
+ end
531
+ end
532
+ end
533
+
534
+ def cast_keys
535
+ return unless self.class.casts
536
+ # TODO move the argument checking to the cast method for early crashes
537
+ self.class.casts.each do |k,v|
538
+ next unless self[k]
539
+ target = v[:as]
540
+ if target.is_a?(Array)
541
+ klass = ::Extlib::Inflection.constantize(target[0])
542
+ self[k] = self[k].collect do |value|
543
+ klass.new(value)
544
+ end
545
+ else
546
+ self[k] = ::Extlib::Inflection.constantize(target).new(self[k])
547
+ end
548
+ end
549
+ end
550
+
551
+ include ::Extlib::Hook
552
+ # todo: drop create and update hooks...
553
+ # (use new_record? in callbacks if you care)
554
+ register_instance_hooks :save, :create, :update, :destroy
555
+
556
+ end # class Model
557
+ end # module CouchRest
@@ -0,0 +1,51 @@
1
+ module CouchRest
2
+ class Server
3
+ attr_accessor :uri, :uuid_batch_count
4
+ def initialize server = 'http://localhost:5984', uuid_batch_count = 1000
5
+ @uri = server
6
+ @uuid_batch_count = uuid_batch_count
7
+ end
8
+
9
+ # List all databases on the server
10
+ def databases
11
+ CouchRest.get "#{@uri}/_all_dbs"
12
+ end
13
+
14
+ # Returns a CouchRest::Database for the given name
15
+ def database name
16
+ CouchRest::Database.new(self, name)
17
+ end
18
+
19
+ # Creates the database if it doesn't exist
20
+ def database! name
21
+ create_db(name) rescue nil
22
+ database name
23
+ end
24
+
25
+ # GET the welcome message
26
+ def info
27
+ CouchRest.get "#{@uri}/"
28
+ end
29
+
30
+ # Create a database
31
+ def create_db name
32
+ CouchRest.put "#{@uri}/#{name}"
33
+ database name
34
+ end
35
+
36
+ # Restart the CouchDB instance
37
+ def restart!
38
+ CouchRest.post "#{@uri}/_restart"
39
+ end
40
+
41
+ # Retrive an unused UUID from CouchDB. Server instances manage caching a list of unused UUIDs.
42
+ def next_uuid count = @uuid_batch_count
43
+ @uuids ||= []
44
+ if @uuids.empty?
45
+ @uuids = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"]
46
+ end
47
+ @uuids.pop
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,4 @@
1
+ module CouchRest
2
+ class View
3
+ end
4
+ end