jchris-couchrest 0.12.6 → 0.16

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 (59) hide show
  1. data/README.md +33 -8
  2. data/Rakefile +1 -1
  3. data/examples/model/example.rb +19 -13
  4. data/lib/couchrest.rb +27 -2
  5. data/lib/couchrest/core/database.rb +113 -41
  6. data/lib/couchrest/core/document.rb +48 -27
  7. data/lib/couchrest/core/response.rb +15 -0
  8. data/lib/couchrest/core/server.rb +47 -10
  9. data/lib/couchrest/mixins.rb +4 -0
  10. data/lib/couchrest/mixins/attachments.rb +31 -0
  11. data/lib/couchrest/mixins/callbacks.rb +442 -0
  12. data/lib/couchrest/mixins/design_doc.rb +63 -0
  13. data/lib/couchrest/mixins/document_queries.rb +48 -0
  14. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  15. data/lib/couchrest/mixins/extended_document_mixins.rb +6 -0
  16. data/lib/couchrest/mixins/properties.rb +120 -0
  17. data/lib/couchrest/mixins/validation.rb +234 -0
  18. data/lib/couchrest/mixins/views.rb +168 -0
  19. data/lib/couchrest/monkeypatches.rb +75 -0
  20. data/lib/couchrest/more/casted_model.rb +28 -0
  21. data/lib/couchrest/more/extended_document.rb +215 -0
  22. data/lib/couchrest/more/property.rb +40 -0
  23. data/lib/couchrest/support/blank.rb +42 -0
  24. data/lib/couchrest/support/class.rb +175 -0
  25. data/lib/couchrest/validation/auto_validate.rb +163 -0
  26. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  27. data/lib/couchrest/validation/validation_errors.rb +118 -0
  28. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  29. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  30. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  31. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  32. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  33. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  34. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  35. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  36. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  37. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  38. data/spec/couchrest/core/database_spec.rb +183 -67
  39. data/spec/couchrest/core/design_spec.rb +1 -1
  40. data/spec/couchrest/core/document_spec.rb +271 -173
  41. data/spec/couchrest/core/server_spec.rb +35 -0
  42. data/spec/couchrest/helpers/pager_spec.rb +1 -1
  43. data/spec/couchrest/more/casted_model_spec.rb +97 -0
  44. data/spec/couchrest/more/extended_doc_attachment_spec.rb +129 -0
  45. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  46. data/spec/couchrest/more/extended_doc_view_spec.rb +204 -0
  47. data/spec/couchrest/more/property_spec.rb +129 -0
  48. data/spec/fixtures/more/article.rb +34 -0
  49. data/spec/fixtures/more/card.rb +20 -0
  50. data/spec/fixtures/more/course.rb +14 -0
  51. data/spec/fixtures/more/event.rb +6 -0
  52. data/spec/fixtures/more/invoice.rb +17 -0
  53. data/spec/fixtures/more/person.rb +8 -0
  54. data/spec/fixtures/more/question.rb +6 -0
  55. data/spec/fixtures/more/service.rb +12 -0
  56. data/spec/spec_helper.rb +13 -7
  57. metadata +76 -3
  58. data/lib/couchrest/core/model.rb +0 -613
  59. data/spec/couchrest/core/model_spec.rb +0 -855
@@ -0,0 +1,12 @@
1
+ class Service < CouchRest::ExtendedDocument
2
+ # Include the validation module to get access to the validation methods
3
+ include CouchRest::Validation
4
+ auto_validate!
5
+ # Set the default database to use
6
+ use_database TEST_SERVER.default_database
7
+
8
+ # Official Schema
9
+ property :name, :length => 4...20
10
+ property :price, :type => Integer
11
+
12
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,20 +1,26 @@
1
1
  require "rubygems"
2
2
  require "spec" # Satisfies Autotest and anyone else not using the Rake tasks
3
3
 
4
- require File.dirname(__FILE__) + '/../lib/couchrest'
4
+ require File.join(File.dirname(__FILE__), '..','lib','couchrest')
5
+ # check the following file to see how to use the spec'd features.
5
6
 
6
7
  unless defined?(FIXTURE_PATH)
