openlogic-couchrest_model 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/.gitignore +11 -0
  2. data/.rspec +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +176 -0
  5. data/README.md +137 -0
  6. data/Rakefile +38 -0
  7. data/THANKS.md +21 -0
  8. data/VERSION +1 -0
  9. data/benchmarks/dirty.rb +118 -0
  10. data/couchrest_model.gemspec +36 -0
  11. data/history.md +309 -0
  12. data/init.rb +1 -0
  13. data/lib/couchrest/model.rb +10 -0
  14. data/lib/couchrest/model/associations.rb +231 -0
  15. data/lib/couchrest/model/base.rb +129 -0
  16. data/lib/couchrest/model/callbacks.rb +28 -0
  17. data/lib/couchrest/model/casted_array.rb +83 -0
  18. data/lib/couchrest/model/casted_by.rb +33 -0
  19. data/lib/couchrest/model/casted_hash.rb +84 -0
  20. data/lib/couchrest/model/class_proxy.rb +135 -0
  21. data/lib/couchrest/model/collection.rb +273 -0
  22. data/lib/couchrest/model/configuration.rb +67 -0
  23. data/lib/couchrest/model/connection.rb +70 -0
  24. data/lib/couchrest/model/core_extensions/hash.rb +9 -0
  25. data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
  26. data/lib/couchrest/model/design_doc.rb +128 -0
  27. data/lib/couchrest/model/designs.rb +91 -0
  28. data/lib/couchrest/model/designs/view.rb +513 -0
  29. data/lib/couchrest/model/dirty.rb +39 -0
  30. data/lib/couchrest/model/document_queries.rb +99 -0
  31. data/lib/couchrest/model/embeddable.rb +78 -0
  32. data/lib/couchrest/model/errors.rb +25 -0
  33. data/lib/couchrest/model/extended_attachments.rb +83 -0
  34. data/lib/couchrest/model/persistence.rb +178 -0
  35. data/lib/couchrest/model/properties.rb +228 -0
  36. data/lib/couchrest/model/property.rb +114 -0
  37. data/lib/couchrest/model/property_protection.rb +71 -0
  38. data/lib/couchrest/model/proxyable.rb +183 -0
  39. data/lib/couchrest/model/support/couchrest_database.rb +13 -0
  40. data/lib/couchrest/model/support/couchrest_design.rb +33 -0
  41. data/lib/couchrest/model/typecast.rb +154 -0
  42. data/lib/couchrest/model/validations.rb +80 -0
  43. data/lib/couchrest/model/validations/casted_model.rb +16 -0
  44. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  45. data/lib/couchrest/model/validations/uniqueness.rb +69 -0
  46. data/lib/couchrest/model/views.rb +151 -0
  47. data/lib/couchrest/railtie.rb +24 -0
  48. data/lib/couchrest_model.rb +66 -0
  49. data/lib/rails/generators/couchrest_model.rb +16 -0
  50. data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
  51. data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
  52. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  53. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  54. data/spec/.gitignore +1 -0
  55. data/spec/fixtures/attachments/README +3 -0
  56. data/spec/fixtures/attachments/couchdb.png +0 -0
  57. data/spec/fixtures/attachments/test.html +11 -0
  58. data/spec/fixtures/config/couchdb.yml +10 -0
  59. data/spec/fixtures/models/article.rb +36 -0
  60. data/spec/fixtures/models/base.rb +164 -0
  61. data/spec/fixtures/models/card.rb +19 -0
  62. data/spec/fixtures/models/cat.rb +23 -0
  63. data/spec/fixtures/models/client.rb +6 -0
  64. data/spec/fixtures/models/course.rb +27 -0
  65. data/spec/fixtures/models/event.rb +8 -0
  66. data/spec/fixtures/models/invoice.rb +14 -0
  67. data/spec/fixtures/models/key_chain.rb +5 -0
  68. data/spec/fixtures/models/membership.rb +4 -0
  69. data/spec/fixtures/models/person.rb +11 -0
  70. data/spec/fixtures/models/project.rb +6 -0
  71. data/spec/fixtures/models/question.rb +7 -0
  72. data/spec/fixtures/models/sale_entry.rb +9 -0
  73. data/spec/fixtures/models/sale_invoice.rb +14 -0
  74. data/spec/fixtures/models/service.rb +10 -0
  75. data/spec/fixtures/models/user.rb +22 -0
  76. data/spec/fixtures/views/lib.js +3 -0
  77. data/spec/fixtures/views/test_view/lib.js +3 -0
  78. data/spec/fixtures/views/test_view/only-map.js +4 -0
  79. data/spec/fixtures/views/test_view/test-map.js +3 -0
  80. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  81. data/spec/functional/validations_spec.rb +8 -0
  82. data/spec/spec_helper.rb +60 -0
  83. data/spec/unit/active_model_lint_spec.rb +30 -0
  84. data/spec/unit/assocations_spec.rb +242 -0
  85. data/spec/unit/attachment_spec.rb +176 -0
  86. data/spec/unit/base_spec.rb +537 -0
  87. data/spec/unit/casted_spec.rb +72 -0
  88. data/spec/unit/class_proxy_spec.rb +167 -0
  89. data/spec/unit/collection_spec.rb +86 -0
  90. data/spec/unit/configuration_spec.rb +77 -0
  91. data/spec/unit/connection_spec.rb +148 -0
  92. data/spec/unit/core_extensions/time_parsing.rb +77 -0
  93. data/spec/unit/design_doc_spec.rb +241 -0
  94. data/spec/unit/designs/view_spec.rb +831 -0
  95. data/spec/unit/designs_spec.rb +134 -0
  96. data/spec/unit/dirty_spec.rb +436 -0
  97. data/spec/unit/embeddable_spec.rb +498 -0
  98. data/spec/unit/inherited_spec.rb +33 -0
  99. data/spec/unit/persistence_spec.rb +481 -0
  100. data/spec/unit/property_protection_spec.rb +192 -0
  101. data/spec/unit/property_spec.rb +481 -0
  102. data/spec/unit/proxyable_spec.rb +376 -0
  103. data/spec/unit/subclass_spec.rb +85 -0
  104. data/spec/unit/typecast_spec.rb +521 -0
  105. data/spec/unit/validations_spec.rb +140 -0
  106. data/spec/unit/view_spec.rb +367 -0
  107. metadata +301 -0
