openlogic-couchrest_model 1.0.0

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 (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