addywaddy-couch_surfer 0.0.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.
@@ -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
+ };