mattetti-couchrest 0.14.2 → 0.15

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