addywaddy-couch_surfer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,620 @@
1
+ require 'rubygems'
2
+ require 'extlib'
3
+ require 'couchrest'
4
+ require 'digest/md5'
5
+ require 'mime/types'
6
+
7
+ module CouchSurfer
8
+ module Model
9
+ @database = nil
10
+ def self.default_database
11
+ @database
12
+ end
13
+
14
+ def self.default_database= db
15
+ @database = db
16
+ end
17
+
18
+ module ClassMethods
19
+ # override the CouchSurfer::Model-wide default_database
20
+ def use_database db
21
+ self.class_database = db
22
+ end
23
+
24
+ # returns the CouchRest::Database instance that this class uses
25
+ def database
26
+ self.class_database || CouchSurfer::Model.default_database
27
+ end
28
+
29
+ # Load a document from the database by id
30
+ def get id
31
+ doc = database.get id
32
+ new(doc)
33
+ end
34
+
35
+ def create(attrs = {})
36
+ instance = new(attrs)
37
+ instance.save
38
+ instance
39
+ end
40
+
41
+ # Load all documents that have the "couchrest-type" field equal to the
42
+ # name of the current class. Take the standard set of
43
+ # CouchRest::Database#view options.
44
+ def all opts = {}, &block
45
+ self.design_doc ||= CouchRest::Design.new(default_design_doc)
46
+ unless design_doc_fresh
47
+ refresh_design_doc
48
+ end
49
+ view :all, opts, &block
50
+ end
51
+
52
+ # Load the first document that have the "couchrest-type" field equal to
53
+ # the name of the current class.
54
+ #
55
+ # ==== Returns
56
+ # Object:: The first object instance available
57
+ # or
58
+ # Nil:: if no instances available
59
+ #
60
+ # ==== Parameters
61
+ # opts<Hash>::
62
+ # View options, see <tt>CouchRest::Database#view</tt> options for more info.
63
+ def first opts = {}
64
+ first_instance = self.all(opts.merge!(:limit => 1))
65
+ first_instance.empty? ? nil : first_instance.first
66
+ end
67
+
68
+ # Cast a field as another class. The class must be happy to have the
69
+ # field's primitive type as the argument to it's constuctur. Classes
70
+ # which inherit from CouchRest::Model are happy to act as sub-objects
71
+ # for any fields that are stored in JSON as object (and therefore are
72
+ # parsed from the JSON as Ruby Hashes).
73
+ #
74
+ # Example:
75
+ #
76
+ # class Post < CouchRest::Model
77
+ #
78
+ # key_accessor :title, :body, :author
79
+ #
80
+ # cast :author, :as => 'Author'
81
+ #
82
+ # end
83
+ #
84
+ # post.author.class #=> Author
85
+ #
86
+ # Using the same example, if a Post should have many Comments, we
87
+ # would declare it like this:
88
+ #
89
+ # class Post < CouchRest::Model
90
+ #
91
+ # key_accessor :title, :body, :author, comments
92
+ #
93
+ # cast :author, :as => 'Author'
94
+ # cast :comments, :as => ['Comment']
95
+ #
96
+ # end
97
+ #
98
+ # post.author.class #=> Author
99
+ # post.comments.class #=> Array
100
+ # post.comments.first #=> Comment
101
+ #
102
+ def cast field, opts = {}
103
+ self.casts ||= {}
104
+ self.casts[field.to_s] = opts
105
+ end
106
+
107
+ # Defines methods for reading and writing from fields in the document.
108
+ # Uses key_writer and key_reader internally.
109
+ def key_accessor *keys
110
+ key_writer *keys
111
+ key_reader *keys
112
+ end
113
+
114
+ # For each argument key, define a method <tt>key=</tt> that sets the
115
+ # corresponding field on the CouchDB document.
116
+ def key_writer *keys
117
+ keys.each do |method|
118
+ key = method.to_s
119
+ define_method "#{method}=" do |value|
120
+ #puts "#@attributes['#{method}'] = #{value}"
121
+ @attributes[method] = value# ||= nil
122
+ end
123
+ end
124
+ end
125
+
126
+ # For each argument key, define a method <tt>key</tt> that reads the
127
+ # corresponding field on the CouchDB document.
128
+ def key_reader *keys
129
+ keys.each do |method|
130
+ key = method.to_s
131
+ define_method method do
132
+ @attributes[method]
133
+ end
134
+ end
135
+ end
136
+
137
+ def set_default hash
138
+ self.default_obj = hash
139
+ end
140
+
141
+ def default
142
+ self.default_obj
143
+ end
144
+
145
+ # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
146
+ # on the document whenever saving occurs. CouchRest uses a pretty
147
+ # decent time format by default. See Time#to_json
148
+ def timestamps!
149
+ before(:save) do
150
+ self['updated_at'] = Time.now
151
+ self['created_at'] = self['updated_at'] if new_document?
152
+ end
153
+ end
154
+
155
+ # Name a method that will be called before the document is first saved,
156
+ # which returns a string to be used for the document's <tt>_id</tt>.
157
+ # Because CouchDB enforces a constraint that each id must be unique,
158
+ # this can be used to enforce eg: uniq usernames. Note that this id
159
+ # must be globally unique across all document types which share a
160
+ # database, so if you'd like to scope uniqueness to this class, you
161
+ # should use the class name as part of the unique id.
162
+ def unique_id method = nil, &block
163
+ if method
164
+ define_method :set_unique_id do
165
+ self['_id'] ||= self.send(method)
166
+ end
167
+ elsif block
168
+ define_method :set_unique_id do
169
+ uniqid = block.call(self)
170
+ raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
171
+ self['_id'] ||= uniqid
172
+ end
173
+ end
174
+ end
175
+
176
+ # Define a CouchDB view. The name of the view will be the concatenation
177
+ # of <tt>by</tt> and the keys joined by <tt>_and_</tt>
178
+ #
179
+ # ==== Example views:
180
+ #
181
+ # class Post
182
+ # # view with default options
183
+ # # query with Post.by_date
184
+ # view_by :date, :descending => true
185
+ #
186
+ # # view with compound sort-keys
187
+ # # query with Post.by_user_id_and_date
188
+ # view_by :user_id, :date
189
+ #
190
+ # # view with custom map/reduce functions
191
+ # # query with Post.by_tags :reduce => true
192
+ # view_by :tags,
193
+ # :map =>
194
+ # "function(doc) {
195
+ # if (doc['couchrest-type'] == 'Post' && doc.tags) {
196
+ # doc.tags.forEach(function(tag){
197
+ # emit(doc.tag, 1);
198
+ # });
199
+ # }
200
+ # }",
201
+ # :reduce =>
202
+ # "function(keys, values, rereduce) {
203
+ # return sum(values);
204
+ # }"
205
+ # end
206
+ #
207
+ # <tt>view_by :date</tt> will create a view defined by this Javascript
208
+ # function:
209
+ #
210
+ # function(doc) {
211
+ # if (doc['couchrest-type'] == 'Post' && doc.date) {
212
+ # emit(doc.date, null);
213
+ # }
214
+ # }
215
+ #
216
+ # It can be queried by calling <tt>Post.by_date</tt> which accepts all
217
+ # valid options for CouchRest::Database#view. In addition, calling with
218
+ # the <tt>:raw => true</tt> option will return the view rows
219
+ # themselves. By default <tt>Post.by_date</tt> will return the
220
+ # documents included in the generated view.
221
+ #
222
+ # CouchRest::Database#view options can be applied at view definition
223
+ # time as defaults, and they will be curried and used at view query
224
+ # time. Or they can be overridden at query time.
225
+ #
226
+ # Custom views can be queried with <tt>:reduce => true</tt> to return
227
+ # reduce results. The default for custom views is to query with
228
+ # <tt>:reduce => false</tt>.
229
+ #
230
+ # Views are generated (on a per-model basis) lazily on first-access.
231
+ # This means that if you are deploying changes to a view, the views for
232
+ # that model won't be available until generation is complete. This can
233
+ # take some time with large databases. Strategies are in the works.
234
+ #
235
+ # To understand the capabilities of this view system more compeletly,
236
+ # it is recommended that you read the RSpec file at
237
+ # <tt>spec/core/model_spec.rb</tt>.
238
+ def view_by *keys
239
+ self.design_doc ||= CouchRest::Design.new(default_design_doc)
240
+ opts = keys.pop if keys.last.is_a?(Hash)
241
+ opts ||= {}
242
+ ducktype = opts.delete(:ducktype)
243
+ unless ducktype || opts[:map]
244
+ opts[:guards] ||= []
245
+ opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
246
+ end
247
+ keys.push opts
248
+ self.design_doc.view_by(*keys)
249
+ self.design_doc_fresh = false
250
+ end
251
+
252
+ def method_missing m, *args
253
+ if has_view?(m)
254
+ query = args.shift || {}
255
+ view(m, query, *args)
256
+ else
257
+ CouchRest::Document.send(:method_missing, m, *args)
258
+ end
259
+ end
260
+
261
+ # returns stored defaults if the there is a view named this in the design doc
262
+ def has_view?(view)
263
+ view = view.to_s
264
+ design_doc && design_doc['views'] && design_doc['views'][view]
265
+ end
266
+
267
+ # Dispatches to any named view.
268
+ def view name, query={}, &block
269
+ unless design_doc_fresh
270
+ refresh_design_doc
271
+ end
272
+ query[:raw] = true if query[:reduce]
273
+ raw = query.delete(:raw)
274
+ fetch_view_with_docs(name, query, raw, &block)
275
+ end
276
+
277
+ def all_design_doc_versions
278
+ database.documents :startkey => "_design/#{self.to_s}-",
279
+ :endkey => "_design/#{self.to_s}-\u9999"
280
+ end
281
+
282
+ # Deletes any non-current design docs that were created by this class.
283
+ # Running this when you're deployed version of your application is steadily
284
+ # and consistently using the latest code, is the way to clear out old design
285
+ # docs. Running it to early could mean that live code has to regenerate
286
+ # potentially large indexes.
287
+ def cleanup_design_docs!
288
+ ddocs = all_design_doc_versions
289
+ ddocs["rows"].each do |row|
290
+ if (row['id'] != design_doc_id)
291
+ database.delete({
292
+ "_id" => row['id'],
293
+ "_rev" => row['value']['rev']
294
+ })
295
+ end
296
+ end
297
+ end
298
+
299
+ private
300
+
301
+ def fetch_view_with_docs name, opts, raw=false, &block
302
+ if raw
303
+ fetch_view name, opts, &block
304
+ else
305
+ begin
306
+ view = fetch_view name, opts.merge({:include_docs => true}), &block
307
+ view['rows'].collect{|r|new(r['doc'])} if view['rows']
308
+ rescue
309
+ # fallback for old versions of couchdb that don't
310
+ # have include_docs support
311
+ view = fetch_view name, opts, &block
312
+ view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
313
+ end
314
+ end
315
+ end
316
+
317
+ def fetch_view view_name, opts, &block
318
+ retryable = true
319
+ begin
320
+ design_doc.view(view_name, opts, &block)
321
+ # the design doc could have been deleted by a rouge process
322
+ rescue RestClient::ResourceNotFound => e
323
+ if retryable
324
+ refresh_design_doc
325
+ retryable = false
326
+ retry
327
+ else
328
+ raise e
329
+ end
330
+ end
331
+ end
332
+
333
+ def design_doc_id
334
+ "_design/#{design_doc_slug}"
335
+ end
336
+
337
+ def design_doc_slug
338
+ return design_doc_slug_cache if design_doc_slug_cache && design_doc_fresh
339
+ funcs = []
340
+ design_doc['views'].each do |name, view|
341
+ funcs << "#{name}/#{view['map']}#{view['reduce']}"
342
+ end
343
+ md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
344
+ self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
345
+ end
346
+
347
+ def default_design_doc
348
+ {
349
+ "language" => "javascript",
350
+ "views" => {
351
+ 'all' => {
352
+ 'map' => "function(doc) {
353
+ if (doc['couchrest-type'] == '#{self.to_s}') {
354
+ emit(null,null);
355
+ }
356
+ }"
357
+ }
358
+ }
359
+ }
360
+ end
361
+
362
+ def refresh_design_doc
363
+ did = design_doc_id
364
+ saved = database.get(did) rescue nil
365
+ if saved
366
+ design_doc['views'].each do |name, view|
367
+ saved['views'][name] = view
368
+ end
369
+ database.save(saved)
370
+ design_doc['_id'] = did
371
+ design_doc.database = database
372
+ else
373
+ design_doc['_id'] = did
374
+ design_doc.delete('_rev')
375
+ design_doc.database = database
376
+ design_doc.save
377
+ end
378
+ self.design_doc_fresh = true
379
+ end
380
+ end
381
+
382
+ module InstanceMethods
383
+
384
+ # returns the database used by this model's class
385
+ def database
386
+ self.class.database
387
+ end
388
+
389
+ # Takes a hash as argument, and applies the values by using writer methods
390
+ # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
391
+ # missing. In case of error, no attributes are changed.
392
+ def update_attributes_without_saving hash
393
+ hash.each do |k, v|
394
+ raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=")
395
+ end
396
+ hash.each do |k, v|
397
+ @attributes[k] = v
398
+ end
399
+ end
400
+
401
+ # Takes a hash as argument, and applies the values by using writer methods
402
+ # for each key. Raises a NoMethodError if the corresponding methods are
403
+ # missing. In case of error, no attributes are changed.
404
+ def update_attributes hash
405
+ update_attributes_without_saving hash
406
+ save
407
+ end
408
+
409
+ def []= key, value
410
+ @attributes[key.to_s] = value
411
+ end
412
+
413
+ def [] key
414
+ @attributes[key.to_s]
415
+ end
416
+
417
+ def key? key
418
+ @attributes.key? key
419
+ end
420
+
421
+ def new_document?
422
+ !@attributes['_rev']
423
+ end
424
+
425
+ def id
426
+ @attributes.id
427
+ end
428
+
429
+ def identity_field
430
+ [CouchSurfer::Model, :id]
431
+ end
432
+
433
+ def rev
434
+ @attributes.rev
435
+ end
436
+
437
+ # Is this OK?
438
+ def == other_object
439
+ attributes == other_object.attributes
440
+ end
441
+
442
+ # def save
443
+ # @attributes.database = self.class.database
444
+ # @attributes.save
445
+ # end
446
+
447
+ # Overridden to set the unique ID.
448
+ def save bulk = false
449
+ if new_document?
450
+ create(bulk)
451
+ else
452
+ update(bulk)
453
+ end
454
+ end
455
+
456
+ # Saves the document to the db using create or update. Raises an exception
457
+ # if the document is not saved properly.
458
+ def save!
459
+ raise "#{self.inspect} failed to save" unless self.save
460
+ end
461
+
462
+ # Deletes the document from the database. Runs the :destroy callbacks.
463
+ # Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
464
+ # document to be saved to a new <tt>_id</tt>.
465
+ def destroy
466
+ result = database.delete self
467
+ if result['ok']
468
+ self['_rev'] = nil
469
+ self['_id'] = nil
470
+ end
471
+ result['ok']
472
+ end
473
+
474
+ # creates a file attachment to the current doc
475
+ def create_attachment(args={})
476
+ raise ArgumentError unless args[:file] && args[:name]
477
+ return if has_attachment?(args[:name])
478
+ self['_attachments'] ||= {}
479
+ set_attachment_attr(args)
480
+ rescue ArgumentError => e
481
+ raise ArgumentError, 'You must specify :file and :name'
482
+ end
483
+
484
+ # reads the data from an attachment
485
+ def read_attachment(attachment_name)
486
+ Base64.decode64(database.fetch_attachment(self.id, attachment_name))
487
+ end
488
+
489
+ # modifies a file attachment on the current doc
490
+ def update_attachment(args={})
491
+ raise ArgumentError unless args[:file] && args[:name]
492
+ return unless has_attachment?(args[:name])
493
+ delete_attachment(args[:name])
494
+ set_attachment_attr(args)
495
+ rescue ArgumentError => e
496
+ raise ArgumentError, 'You must specify :file and :name'
497
+ end
498
+
499
+ # deletes a file attachment from the current doc
500
+ def delete_attachment(attachment_name)
501
+ return unless self['_attachments']
502
+ self['_attachments'].delete attachment_name
503
+ end
504
+
505
+ # returns true if attachment_name exists
506
+ def has_attachment?(attachment_name)
507
+ !!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
508
+ end
509
+
510
+ # returns URL to fetch the attachment from
511
+ def attachment_url(attachment_name)
512
+ return unless has_attachment?(attachment_name)
513
+ "#{database.root}/#{self.id}/#{attachment_name}"
514
+ end
515
+
516
+
517
+ protected
518
+
519
+ def create bulk = false
520
+ set_unique_id if self.respond_to?(:set_unique_id)
521
+ save_doc(bulk)
522
+ end
523
+
524
+ def update bulk = false
525
+ save_doc(bulk)
526
+ end
527
+
528
+ def save_doc bulk = false
529
+ @attributes.database = self.class.database
530
+ @attributes.save(bulk)
531
+ end
532
+
533
+ private
534
+
535
+ def apply_defaults
536
+ return unless new_document?
537
+ if self.class.default
538
+ self.class.default.each do |k,v|
539
+ unless self.key?(k.to_s)
540
+ if v.class == Proc
541
+ self[k.to_s] = v.call
542
+ else
543
+ self[k.to_s] = Marshal.load(Marshal.dump(v))
544
+ end
545
+ end
546
+ end
547
+ end
548
+ end
549
+
550
+ def cast_keys
551
+ return unless self.class.casts
552
+ # TODO move the argument checking to the cast method for early crashes
553
+ self.class.casts.each do |k,v|
554
+ next unless self[k]
555
+ target = v[:as]
556
+ v[:send] || 'new'
557
+ if target.is_a?(Array)
558
+ klass = ::Extlib::Inflection.constantize(target[0])
559
+ self[k] = self[k].collect do |value|
560
+ (!v[:send] && klass == Time) ? Time.parse(value) : klass.send((v[:send] || 'new'), value)
561
+ end
562
+ else
563
+ self[k] = if (!v[:send] && target == 'Time')
564
+ Time.parse(self[k])
565
+ else
566
+ ::Extlib::Inflection.constantize(target).send((v[:send] || 'new'), self[k])
567
+ end
568
+ end
569
+ end
570
+ end
571
+
572
+ def encode_attachment(data)
573
+ Base64.encode64(data).gsub(/\r|\n/,'')
574
+ end
575
+
576
+ def get_mime_type(file)
577
+ MIME::Types.type_for(file.path).empty? ?
578
+ 'text\/plain' : MIME::Types.type_for(file.path).first.content_type.gsub(/\//,'\/')
579
+ end
580
+
581
+ def set_attachment_attr(args)
582
+ content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file])
583
+ self['_attachments'][args[:name]] = {
584
+ 'content-type' => content_type,
585
+ 'data' => encode_attachment(args[:file].read)
586
+ }
587
+ end
588
+ end
589
+
590
+ def self.included(receiver)
591
+ receiver.extend ClassMethods
592
+ receiver.send :include, InstanceMethods
593
+ receiver.class_eval do
594
+
595
+ attr_reader :attributes
596
+ cattr_accessor :default_database
597
+ class_inheritable_accessor :class_database
598
+ class_inheritable_accessor :default_obj
599
+ class_inheritable_accessor :casts
600
+ class_inheritable_accessor :design_doc
601
+ class_inheritable_accessor :design_doc_slug_cache
602
+ class_inheritable_accessor :design_doc_fresh
603
+
604
+ alias :new_record? :new_document?
605
+
606
+ def initialize attrs = {}
607
+ @attributes = CouchRest::Document.new(attrs)
608
+ apply_defaults
609
+ cast_keys
610
+ unless self['_id'] && self['_rev']
611
+ @attributes['couchrest-type'] = self.class.to_s
612
+ end
613
+ end
614
+
615
+ include ::Extlib::Hook
616
+ register_instance_hooks :save, :create, :destroy
617
+ end
618
+ end
619
+ end
620
+ end
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'extlib'
3
+ require 'validatable'
4
+
5
+ module CouchSurfer
6
+ module Validations
7
+ module ClassMethods
8
+ def validates_uniqueness_of *args
9
+ # add view, validation and before callbacks
10
+ options = args.last.is_a?(Hash) ? args.pop : {}
11
+ field = args.first
12
+ class_eval do
13
+ #view_by *args
14
+ validates_true_for args.first, :logic => lambda { is_unique?(field, options) }, :message => options[:message] || "is taken"
15
+ end
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ def is_unique?(field, options)
21
+ if options[:view].is_a?(Hash)
22
+ view_name = options[:view][:name]
23
+ query = options[:view][:query].is_a?(Proc) ? self.instance_eval(&options[:view][:query]) : nil
24
+ end
25
+ view_name ||= "by_#{field}"
26
+ query ||= {:key => self.send(field)}
27
+ result = self.class.send(view_name, query)
28
+ if result.blank?
29
+ return true
30
+ else
31
+ return !id.blank? && (id == result.first.id)
32
+ end
33
+ end
34
+
35
+ def validate_instance
36
+ throw(:halt, false) unless valid?
37
+ end
38
+ end
39
+
40
+ def self.included(receiver)
41
+ receiver.extend ClassMethods
42
+ receiver.send :include, Validatable
43
+ receiver.send :include, InstanceMethods
44
+ receiver.class_eval do
45
+ [:save].each do |method|
46
+ before method, :validate_instance
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,9 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ module CouchSurfer
5
+ VERSION = '0.0.1'
6
+ autoload :Model, 'couch_surfer/model'
7
+ autoload :Validations, 'couch_surfer/validations'
8
+ autoload :Associations, 'couch_surfer/associations'
9
+ end
@@ -0,0 +1,3 @@
1
+ This is an example README file.
2
+
3
+ More of the README, whee.
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Test</title>
5
+ </head>
6
+ <body>
7
+ <p>
8
+ Test
9
+ </p>
10
+ </body>
11
+ </html>
@@ -0,0 +1,3 @@
1
+ function globalLib() {
2
+ return "fixture";
3
+ };