7
- FIXTURE_PATH = File.dirname(__FILE__) + '/fixtures'
8
- SCRATCH_PATH = File.dirname(__FILE__) + '/tmp'
8
+ FIXTURE_PATH = File.join(File.dirname(__FILE__), '/fixtures')
9
+ SCRATCH_PATH = File.join(File.dirname(__FILE__), '/tmp')
9
10
 
10
11
  COUCHHOST = "http://127.0.0.1:5984"
11
- TESTDB = 'couchrest-test'
12
+ TESTDB = 'couchrest-test'
13
+ TEST_SERVER = CouchRest.new
14
+ TEST_SERVER.default_database = TESTDB
15
+ end
16
+
17
+ class Basic < CouchRest::ExtendedDocument
18
+ use_database TEST_SERVER.default_database
12
19
  end
13
20
 
14
21
  def reset_test_db!
15
- cr = CouchRest.new(COUCHHOST)
22
+ cr = TEST_SERVER
16
23
  db = cr.database(TESTDB)
17
- db.delete! rescue nil
18
- db = cr.create_db(TESTDB) rescue nin
24
+ db.recreate! rescue nil
19
25
  db
20
26
  end
metadata CHANGED
@@ -1,10 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jchris-couchrest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.6
4
+ version: "0.16"
5
5
  platform: ruby
6
6
  authors:
7
7
  - J. Chris Anderson
8
+ - Matt Aimonetti
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
@@ -14,6 +15,7 @@ default_executable:
14
15
  dependencies:
15
16
  - !ruby/object:Gem::Dependency
16
17
  name: json
18
+ type: :runtime
17
19
  version_requirement:
18
20
  version_requirements: !ruby/object:Gem::Requirement
19
21
  requirements:
@@ -23,6 +25,7 @@ dependencies:
23
25
  version:
24
26
  - !ruby/object:Gem::Dependency
25
27
  name: rest-client
28
+ type: :runtime
26
29
  version_requirement:
27
30
  version_requirements: !ruby/object:Gem::Requirement
28
31
  requirements:
@@ -32,6 +35,7 @@ dependencies:
32
35
  version:
33
36
  - !ruby/object:Gem::Dependency
34
37
  name: mime-types
38
+ type: :runtime
35
39
  version_requirement:
36
40
  version_requirements: !ruby/object:Gem::Requirement
37
41
  requirements:
@@ -79,13 +83,47 @@ files:
79
83
  - lib/couchrest/core/database.rb
80
84
  - lib/couchrest/core/design.rb
81
85
  - lib/couchrest/core/document.rb
82
- - lib/couchrest/core/model.rb
86
+ - lib/couchrest/core/response.rb
83
87
  - lib/couchrest/core/server.rb
84
88
  - lib/couchrest/core/view.rb
85
89
  - lib/couchrest/helper
86
90
  - lib/couchrest/helper/pager.rb
87
91
  - lib/couchrest/helper/streamer.rb
92
+ - lib/couchrest/mixins
93
+ - lib/couchrest/mixins/attachments.rb
94
+ - lib/couchrest/mixins/callbacks.rb
95
+ - lib/couchrest/mixins/design_doc.rb
96
+ - lib/couchrest/mixins/document_queries.rb
97
+ - lib/couchrest/mixins/extended_attachments.rb
98
+ - lib/couchrest/mixins/extended_document_mixins.rb
99
+ - lib/couchrest/mixins/properties.rb
100
+ - lib/couchrest/mixins/validation.rb
101
+ - lib/couchrest/mixins/views.rb
102
+ - lib/couchrest/mixins.rb
88
103
  - lib/couchrest/monkeypatches.rb
