jchris-couchrest 0.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.
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')