jchris-couchrest 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +93 -0
  3. data/Rakefile +66 -0
  4. data/THANKS.md +18 -0
  5. data/examples/model/example.rb +144 -0
  6. data/examples/word_count/markov +38 -0
  7. data/examples/word_count/views/books/chunked-map.js +3 -0
  8. data/examples/word_count/views/books/united-map.js +1 -0
  9. data/examples/word_count/views/markov/chain-map.js +6 -0
  10. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  11. data/examples/word_count/views/word_count/count-map.js +6 -0
  12. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  13. data/examples/word_count/word_count.rb +46 -0
  14. data/examples/word_count/word_count_query.rb +40 -0
  15. data/examples/word_count/word_count_views.rb +26 -0
  16. data/lib/couchrest/commands/generate.rb +71 -0
  17. data/lib/couchrest/commands/push.rb +103 -0
  18. data/lib/couchrest/core/database.rb +313 -0
  19. data/lib/couchrest/core/design.rb +89 -0
  20. data/lib/couchrest/core/document.rb +96 -0
  21. data/lib/couchrest/core/response.rb +16 -0
  22. data/lib/couchrest/core/server.rb +88 -0
  23. data/lib/couchrest/core/view.rb +4 -0
  24. data/lib/couchrest/helper/pager.rb +103 -0
  25. data/lib/couchrest/helper/streamer.rb +44 -0
  26. data/lib/couchrest/mixins/attachments.rb +31 -0
  27. data/lib/couchrest/mixins/callbacks.rb +442 -0
  28. data/lib/couchrest/mixins/design_doc.rb +63 -0
  29. data/lib/couchrest/mixins/document_queries.rb +48 -0
  30. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  31. data/lib/couchrest/mixins/extended_document_mixins.rb +6 -0
  32. data/lib/couchrest/mixins/properties.rb +125 -0
  33. data/lib/couchrest/mixins/validation.rb +234 -0
  34. data/lib/couchrest/mixins/views.rb +169 -0
  35. data/lib/couchrest/mixins.rb +4 -0
  36. data/lib/couchrest/monkeypatches.rb +113 -0
  37. data/lib/couchrest/more/casted_model.rb +28 -0
  38. data/lib/couchrest/more/extended_document.rb +217 -0
  39. data/lib/couchrest/more/property.rb +40 -0
  40. data/lib/couchrest/support/blank.rb +42 -0
  41. data/lib/couchrest/support/class.rb +175 -0
  42. data/lib/couchrest/validation/auto_validate.rb +163 -0
  43. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  44. data/lib/couchrest/validation/validation_errors.rb +118 -0
  45. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  46. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  47. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  48. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  49. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  50. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  51. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  52. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  53. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  54. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  55. data/lib/couchrest.rb +189 -0
  56. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  57. data/spec/couchrest/core/database_spec.rb +745 -0
  58. data/spec/couchrest/core/design_spec.rb +131 -0
  59. data/spec/couchrest/core/document_spec.rb +311 -0
  60. data/spec/couchrest/core/server_spec.rb +35 -0
  61. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  62. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  63. data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
  64. data/spec/couchrest/more/casted_model_spec.rb +97 -0
  65. data/spec/couchrest/more/extended_doc_attachment_spec.rb +129 -0
  66. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  67. data/spec/couchrest/more/extended_doc_view_spec.rb +207 -0
  68. data/spec/couchrest/more/property_spec.rb +129 -0
  69. data/spec/fixtures/attachments/README +3 -0
  70. data/spec/fixtures/attachments/couchdb.png +0 -0
  71. data/spec/fixtures/attachments/test.html +11 -0
  72. data/spec/fixtures/more/article.rb +34 -0
  73. data/spec/fixtures/more/card.rb +20 -0
  74. data/spec/fixtures/more/course.rb +14 -0
  75. data/spec/fixtures/more/event.rb +6 -0
  76. data/spec/fixtures/more/invoice.rb +17 -0
  77. data/spec/fixtures/more/person.rb +8 -0
  78. data/spec/fixtures/more/question.rb +6 -0
  79. data/spec/fixtures/more/service.rb +12 -0
  80. data/spec/fixtures/views/lib.js +3 -0
  81. data/spec/fixtures/views/test_view/lib.js +3 -0
  82. data/spec/fixtures/views/test_view/only-map.js +4 -0
  83. data/spec/fixtures/views/test_view/test-map.js +3 -0
  84. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  85. data/spec/spec.opts +6 -0
  86. data/spec/spec_helper.rb +26 -0
  87. data/utils/remap.rb +27 -0
  88. data/utils/subset.rb +30 -0
  89. metadata +217 -0