104
+ - lib/couchrest/more
105
+ - lib/couchrest/more/casted_model.rb
106
+ - lib/couchrest/more/extended_document.rb
107
+ - lib/couchrest/more/property.rb
108
+ - lib/couchrest/support
109
+ - lib/couchrest/support/blank.rb
110
+ - lib/couchrest/support/class.rb
111
+ - lib/couchrest/validation
112
+ - lib/couchrest/validation/auto_validate.rb
113
+ - lib/couchrest/validation/contextual_validators.rb
114
+ - lib/couchrest/validation/validation_errors.rb
115
+ - lib/couchrest/validation/validators
116
+ - lib/couchrest/validation/validators/absent_field_validator.rb
117
+ - lib/couchrest/validation/validators/confirmation_validator.rb
118
+ - lib/couchrest/validation/validators/format_validator.rb
119
+ - lib/couchrest/validation/validators/formats
120
+ - lib/couchrest/validation/validators/formats/email.rb
121
+ - lib/couchrest/validation/validators/formats/url.rb
122
+ - lib/couchrest/validation/validators/generic_validator.rb
123
+ - lib/couchrest/validation/validators/length_validator.rb
124
+ - lib/couchrest/validation/validators/method_validator.rb
125
+ - lib/couchrest/validation/validators/numeric_validator.rb
126
+ - lib/couchrest/validation/validators/required_field_validator.rb
89
127
  - lib/couchrest.rb
90
128
  - spec/couchrest
91
129
  - spec/couchrest/core
@@ -93,15 +131,50 @@ files:
93
131
  - spec/couchrest/core/database_spec.rb
94
132
  - spec/couchrest/core/design_spec.rb
95
133
  - spec/couchrest/core/document_spec.rb
96
- - spec/couchrest/core/model_spec.rb
134
+ - spec/couchrest/core/server_spec.rb
97
135
  - spec/couchrest/helpers
98
136
  - spec/couchrest/helpers/pager_spec.rb
99
137
  - spec/couchrest/helpers/streamer_spec.rb
138
+ - spec/couchrest/more
139
+ - spec/couchrest/more/casted_model_spec.rb
140
+ - spec/couchrest/more/extended_doc_attachment_spec.rb
141
+ - spec/couchrest/more/extended_doc_spec.rb
142
+ - spec/couchrest/more/extended_doc_view_spec.rb
143
+ - spec/couchrest/more/property_spec.rb
100
144
  - spec/fixtures
101
145
  - spec/fixtures/attachments
102
146
  - spec/fixtures/attachments/couchdb.png
103
147
  - spec/fixtures/attachments/README
104
148
  - spec/fixtures/attachments/test.html
149
+ - spec/fixtures/couchapp
150
+ - spec/fixtures/couchapp/_attachments
151
+ - spec/fixtures/couchapp/_attachments/index.html
152
+ - spec/fixtures/couchapp/doc.json
153
+ - spec/fixtures/couchapp/foo
154
+ - spec/fixtures/couchapp/foo/bar.txt
155
+ - spec/fixtures/couchapp/foo/test.json
156
+ - spec/fixtures/couchapp/test.json
157
+ - spec/fixtures/couchapp/views
158
+ - spec/fixtures/couchapp/views/example-map.js
159
+ - spec/fixtures/couchapp/views/example-reduce.js
160
+ - spec/fixtures/couchapp-test
161
+ - spec/fixtures/couchapp-test/my-app
162
+ - spec/fixtures/couchapp-test/my-app/_attachments
163
+ - spec/fixtures/couchapp-test/my-app/_attachments/index.html
164
+ - spec/fixtures/couchapp-test/my-app/foo
165
+ - spec/fixtures/couchapp-test/my-app/foo/bar.txt
166
+ - spec/fixtures/couchapp-test/my-app/views
167
+ - spec/fixtures/couchapp-test/my-app/views/example-map.js
168
+ - spec/fixtures/couchapp-test/my-app/views/example-reduce.js
169
+ - spec/fixtures/more
170
+ - spec/fixtures/more/article.rb
171
+ - spec/fixtures/more/card.rb
172
+ - spec/fixtures/more/course.rb
173
+ - spec/fixtures/more/event.rb
174
+ - spec/fixtures/more/invoice.rb
175
+ - spec/fixtures/more/person.rb
176
+ - spec/fixtures/more/question.rb
177
+ - spec/fixtures/more/service.rb
105
178
  - spec/fixtures/views
106
179
  - spec/fixtures/views/lib.js
107
180
  - spec/fixtures/views/test_view
