couch_surfer 0.3.2

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