@@ -0,0 +1,128 @@
1
+ # encoding: utf-8
2
+ module CouchRest
3
+ module Model
4
+ module DesignDoc
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def design_doc
10
+ @design_doc ||= if auto_update_design_doc
11
+ ::CouchRest::Design.new(default_design_doc)
12
+ else
13
+ stored_design_doc || ::CouchRest::Design.new(default_design_doc)
14
+ end
15
+ end
16
+
17
+ def design_doc_id
18
+ "_design/#{design_doc_slug}"
19
+ end
20
+
21
+ def design_doc_slug
22
+ self.to_s
23
+ end
24
+
25
+ def design_doc_uri(db = database)
26
+ "#{db.root}/#{design_doc_id}"
27
+ end
28
+
29
+ # Retreive the latest version of the design document directly
30
+ # from the database. This is never cached and will return nil if
31
+ # the design is not present.
32
+ #
33
+ # Use this method if you'd like to compare revisions [_rev] which
34
+ # is not stored in the normal design doc.
35
+ def stored_design_doc(db = database)
36
+ db.get(design_doc_id)
37
+ rescue RestClient::ResourceNotFound
38
+ nil
39
+ end
40
+
41
+ # Save the design doc onto a target database in a thread-safe way,
42
+ # not modifying the model's design_doc
43
+ #
44
+ # See also save_design_doc! to always save the design doc even if there
45
+ # are no changes.
46
+ def save_design_doc(db = database, force = false)
47
+ update_design_doc(db, force)
48
+ end
49
+
50
+ # Force the update of the model's design_doc even if it hasn't changed.
51
+ def save_design_doc!(db = database)
52
+ save_design_doc(db, true)
53
+ end
54
+
55
+ private
56
+
57
+ def design_doc_cache
58
+ Thread.current[:couchrest_design_cache] ||= {}
59
+ end
60
+ def design_doc_cache_checksum(db)
61
+ design_doc_cache[design_doc_uri(db)]
62
+ end
63
+ def set_design_doc_cache_checksum(db, checksum)
64
+ design_doc_cache[design_doc_uri(db)] = checksum
65
+ end
66
+
67
+ # Writes out a design_doc to a given database if forced
68
+ # or the stored checksum is not the same as the current
69
+ # generated checksum.
70
+ #
71
+ # Returns the original design_doc provided, but does
72
+ # not update it with the revision.
73
+ def update_design_doc(db, force = false)
74
+ return design_doc unless force || auto_update_design_doc
75
+
76
+ # Grab the design doc's checksum
77
+ checksum = design_doc.checksum!
78
+
79
+ # If auto updates enabled, check checksum cache
80
+ return design_doc if auto_update_design_doc && design_doc_cache_checksum(db) == checksum
81
+
82
+ retries = 1
83
+ begin
84
+ # Load up the stored doc (if present), update, and save
85
+ saved = stored_design_doc(db)
86
+ if saved
87
+ if force || saved['couchrest-hash'] != checksum
88
+ saved.merge!(design_doc)
89
+ db.save_doc(saved)
90
+ @design_doc = saved # update memo to point to the document we actually saved
91
+ end
92
+ else
93
+ design_doc.delete('_rev') # This is a new document and so doesn't have a revision yet
94
+ db.save_doc(design_doc)
95
+ end
96
+ rescue RestClient::Conflict
97
+ # if we get a conflict retry the operation...
98
+ raise if retries < 1
99
+ retries -= 1
100
+ retry
101
+ end
102
+
103
+ # Ensure checksum cached for next attempt if using auto updates
104
+ set_design_doc_cache_checksum(db, checksum) if auto_update_design_doc
105
+ design_doc
106
+ end
107
+
108
+ def default_design_doc
109
+ {
110
+ "_id" => design_doc_id,
111
+ "language" => "javascript",
112
+ "views" => {
113
+ 'all' => {
114
+ 'map' => "function(doc) {
115
+ if (doc['#{self.model_type_key}'] == '#{self.to_s}') {
116
+ emit(doc['_id'],1);
117
+ }
118
+ }"
119
+ }
120
+ }
121
+ }
122
+ end
123
+
124
+ end # module ClassMethods
125
+
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,91 @@
1
+
2
+ #### NOTE Work in progress! Not yet used!
3
+
4
+ module CouchRest
5
+ module Model
6
+
7
+ # A design block in CouchRest Model groups together the functionality of CouchDB's
8
+ # design documents in a simple block definition.
9
+ #
10
+ # class Person < CouchRest::Model::Base
11
+ # property :name
12
+ # timestamps!
13
+ #
14
+ # design do
15
+ # view :by_name
16
+ # end
17
+ # end
18
+ #
19
+ module Designs
20
+ extend ActiveSupport::Concern
21
+
22
+ module ClassMethods
23
+
24
+ # Add views and other design document features
25
+ # to the current model.
26
+ def design(*args, &block)
27
+ mapper = DesignMapper.new(self)
28
+ mapper.create_view_method(:all)
29
+
30
+ mapper.instance_eval(&block) if block_given?
31
+ end
32
+
33
+ # Override the default page pagination value:
34
+ #
35
+ # class Person < CouchRest::Model::Base
36
+ # paginates_per 10
37
+ # end
38
+ #
39
+ def paginates_per(val)
40
+ @_default_per_page = val
41
+ end
42
+
43
+ # The models number of documents to return
44
+ # by default when performing pagination.
45
+ # Returns 25 unless explicitly overridden via <tt>paginates_per</tt>
46
+ def default_per_page
47
+ @_default_per_page || 25
48
+ end
49
+
50
+ end
51
+
52
+ #
53
+ class DesignMapper
54
+
55
+ attr_accessor :model
56
+
57
+ def initialize(model)
58
+ self.model = model
59
+ end
60
+
61
+ # Generate a method that will provide a new View instance when
62
+ # requested. This will also define the view in CouchDB unless
63
+ # auto_update_design_doc is disabled.
64
+ def view(name, opts = {})
65
+ View.create(model, name, opts) if model.auto_update_design_doc
66
+ create_view_method(name)
67
+ end
68
+
69
+ # Really simple design function that allows a filter
70
+ # to be added. Filters are simple functions used when listening
71
+ # to the _changes feed.
72
+ #
73
+ # No methods are created here, the design is simply updated.
74
+ # See the CouchDB API for more information on how to use this.
75
+ def filter(name, function)
76
+ filters = (self.model.design_doc['filters'] ||= {})
77
+ filters[name.to_s] = function
78
+ end
79
+
80
+ def create_view_method(name)
81
+ model.class_eval <<-EOS, __FILE__, __LINE__ + 1
82
+ def self.#{name}(opts = {})
83
+ CouchRest::Model::Designs::View.new(self, opts, '#{name}')
84
+ end
85
+ EOS
86
+ end
87
+
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,513 @@
1
+ module CouchRest
2
+ module Model
3
+ module Designs
4
+
5
+ #
6
+ # A proxy class that allows view queries to be created using
7
+ # chained method calls. After each call a new instance of the method
8
+ # is created based on the original in a similar fashion to ruby's Sequel
9
+ # library, or Rails 3's Arel.
10
+ #
11
+ # CouchDB views have inherent limitations, so joins and filters as used in
12
+ # a normal relational database are not possible.
13
+ #
14
+ class View
15
+ include Enumerable
16
+
17
+ attr_accessor :owner, :model, :name, :query, :result
18
+
19
+ # Initialize a new View object. This method should not be called from
20
+ # outside CouchRest Model.
21
+ def initialize(parent, new_query = {}, name = nil)
22
+ if parent.is_a?(Class) && parent < CouchRest::Model::Base
23
+ raise "Name must be provided for view to be initialized" if name.nil?
24
+ self.model = parent
25
+ self.owner = parent
26
+ self.name = name.to_s
27
+ # Default options:
28
+ self.query = { }
29
+ elsif parent.is_a?(self.class)
30
+ self.model = (new_query.delete(:proxy) || parent.model)
31
+ self.owner = parent.owner
32
+ self.name = parent.name
33
+ self.query = parent.query.dup
34
+ else
35
+ raise "View cannot be initialized without a parent Model or View"
36
+ end
37
+ query.update(new_query)
38
+ super()
39
+ end
40
+
41
+
42
+ # == View Execution Methods
43
+ #
44
+ # Request to the CouchDB database using the current query values.
45
+
46
+ # Return each row wrapped in a ViewRow object. Unlike the raw
47
+ # CouchDB request, this will provide an empty array if there
48
+ # are no results.
49
+ def rows
50
+ return @rows if @rows
51
+ if execute && result['rows']
52
+ @rows ||= result['rows'].map{|v| ViewRow.new(v, model)}
53
+ else
54
+ [ ]
55
+ end
56
+ end
57
+
58
+ # Fetch all the documents the view can access. If the view has
59
+ # not already been prepared for including documents in the query,
60
+ # it will be added automatically and reset any previously cached
61
+ # results.
62
+ def all
63
+ include_docs!
64
+ docs
65
+ end
66
+
67
+ # Provide all the documents from the view. If the view has not been
68
+ # prepared with the +include_docs+ option, each document will be
69
+ # loaded individually.
70
+ def docs
71
+ @docs ||= rows.map{|r| r.doc}
72
+ end
73
+
74
+ # If another request has been made on the view, this will return
75
+ # the first document in the set. If not, a new query object will be
76
+ # generated with a limit of 1 so that only the first document is
77
+ # loaded.
78
+ def first
79
+ result ? all.first : limit(1).all.first
80
+ end
81
+
82
+ # Same as first but will order the view in descending order. This
83
+ # does not however reverse the search keys or the offset, so if you
84
+ # are using a +startkey+ and +endkey+ you might end up with
85
+ # unexpected results.
86
+ #
87
+ # If in doubt, don't use this method!
88
+ #
89
+ def last
90
+ result ? all.last : limit(1).descending.all.last
91
+ end
92
+
93
+ # Return the number of documents in the currently defined result set.
94
+ # Use <tt>#count</tt> for the total number of documents regardless
95
+ # of the current limit defined.
96
+ def length
97
+ docs.length
98
+ end
99
+
100
+ # Perform a count operation based on the current view. If the view
101
+ # can be reduced, the reduce will be performed and return the first
102
+ # value. This is okay for most simple queries, but may provide
103
+ # unexpected results if your reduce method does not calculate
104
+ # the total number of documents in a result set.
105
+ #
106
+ # Trying to use this method with the group option will raise an error.
107
+ #
108
+ # If no reduce function is defined, a query will be performed
109
+ # to return the total number of rows, this is the equivalant of:
110
+ #
111
+ # view.limit(0).total_rows
112
+ #
113
+ def count
114
+ raise "View#count cannot be used with group options" if query[:group]
115
+ if can_reduce?
116
+ row = reduce.skip(0).limit(1).rows.first
117
+ row.nil? ? 0 : row.value
118
+ else
119
+ limit(0).total_rows
120
+ end
121
+ end
122
+
123
+ # Check to see if the array of documents is empty. This *will*
124
+ # perform the query and return all documents ready to use, if you don't
125
+ # want to load anything, use +#total_rows+ or +#count+ instead.
126
+ def empty?
127
+ all.empty?
128
+ end
129
+
130
+ # Run through each document provided by the +#all+ method.
131
+ # This is also used by the Enumerator mixin to provide all the standard
132
+ # ruby collection directly on the view.
133
+ def each(&block)
134
+ all.each(&block)
135
+ end
136
+
137
+ # Wrapper for the results offset. As per the CouchDB API,
138
+ # this may be nil if groups are used.
139
+ def offset
140
+ execute['offset']
141
+ end
142
+
143
+ # Wrapper for the total_rows value provided by the query. As per the
144
+ # CouchDB API, this may be nil if groups are used.
145
+ def total_rows
146
+ execute['total_rows']
147
+ end
148
+
149
+ # Convenience wrapper to provide all the values from the route
150
+ # set without having to go through +rows+.
151
+ def values
152
+ rows.map{|r| r.value}
153
+ end
154
+
155
+ # Accept requests as if the view was an array. Used for backwards compatibity
156
+ # with older queries:
157
+ #
158
+ # Model.all(:raw => true, :limit => 0)['total_rows']
159
+ #
160
+ # In this example, the raw option will be ignored, and the total rows
161
+ # will still be accessible.
162
+ #
163
+ def [](value)
164
+ execute[value]
165
+ end
166
+
167
+ # No yet implemented. Eventually this will provide a raw hash
168
+ # of the information CouchDB holds about the view.
169
+ def info
170
+ raise "Not yet implemented"
171
+ end
172
+
173
+
174
+ # == View Filter Methods
175
+ #
176
+ # View filters return a copy of the view instance with the query
177
+ # modified appropriatly. Errors will be raised if the methods
178
+ # are combined in an incorrect fashion.
179
+ #
180
+
181
+ # Find all entries in the index whose key matches the value provided.
182
+ #
183
+ # Cannot be used when the +#startkey+ or +#endkey+ have been set.
184
+ def key(value)
185
+ raise "View#key cannot be used when startkey or endkey have been set" unless query[:keys].nil? && query[:startkey].nil? && query[:endkey].nil?
186
+ update_query(:key => value)
187
+ end
188
+
189
+ # Find all index keys that start with the value provided. May or may
190
+ # not be used in conjunction with the +endkey+ option.
191
+ #
192
+ # When the +#descending+ option is used (not the default), the start
193
+ # and end keys should be reversed, as per the CouchDB API.
194
+ #
195
+ # Cannot be used if the key has been set.
196
+ def startkey(value)
197
+ raise "View#startkey cannot be used when key has been set" unless query[:key].nil? && query[:keys].nil?
198
+ update_query(:startkey => value)
199
+ end
200
+
201
+ # The result set should start from the position of the provided document.
202
+ # The value may be provided as an object that responds to the +#id+ call
203
+ # or a string.
204
+ def startkey_doc(value)
205
+ update_query(:startkey_docid => value.is_a?(String) ? value : value.id)
206
+ end
207
+
208
+ # The opposite of +#startkey+, finds all index entries whose key is before
209
+ # the value specified.
210
+ #
211
+ # See the +#startkey+ method for more details and the +#inclusive_end+
212
+ # option.
213
+ def endkey(value)
214
+ raise "View#endkey cannot be used when key has been set" unless query[:key].nil? && query[:keys].nil?
215
+ update_query(:endkey => value)
216
+ end
217
+
218
+ # The result set should end at the position of the provided document.
219
+ # The value may be provided as an object that responds to the +#id+
220
+ # call or a string.
221
+ def endkey_doc(value)
222
+ update_query(:endkey_docid => value.is_a?(String) ? value : value.id)
223
+ end
224
+
225
+ # Keys is a special CouchDB option that will cause the view request to be POSTed
226
+ # including an array of keys. Only documents with the matching keys will be
227
+ # returned. This is much faster than sending multiple requests for a set
228
+ # non-consecutive documents.
229
+ #
230
+ # If no values are provided, this method will act as a wrapper around
231
+ # the rows result set, providing an array of keys.
232
+ def keys(*keys)
233
+ if keys.empty?
234
+ rows.map{|r| r.key}
235
+ else
236
+ raise "View#keys cannot by used when key or startkey/endkey have been set" unless query[:key].nil? && query[:startkey].nil? && query[:endkey].nil?
237
+ update_query(:keys => keys.first)
238
+ end
239
+ end
240
+
241
+
242
+ # The results should be provided in descending order. If the startkey or
243
+ # endkey query options have already been seen set, calling this method
244
+ # will automatically swap the options around. If you don't want this,
245
+ # simply set descending before any other option.
246
+ #
247
+ # Descending is false by default, and this method cannot
248
+ # be undone once used, it has no inverse option.
249
+ def descending
250
+ if query[:startkey] || query[:endkey]
251
+ query[:startkey], query[:endkey] = query[:endkey], query[:startkey]
252
+ elsif query[:startkey_docid] || query[:endkey_docid]
253
+ query[:startkey_docid], query[:endkey_docid] = query[:endkey_docid], query[:startkey_docid]
254
+ end
255
+ update_query(:descending => true)
256
+ end
257
+
258
+ # Limit the result set to the value supplied.
259
+ def limit(value)
260
+ update_query(:limit => value)
261
+ end
262
+
263
+ # Skip the number of entries in the index specified by value. This would be
264
+ # the equivilent of an offset in SQL.
265
+ #
266
+ # The CouchDB documentation states that the skip option should not be used
267
+ # with large data sets as it is inefficient. Use the +startkey_doc+ method
268
+ # instead to skip ranges efficiently.
269
+ def skip(value = 0)
270
+ update_query(:skip => value)
271
+ end
272
+
273
+ # Use the reduce function on the view. If none is available this method
274
+ # will fail.
275
+ def reduce
276
+ raise "Cannot reduce a view without a reduce method" unless can_reduce?
277
+ update_query(:reduce => true, :include_docs => nil)
278
+ end
279
+
280
+ # Control whether the reduce function reduces to a set of distinct keys
281
+ # or to a single result row.
282
+ #
283
+ # By default the value is false, and can only be set when the view's
284
+ # +#reduce+ option has been set.
285
+ def group
286
+ raise "View#reduce must have been set before grouping is permitted" unless query[:reduce]
287
+ update_query(:group => true)
288
+ end
289
+
290
+ # Will set the level the grouping should be performed to. As per the
291
+ # CouchDB API, it only makes sense when the index key is an array.
292
+ #
293
+ # This will automatically set the group option.
294
+ def group_level(value)
295
+ group.update_query(:group_level => value.to_i)
296
+ end
297
+
298
+ def include_docs
299
+ update_query.include_docs!
300
+ end
301
+
302
+ ### Special View Filter Methods
303
+
304
+ # Specify the database the view should use. If not defined,
305
+ # an attempt will be made to load its value from the model.
306
+ def database(value)
307
+ update_query(:database => value)
308
+ end
309
+
310
+ # Set the view's proxy that will be used instead of the model
311
+ # for any future searches. As soon as this enters the
312
+ # new object's initializer it will be removed and replace
313
+ # the model object.
314
+ #
315
+ # See the Proxyable mixin for more details.
316
+ #
317
+ def proxy(value)
318
+ update_query(:proxy => value)
319
+ end
320
+
321
+ # Return any cached values to their nil state so that any queries
322
+ # requested later will have a fresh set of data.
323
+ def reset!
324
+ self.result = nil
325
+ @rows = nil
326
+ @docs = nil
327
+ end
328
+
329
+ # == Kaminari compatible pagination support
330
+ #
331
+ # Based on the really simple support for scoped pagination in the
332
+ # the Kaminari gem, we provide compatible methods here to perform
333
+ # the same actions you'd expect.
334
+ #
335
+
336
+ def page(page)
337
+ limit(owner.default_per_page).skip(owner.default_per_page * ([page.to_i, 1].max - 1))
338
+ end
339
+
340
+ def per(num)
341
+ raise "View#page must be called before #per!" if limit_value.nil? || offset_value.nil?
342
+ if (n = num.to_i) <= 0
343
+ self
344
+ else
345
+ limit(num).skip(offset_value / limit_value * n)
346
+ end
347
+ end
348
+
349
+ def total_count
350
+ @total_count ||= limit(nil).skip(nil).count
351
+ end
352
+
353
+ def offset_value
354
+ query[:skip]
355
+ end
356
+
357
+ def limit_value
358
+ query[:limit]
359
+ end
360
+
361
+ def num_pages
362
+ (total_count.to_f / limit_value).ceil
363
+ end
364
+
365
+ def current_page
366
+ (offset_value / limit_value) + 1
367
+ end
368
+
369
+ protected
370
+
371
+ def include_docs!
372
+ raise "Cannot include documents in view that has been reduced!" if query[:reduce]
373
+ reset! if result && !include_docs?
374
+ query[:include_docs] = true
375
+ self
376
+ end
377
+
378
+ def include_docs?
379
+ !!query[:include_docs]
380
+ end
381
+
382
+ def update_query(new_query = {})
383
+ self.class.new(self, new_query)
384
+ end
385
+
386
+ def design_doc
387
+ model.design_doc
388
+ end
389
+
390
+ def can_reduce?
391
+ !design_doc['views'][name]['reduce'].blank?
392
+ end
393
+
394
+ def use_database
395
+ query[:database] || model.database
396
+ end
397
+
398
+ def execute
399
+ return self.result if result
400
+ raise "Database must be defined in model or view!" if use_database.nil?
401
+
402
+ # Remove the reduce value if its not needed to prevent CouchDB errors
403
+ query.delete(:reduce) unless can_reduce?
404
+
405
+ model.save_design_doc(use_database)
406
+
407
+ self.result = model.design_doc.view_on(use_database, name, query.reject{|k,v| v.nil?})
408
+ end
409
+
410
+ # Class Methods
411
+ class << self
412
+ # Simplified view creation. A new view will be added to the
413
+ # provided model's design document using the name and options.
414
+ #
415
+ # If the view name starts with "by_" and +:by+ is not provided in
416
+ # the options, the new view's map method will be interpreted and
417
+ # generated automatically. For example:
418
+ #
419
+ # View.create(Meeting, "by_date_and_name")
420
+ #
421
+ # Will create a view that searches by the date and name properties.
422
+ # Explicity setting the attributes to use is possible using the
423
+ # +:by+ option. For example:
424
+ #
425
+ # View.create(Meeting, "by_date_and_name", :by => [:date, :firstname, :lastname])
426
+ #
427
+ # The view name is the same, but three keys would be used in the
428
+ # subsecuent index.
429
+ #
430
+ # By default, a check is made on each of the view's keys to ensure they
431
+ # do not contain a nil value ('null' in javascript). This is probably what
432
+ # you want in most cases but sometimes in can be useful to create an
433
+ # index where nil is permited. Set the <tt>:allow_nil</tt> option to true to
434
+ # remove this check.
435
+ #
436
+ # Conversely, keys are not checked to see if they are empty or blank. If you'd
437
+ # like to enable this, set the <tt>:allow_blank</tt> option to false. The default
438
+ # is true, empty strings are permited in the indexes.
439
+ #
440
+ def create(model, name, opts = {})
441
+
442
+ unless opts[:map]
443
+ if opts[:by].nil? && name.to_s =~ /^by_(.+)/
444
+ opts[:by] = $1.split(/_and_/)
445
+ end
446
+
447
+ raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
448
+
449
+ opts[:allow_blank] = opts[:allow_blank].nil? ? true : opts[:allow_blank]
450
+ opts[:guards] ||= []
451
+ opts[:guards].push "(doc['#{model.model_type_key}'] == '#{model.to_s}')"
452
+
453
+ keys = opts[:by].map{|o| "doc['#{o}']"}
454
+ emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]"
455
+ opts[:guards] += keys.map{|k| "(#{k} != null)"} unless opts[:allow_nil]
456
+ opts[:guards] += keys.map{|k| "(#{k} != '')"} unless opts[:allow_blank]
457
+ opts[:map] = <<-EOF
458
+ function(doc) {
459
+ if (#{opts[:guards].join(' && ')}) {
460
+ emit(#{emit}, 1);
461
+ }
462
+ }
463
+ EOF
464
+ opts[:reduce] = <<-EOF
465
+ function(key, values, rereduce) {
466
+ return sum(values);
467
+ }
468
+ EOF
469
+ end
470
+
471
+ model.design_doc['views'] ||= {}
472
+ view = model.design_doc['views'][name.to_s] = { }
473
+ view['map'] = opts[:map]
474
+ view['reduce'] = opts[:reduce] if opts[:reduce]
475
+ view
476
+ end
477
+
478
+ end
479
+
480
+ end
481
+
482
+ # A special wrapper class that provides easy access to the key
483
+ # fields in a result row.
484
+ class ViewRow < Hash
485
+ attr_reader :model
486
+ def initialize(hash, model)
487
+ @model = model
488
+ replace(hash)
489
+ end
490
+ def id
491
+ self["id"]
492
+ end
493
+ def key
494
+ self["key"]
495
+ end
496
+ def value
497
+ self['value']
498
+ end
499
+ def raw_doc
500
+ self['doc']
501
+ end
502
+ # Send a request for the linked document either using the "id" field's
503
+ # value, or the ["value"]["_id"] used for linked documents.
504
+ def doc
505
+ return model.build_from_database(self['doc']) if self['doc']
506
+ doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id
507
+ doc_id ? model.get(doc_id) : nil
508
+ end
509
+ end
510
+
511
+ end
512
+ end
513
+ end