@@ -0,0 +1,442 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'support', 'class')
2
+
3
+ # Extracted from ActiveSupport::Callbacks written by Yehuda Katz
4
+ # http://github.com/wycats/rails/raw/18b405f154868204a8f332888871041a7bad95e1/activesupport/lib/active_support/callbacks.rb
5
+
6
+ module CouchRest
7
+ # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
8
+ # before or after an alteration of the object state.
9
+ #
10
+ # Mixing in this module allows you to define callbacks in your class.
11
+ #
12
+ # Example:
13
+ # class Storage
14
+ # include CouchRest::Callbacks
15
+ #
16
+ # define_callbacks :save
17
+ # end
18
+ #
19
+ # class ConfigStorage < Storage
20
+ # save_callback :before, :saving_message
21
+ # def saving_message
22
+ # puts "saving..."
23
+ # end
24
+ #
25
+ # save_callback :after do |object|
26
+ # puts "saved"
27
+ # end
28
+ #
29
+ # def save
30
+ # _run_save_callbacks do
31
+ # puts "- save"
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # config = ConfigStorage.new
37
+ # config.save
38
+ #
39
+ # Output:
40
+ # saving...
41
+ # - save
42
+ # saved
43
+ #
44
+ # Callbacks from parent classes are inherited.
45
+ #
46
+ # Example:
47
+ # class Storage
48
+ # include CouchRest::Callbacks
49
+ #
50
+ # define_callbacks :save
51
+ #
52
+ # save_callback :before, :prepare
53
+ # def prepare
54
+ # puts "preparing save"
55
+ # end
56
+ # end
57
+ #
58
+ # class ConfigStorage < Storage
59
+ # save_callback :before, :saving_message
60
+ # def saving_message
61
+ # puts "saving..."
62
+ # end
63
+ #
64
+ # save_callback :after do |object|
65
+ # puts "saved"
66
+ # end
67
+ #
68
+ # def save
69
+ # _run_save_callbacks do
70
+ # puts "- save"
71
+ # end
72
+ # end
73
+ # end
74
+ #
75
+ # config = ConfigStorage.new
76
+ # config.save
77
+ #
78
+ # Output:
79
+ # preparing save
80
+ # saving...
81
+ # - save
82
+ # saved
83
+ module Callbacks
84
+ def self.included(klass)
85
+ klass.extend ClassMethods
86
+ end
87
+
88
+ def run_callbacks(kind, options = {})
89
+ send("_run_#{kind}_callbacks")
90
+ end
91
+
92
+ class Callback
93
+ @@_callback_sequence = 0
94
+
95
+ attr_accessor :filter, :kind, :name, :options, :per_key, :klass
96
+ def initialize(filter, kind, options, klass, name)
97
+ @kind, @klass = kind, klass
98
+ @name = name
99
+
100
+ normalize_options!(options)
101
+
102
+ @per_key = options.delete(:per_key)
103
+ @raw_filter, @options = filter, options
104
+ @filter = _compile_filter(filter)
105
+ @compiled_options = _compile_options(options)
106
+ @callback_id = next_id
107
+
108
+ _compile_per_key_options
109
+ end
110
+
111
+ def clone(klass)
112
+ obj = super()
113
+ obj.klass = klass
114
+ obj.per_key = @per_key.dup
115
+ obj.options = @options.dup
116
+ obj.per_key[:if] = @per_key[:if].dup
117
+ obj.per_key[:unless] = @per_key[:unless].dup
118
+ obj.options[:if] = @options[:if].dup
119
+ obj.options[:unless] = @options[:unless].dup
120
+ obj
121
+ end
122
+
123
+ def normalize_options!(options)
124
+ options[:if] = Array(options[:if])
125
+ options[:unless] = Array(options[:unless])
126
+
127
+ options[:per_key] ||= {}
128
+ options[:per_key][:if] = Array(options[:per_key][:if])
129
+ options[:per_key][:unless] = Array(options[:per_key][:unless])
130
+ end
131
+
132
+ def next_id
133
+ @@_callback_sequence += 1
134
+ end
135
+
136
+ def matches?(_kind, _name, _filter)
137
+ @kind == _kind &&
138
+ @name == _name &&
139
+ @filter == _filter
140
+ end
141
+
142
+ def _update_filter(filter_options, new_options)
143
+ filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
144
+ filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
145
+ end
146
+
147
+ def recompile!(_options, _per_key)
148
+ _update_filter(self.options, _options)
149
+ _update_filter(self.per_key, _per_key)
150
+
151
+ @callback_id = next_id
152
+ @filter = _compile_filter(@raw_filter)
153
+ @compiled_options = _compile_options(@options)
154
+ _compile_per_key_options
155
+ end
156
+
157
+ def _compile_per_key_options
158
+ key_options = _compile_options(@per_key)
159
+
160
+ @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
161
+ def _one_time_conditions_valid_#{@callback_id}?
162
+ true #{key_options[0]}
163
+ end
164
+ RUBY_EVAL
165
+ end
166
+
167
+ # This will supply contents for before and around filters, and no
168
+ # contents for after filters (for the forward pass).
169
+ def start(key = nil, object = nil)
170
+ return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
171
+
172
+ # options[0] is the compiled form of supplied conditions
173
+ # options[1] is the "end" for the conditional
174
+
175
+ if @kind == :before || @kind == :around
176
+ if @kind == :before
177
+ # if condition # before_save :filter_name, :if => :condition
178
+ # filter_name
179
+ # end
180
+ [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
181
+ elsif @compiled_options[0]
182
+ # Compile around filters with conditions into proxy methods
183
+ # that contain the conditions.
184
+ #
185
+ # For `around_save :filter_name, :if => :condition':
186
+ #
187
+ # def _conditional_callback_save_17
188
+ # if condition
189
+ # filter_name do
190
+ # yield self
191
+ # end
192
+ # else
193
+ # yield self
194
+ # end
195
+ # end
196
+
197
+ name = "_conditional_callback_#{@kind}_#{next_id}"
198
+ txt = <<-RUBY_EVAL
199
+ def #{name}
200
+ #{@compiled_options[0]}
201
+ #{@filter} do
202
+ yield self
203
+ end
204
+ else
205
+ yield self
206
+ end
207
+ end
208
+ RUBY_EVAL
209
+ @klass.class_eval(txt)
210
+ "#{name} do"
211
+ else
212
+ "#{@filter} do"
213
+ end
214
+ end
215
+ end
216
+
217
+ # This will supply contents for around and after filters, but not
218
+ # before filters (for the backward pass).
219
+ def end(key = nil, object = nil)
220
+ return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
221
+
222
+ if @kind == :around || @kind == :after
223
+ # if condition # after_save :filter_name, :if => :condition
224
+ # filter_name
225
+ # end
226
+ if @kind == :after
227
+ [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
228
+ else
229
+ "end"
230
+ end
231
+ end
232
+ end
233
+
234
+ private
235
+ # Options support the same options as filters themselves (and support
236
+ # symbols, string, procs, and objects), so compile a conditional
237
+ # expression based on the options
238
+ def _compile_options(options)
239
+ return [] if options[:if].empty? && options[:unless].empty?
240
+
241
+ conditions = []
242
+
243
+ unless options[:if].empty?
244
+ conditions << Array(_compile_filter(options[:if]))
245
+ end
246
+
247
+ unless options[:unless].empty?
248
+ conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"}
249
+ end
250
+
251
+ ["if #{conditions.flatten.join(" && ")}", "end"]
252
+ end
253
+
254
+ # Filters support:
255
+ # Arrays:: Used in conditions. This is used to specify
256
+ # multiple conditions. Used internally to
257
+ # merge conditions from skip_* filters
258
+ # Symbols:: A method to call
259
+ # Strings:: Some content to evaluate
260
+ # Procs:: A proc to call with the object
261
+ # Objects:: An object with a before_foo method on it to call
262
+ #
263
+ # All of these objects are compiled into methods and handled
264
+ # the same after this point:
265
+ # Arrays:: Merged together into a single filter
266
+ # Symbols:: Already methods
267
+ # Strings:: class_eval'ed into methods
268
+ # Procs:: define_method'ed into methods
269
+ # Objects::
270
+ # a method is created that calls the before_foo method
271
+ # on the object.
272
+ def _compile_filter(filter)
273
+ method_name = "_callback_#{@kind}_#{next_id}"
274
+ case filter
275
+ when Array
276
+ filter.map {|f| _compile_filter(f)}
277
+ when Symbol
278
+ filter
279
+ when Proc
280
+ @klass.send(:define_method, method_name, &filter)
281
+ method_name << (filter.arity == 1 ? "(self)" : "")
282
+ when String
283
+ @klass.class_eval <<-RUBY_EVAL
284
+ def #{method_name}
285
+ #{filter}
286
+ end
287
+ RUBY_EVAL
288
+ method_name
289
+ else
290
+ kind, name = @kind, @name
291
+ @klass.send(:define_method, method_name) do
292
+ filter.send("#{kind}_#{name}", self)
293
+ end
294
+ method_name
295
+ end
296
+ end
297
+ end
298
+
299
+ # This method_missing is supplied to catch callbacks with keys and create
300
+ # the appropriate callback for future use.
301
+ def method_missing(meth, *args, &blk)
302
+ if meth.to_s =~ /_run_(\w+)_(\w+)_(\w+)_callbacks/
303
+ return self.class._create_and_run_keyed_callback($1, $2.to_sym, $3.to_sym, self, &blk)
304
+ end
305
+ super
306
+ end
307
+
308
+ # An Array with a compile method
309
+ class CallbackChain < Array
310
+ def compile(key = nil, object = nil)
311
+ method = []
312
+ each do |callback|
313
+ method << callback.start(key, object)
314
+ end
315
+ method << "yield self"
316
+ reverse_each do |callback|
317
+ method << callback.end(key, object)
318
+ end
319
+ method.compact.join("\n")
320
+ end
321
+
322
+ def clone(klass)
323
+ CallbackChain.new(map {|c| c.clone(klass)})
324
+ end
325
+ end
326
+
327
+ module ClassMethods
328
+ CHAINS = {:before => :before, :around => :before, :after => :after} unless self.const_defined?("CHAINS")
329
+
330
+ # Make the _run_save_callbacks method. The generated method takes
331
+ # a block that it'll yield to. It'll call the before and around filters
332
+ # in order, yield the block, and then run the after filters.
333
+ #
334
+ # _run_save_callbacks do
335
+ # save
336
+ # end
337
+ #
338
+ # The _run_save_callbacks method can optionally take a key, which
339
+ # will be used to compile an optimized callback method for each
340
+ # key. See #define_callbacks for more information.
341
+ def _define_runner(symbol, str, options)
342
+ self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
343
+ def _run_#{symbol}_callbacks(key = nil)
344
+ if key
345
+ send("_run_\#{self.class}_#{symbol}_\#{key}_callbacks") { yield }
346
+ else
347
+ #{str}
348
+ end
349
+ end
350
+ RUBY_EVAL
351
+
352
+ before_name, around_name, after_name =
353
+ options.values_at(:before, :after, :around)
354
+ end
355
+
356
+ # This is called the first time a callback is called with a particular
357
+ # key. It creates a new callback method for the key, calculating
358
+ # which callbacks can be omitted because of per_key conditions.
359
+ def _create_and_run_keyed_callback(klass, kind, key, obj, &blk)
360
+ @_keyed_callbacks ||= {}
361
+ @_keyed_callbacks[[kind, key]] ||= begin
362
+ str = self.send("_#{kind}_callbacks").compile(key, obj)
363
+ self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
364
+ def _run_#{klass}_#{kind}_#{key}_callbacks
365
+ #{str}
366
+ end
367
+ RUBY_EVAL
368
+ true
369
+ end
370
+ obj.send("_run_#{klass}_#{kind}_#{key}_callbacks", &blk)
371
+ end
372
+
373
+ # Define callbacks.
374
+ #
375
+ # Creates a <name>_callback method that you can use to add callbacks.
376
+ #
377
+ # Syntax:
378
+ # save_callback :before, :before_meth
379
+ # save_callback :after, :after_meth, :if => :condition
380
+ # save_callback :around {|r| stuff; yield; stuff }
381
+ #
382
+ # The <name>_callback method also updates the _run_<name>_callbacks
383
+ # method, which is the public API to run the callbacks.
384
+ #
385
+ # Also creates a skip_<name>_callback method that you can use to skip
386
+ # callbacks.
387
+ #
388
+ # When creating or skipping callbacks, you can specify conditions that
389
+ # are always the same for a given key. For instance, in ActionPack,
390
+ # we convert :only and :except conditions into per-key conditions.
391
+ #
392
+ # before_filter :authenticate, :except => "index"
393
+ # becomes
394
+ # dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
395
+ #
396
+ # Per-Key conditions are evaluated only once per use of a given key.
397
+ # In the case of the above example, you would do:
398
+ #
399
+ # run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
400
+ #
401
+ # In that case, each action_name would get its own compiled callback
402
+ # method that took into consideration the per_key conditions. This
403
+ # is a speed improvement for ActionPack.
404
+ def define_callbacks(*symbols)
405
+ symbols.each do |symbol|
406
+ self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
407
+ class_inheritable_accessor :_#{symbol}_callbacks
408
+ self._#{symbol}_callbacks = CallbackChain.new
409
+
410
+ def self.#{symbol}_callback(type, *filters, &blk)
411
+ options = filters.last.is_a?(Hash) ? filters.pop : {}
412
+ filters.unshift(blk) if block_given?
413
+ filters.map! {|f| Callback.new(f, type, options.dup, self, :#{symbol})}
414
+ self._#{symbol}_callbacks.push(*filters)
415
+ _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, options)
416
+ end
417
+
418
+ def self.skip_#{symbol}_callback(type, *filters, &blk)
419
+ options = filters.last.is_a?(Hash) ? filters.pop : {}
420
+ filters.unshift(blk) if block_given?
421
+ filters.each do |filter|
422
+ self._#{symbol}_callbacks = self._#{symbol}_callbacks.clone(self)
423
+
424
+ filter = self._#{symbol}_callbacks.find {|c| c.matches?(type, :#{symbol}, filter) }
425
+ per_key = options[:per_key] || {}
426
+ if filter
427
+ filter.recompile!(options, per_key)
428
+ else
429
+ self._#{symbol}_callbacks.delete(filter)
430
+ end
431
+ _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, options)
432
+ end
433
+
434
+ end
435
+
436
+ self.#{symbol}_callback(:before)
437
+ RUBY_EVAL
438
+ end
439
+ end
440
+ end
441
+ end
442
+ end
@@ -0,0 +1,63 @@
1
+ require 'digest/md5'
2
+
3
+ module CouchRest
4
+ module Mixins
5
+ module DesignDoc
6
+
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def design_doc_id
13
+ "_design/#{design_doc_slug}"
14
+ end
15
+
16
+ def design_doc_slug
17
+ return design_doc_slug_cache if (design_doc_slug_cache && design_doc_fresh)
18
+ funcs = []
19
+ design_doc['views'].each do |name, view|
20
+ funcs << "#{name}/#{view['map']}#{view['reduce']}"
21
+ end
22
+ md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
23
+ self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
24
+ end
25
+
26
+ def default_design_doc
27
+ {
28
+ "language" => "javascript",
29
+ "views" => {
30
+ 'all' => {
31
+ 'map' => "function(doc) {
32
+ if (doc['couchrest-type'] == '#{self.to_s}') {
33
+ emit(null,null);
34
+ }
35
+ }"
36
+ }
37
+ }
38
+ }
39
+ end
40
+
41
+ def refresh_design_doc
42
+ did = design_doc_id
43
+ saved = database.get(did) rescue nil
44
+ if saved
45
+ design_doc['views'].each do |name, view|
46
+ saved['views'][name] = view
47
+ end
48
+ database.save_doc(saved)
49
+ self.design_doc = saved
50
+ else
51
+ design_doc['_id'] = did
52
+ design_doc.delete('_rev')
53
+ design_doc.database = database
54
+ design_doc.save
55
+ end
56
+ self.design_doc_fresh = true
57
+ end
58
+
59
+ end # module ClassMethods
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,48 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module DocumentQueries
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # Load all documents that have the "couchrest-type" field equal to the
12
+ # name of the current class. Take the standard set of
13
+ # CouchRest::Database#view options.
14
+ def all(opts = {}, &block)
15
+ self.design_doc ||= Design.new(default_design_doc)
16
+ unless design_doc_fresh
17
+ refresh_design_doc
18
+ end
19
+ view(:all, opts, &block)
20
+ end
21
+
22
+ # Load the first document that have the "couchrest-type" field equal to
23
+ # the name of the current class.
24
+ #
25
+ # ==== Returns
26
+ # Object:: The first object instance available
27
+ # or
28
+ # Nil:: if no instances available
29
+ #
30
+ # ==== Parameters
31
+ # opts<Hash>::
32
+ # View options, see <tt>CouchRest::Database#view</tt> options for more info.
33
+ def first(opts = {})
34
+ first_instance = self.all(opts.merge!(:limit => 1))
35
+ first_instance.empty? ? nil : first_instance.first
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
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,68 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module ExtendedAttachments
4
+
5
+ # creates a file attachment to the current doc
6
+ def create_attachment(args={})
7
+ raise ArgumentError unless args[:file] && args[:name]
8
+ return if has_attachment?(args[:name])
9
+ self['_attachments'] ||= {}
10
+ set_attachment_attr(args)
11
+ rescue ArgumentError => e
12
+ raise ArgumentError, 'You must specify :file and :name'
13
+ end
14
+
15
+ # reads the data from an attachment
16
+ def read_attachment(attachment_name)
17
+ Base64.decode64(database.fetch_attachment(self, attachment_name))
18
+ end
19
+
20
+ # modifies a file attachment on the current doc
21
+ def update_attachment(args={})
22
+ raise ArgumentError unless args[:file] && args[:name]
23
+ return unless has_attachment?(args[:name])
24
+ delete_attachment(args[:name])
25
+ set_attachment_attr(args)
26
+ rescue ArgumentError => e
27
+ raise ArgumentError, 'You must specify :file and :name'
28
+ end
29
+
30
+ # deletes a file attachment from the current doc
31
+ def delete_attachment(attachment_name)
32
+ return unless self['_attachments']
33
+ self['_attachments'].delete attachment_name
34
+ end
35
+
36
+ # returns true if attachment_name exists
37
+ def has_attachment?(attachment_name)
38
+ !!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
39
+ end
40
+
41
+ # returns URL to fetch the attachment from
42
+ def attachment_url(attachment_name)
43
+ return unless has_attachment?(attachment_name)
44
+ "#{database.root}/#{self.id}/#{attachment_name}"
45
+ end
46
+
47
+ private
48
+
49
+ def encode_attachment(data)
50
+ ::Base64.encode64(data).gsub(/\r|\n/,'')
51
+ end
52
+
53
+ def get_mime_type(file)
54
+ ::MIME::Types.type_for(file.path).empty? ?
55
+ 'text\/plain' : MIME::Types.type_for(file.path).first.content_type.gsub(/\//,'\/')
56
+ end
57
+
58
+ def set_attachment_attr(args)
59
+ content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file])
60
+ self['_attachments'][args[:name]] = {
61
+ 'content-type' => content_type,
62
+ 'data' => encode_attachment(args[:file].read)
63
+ }
64
+ end
65
+
66
+ end # module ExtendedAttachments
67
+ end
68
+ end
@@ -0,0 +1,6 @@
1
+ require File.join(File.dirname(__FILE__), 'properties')
2
+ require File.join(File.dirname(__FILE__), 'document_queries')
3
+ require File.join(File.dirname(__FILE__), 'views')
4
+ require File.join(File.dirname(__FILE__), 'design_doc')
5
+ require File.join(File.dirname(__FILE__), 'validation')
6
+ require File.join(File.dirname(__FILE__), 'extended_attachments')