@@ -1,613 +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({
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
- def save bulk = false
489
- set_unique_id if new_document? && self.respond_to?(:set_unique_id)
490
- super(bulk)
491
- end
492
-
493
- # Saves the document to the db using create or update. Raises an exception
494
- # if the document is not saved properly.
495
- def save!
496
- raise "#{self.inspect} failed to save" unless self.save
497
- end
498
-
499
- # Deletes the document from the database. Runs the :destroy callbacks.
500
- # Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
501
- # document to be saved to a new <tt>_id</tt>.
502
- def destroy
503
- result = database.delete self
504
- if result['ok']
505
- self['_rev'] = nil
506
- self['_id'] = nil
507
- end
508
- result['ok']
509
- end
510
-
511
- # creates a file attachment to the current doc
512
- def create_attachment(args={})
513
- raise ArgumentError unless args[:file] && args[:name]
514
- return if has_attachment?(args[:name])
515
- self['_attachments'] ||= {}
516
- set_attachment_attr(args)
517
- rescue ArgumentError => e
518
- raise ArgumentError, 'You must specify :file and :name'
519
- end
520
-
521
- # reads the data from an attachment
522
- def read_attachment(attachment_name)
523
- Base64.decode64(database.fetch_attachment(self.id, attachment_name))
524
- end
525
-
526
- # modifies a file attachment on the current doc
527
- def update_attachment(args={})
528
- raise ArgumentError unless args[:file] && args[:name]
529
- return unless has_attachment?(args[:name])
530
- delete_attachment(args[:name])
531
- set_attachment_attr(args)
532
- rescue ArgumentError => e
533
- raise ArgumentError, 'You must specify :file and :name'
534
- end
535
-
536
- # deletes a file attachment from the current doc
537
- def delete_attachment(attachment_name)
538
- return unless self['_attachments']
539
- self['_attachments'].delete attachment_name
540
- end
541
-
542
- # returns true if attachment_name exists
543
- def has_attachment?(attachment_name)
544
- !!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
545
- end
546
-
547
- # returns URL to fetch the attachment from
548
- def attachment_url(attachment_name)
549
- return unless has_attachment?(attachment_name)
550
- "#{database.root}/#{self.id}/#{attachment_name}"
551
- end
552
-
553
- private
554
-
555
- def apply_defaults
556
- return unless new_document?
557
- if self.class.default
558
- self.class.default.each do |k,v|
559
- unless self.key?(k.to_s)
560
- if v.class == Proc
561
- self[k.to_s] = v.call
562
- else
563
- self[k.to_s] = Marshal.load(Marshal.dump(v))
564
- end
565
- end
566
- end
567
- end
568
- end
569
-
570
- def cast_keys
571
- return unless self.class.casts
572
- # TODO move the argument checking to the cast method for early crashes
573
- self.class.casts.each do |k,v|
574
- next unless self[k]
575
- target = v[:as]
576
- v[:send] || 'new'
577
- if target.is_a?(Array)
578
- klass = ::Extlib::Inflection.constantize(target[0])
579
- self[k] = self[k].collect do |value|
580
- (!v[:send] && klass == Time) ? Time.parse(value) : klass.send((v[:send] || 'new'), value)
581
- end
582
- else
583
- self[k] = if (!v[:send] && target == 'Time')
584
- Time.parse(self[k])
585
- else
586
- ::Extlib::Inflection.constantize(target).send((v[:send] || 'new'), self[k])
587
- end
588
- end
589
- end
590
- end
591
-
592
- def encode_attachment(data)
593
- Base64.encode64(data).gsub(/\r|\n/,'')
594
- end
595
-
596
- def get_mime_type(file)
597
- MIME::Types.type_for(file.path).empty? ?
598
- 'text\/plain' : MIME::Types.type_for(file.path).first.content_type.gsub(/\//,'\/')
599
- end
600
-
601
- def set_attachment_attr(args)
602
- content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file])
603
- self['_attachments'][args[:name]] = {
604
- 'content-type' => content_type,
605
- 'data' => encode_attachment(args[:file].read)
606
- }
607
- end
608
-
609
- include ::Extlib::Hook
610
- register_instance_hooks :save, :destroy
611
-
612
- end # class Model
613
- end # module CouchRest