couch_potato 0.2.32 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES.md CHANGED
@@ -1,5 +1,8 @@
1
1
  ## Changes
2
2
 
3
+ ### 0.3.0
4
+ * support for lists (langalex)
5
+
3
6
  ### 0.2.32
4
7
  * added persisted? and to_key for proper ActiveModel compliance (thilo)
5
8
  * id setter (jhohertz-work)
data/README.md CHANGED
@@ -175,9 +175,9 @@ Couch Potato uses the validatable library for validation (http://validatable.rub
175
175
  user.valid? # => false
176
176
  user.errors.on(:name) # => [:name, 'can't be blank']
177
177
 
178
- #### Finding stuff
178
+ #### Finding stuff / views / lists
179
179
 
180
- In order to find data in your CouchDB you have to create a view first. Couch Potato offers you to create and manage those views for you. All you have to do is declare them in your classes:
180
+ In order to find data in your CouchDB you have to create a [view](http://books.couchdb.org/relax/design-documents/views) first. Couch Potato offers you to create and manage those views for you. All you have to do is declare them in your classes:
181
181
 
182
182
  class User
183
183
  include CouchPotato::Persistence
@@ -250,6 +250,39 @@ In this case querying the view would only return the emitted value for each row.
250
250
 
251
251
  You can pass in your own view specifications by passing in :type => MyViewSpecClass. Take a look at the CouchPotato::View::*ViewSpec classes to get an idea of how this works.
252
252
 
253
+ ##### Lists
254
+
255
+ CouchPotato also supports [CouchDB lists](http://books.couchdb.org/relax/design-documents/lists). With lists you can process the result of a view query with another JavaScript function. This can be useful for example if you want to filter your results, or add some data to each document.
256
+
257
+ Defining a list works similarly to views:
258
+
259
+ class User
260
+ include CouchPotato::Persistence
261
+
262
+ property :first_name
263
+ view :with_full_name, key: first_namne, list: :add_last_name
264
+ view :all, key: :first_name
265
+
266
+ list :add_last_name, <<-JS
267
+ function(head, req) {
268
+ var row;
269
+ send('{"rows": [');
270
+ while(row = getRow()) {
271
+ row.doc.name = row.doc.first_name + ' doe';
272
+ send(JSON.stringify(row));
273
+ };
274
+ send(']}');
275
+ }
276
+ JS
277
+ end
278
+
279
+ CouchPotato.database.save User.new(first_name: 'joe')
280
+ CouchPotato.database.view(User.with_full_name).first.name # => 'joe doe'
281
+
282
+ You can also pass in the list at query time:
283
+
284
+ CouchPotato.database.view(User.all(list: :add_last_name))
285
+
253
286
  #### Associations
254
287
 
255
288
  Not supported. Not sure if they ever will be. You can implement those yourself using views and custom methods on your models.
data/VERSION.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
2
  :major: 0
3
- :minor: 2
4
- :patch: 32
3
+ :minor: 3
4
+ :patch: 0
5
5
  :build:
@@ -44,9 +44,15 @@ module CouchPotato
44
44
  #
45
45
  # db.view(User.all(keys: [1, 2, 3]))
46
46
  def view(spec)
47
- results = CouchPotato::View::ViewQuery.new(database,
48
- spec.design_document, spec.view_name, spec.map_function,
49
- spec.reduce_function).query_view!(spec.view_parameters)
47
+ results = CouchPotato::View::ViewQuery.new(
48
+ database,
49
+ spec.design_document,
50
+ {spec.view_name => {
51
+ :map => spec.map_function,
52
+ :reduce => spec.reduce_function}
53
+ },
54
+ ({spec.list_name => spec.list_function} unless spec.list_name.nil?)
55
+ ).query_view!(spec.view_parameters)
50
56
  processed_results = spec.process_results results
51
57
  processed_results.instance_eval "def total_rows; #{results['total_rows']}; end" if results['total_rows']
52
58
  processed_results.each do |document|
@@ -27,12 +27,12 @@ module CouchPotato
27
27
  end.flatten.compact.uniq
28
28
 
29
29
  callbacks.each do |callback|
30
- if callback.is_a?(Symbol)
30
+ if [Symbol, String].include?(callback.class)
31
31
  send callback
32
32
  elsif callback.is_a?(Proc)
33
33
  callback.call self
34
34
  else
35
- raise "Don't know how to handle callback of type #{name.class.name}"
35
+ raise "Don't know how to handle callback of type #{callback.class.name}"
36
36
  end
37
37
  end
38
38
  end
@@ -10,6 +10,7 @@ require File.dirname(__FILE__) + '/persistence/ghost_attributes'
10
10
  require File.dirname(__FILE__) + '/persistence/attachments'
11
11
  require File.dirname(__FILE__) + '/persistence/type_caster'
12
12
  require File.dirname(__FILE__) + '/view/custom_views'
13
+ require File.dirname(__FILE__) + '/view/lists'
13
14
  require File.dirname(__FILE__) + '/view/view_query'
14
15
 
15
16
 
@@ -17,7 +18,7 @@ module CouchPotato
17
18
  module Persistence
18
19
 
19
20
  def self.included(base) #:nodoc:
20
- base.send :include, Properties, Callbacks, Validation, Json, CouchPotato::View::CustomViews
21
+ base.send :include, Properties, Callbacks, Validation, Json, CouchPotato::View::CustomViews, CouchPotato::View::Lists
21
22
  base.send :include, DirtyAttributes, GhostAttributes, Attachments
22
23
  base.send :include, MagicTimestamps, ActiveModelCompliance
23
24
  base.class_eval do
@@ -0,0 +1,482 @@
1
+ /*
2
+ http://www.JSON.org/json2.js
3
+ 2010-03-20
4
+
5
+ Public Domain.
6
+
7
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8
+
9
+ See http://www.JSON.org/js.html
10
+
11
+
12
+ This code should be minified before deployment.
13
+ See http://javascript.crockford.com/jsmin.html
14
+
15
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16
+ NOT CONTROL.
17
+
18
+
19
+ This file creates a global JSON object containing two methods: stringify
20
+ and parse.
21
+
22
+ JSON.stringify(value, replacer, space)
23
+ value any JavaScript value, usually an object or array.
24
+
25
+ replacer an optional parameter that determines how object
26
+ values are stringified for objects. It can be a
27
+ function or an array of strings.
28
+
29
+ space an optional parameter that specifies the indentation
30
+ of nested structures. If it is omitted, the text will
31
+ be packed without extra whitespace. If it is a number,
32
+ it will specify the number of spaces to indent at each
33
+ level. If it is a string (such as '\t' or '&nbsp;'),
34
+ it contains the characters used to indent at each level.
35
+
36
+ This method produces a JSON text from a JavaScript value.
37
+
38
+ When an object value is found, if the object contains a toJSON
39
+ method, its toJSON method will be called and the result will be
40
+ stringified. A toJSON method does not serialize: it returns the
41
+ value represented by the name/value pair that should be serialized,
42
+ or undefined if nothing should be serialized. The toJSON method
43
+ will be passed the key associated with the value, and this will be
44
+ bound to the value
45
+
46
+ For example, this would serialize Dates as ISO strings.
47
+
48
+ Date.prototype.toJSON = function (key) {
49
+ function f(n) {
50
+ // Format integers to have at least two digits.
51
+ return n < 10 ? '0' + n : n;
52
+ }
53
+
54
+ return this.getUTCFullYear() + '-' +
55
+ f(this.getUTCMonth() + 1) + '-' +
56
+ f(this.getUTCDate()) + 'T' +
57
+ f(this.getUTCHours()) + ':' +
58
+ f(this.getUTCMinutes()) + ':' +
59
+ f(this.getUTCSeconds()) + 'Z';
60
+ };
61
+
62
+ You can provide an optional replacer method. It will be passed the
63
+ key and value of each member, with this bound to the containing
64
+ object. The value that is returned from your method will be
65
+ serialized. If your method returns undefined, then the member will
66
+ be excluded from the serialization.
67
+
68
+ If the replacer parameter is an array of strings, then it will be
69
+ used to select the members to be serialized. It filters the results
70
+ such that only members with keys listed in the replacer array are
71
+ stringified.
72
+
73
+ Values that do not have JSON representations, such as undefined or
74
+ functions, will not be serialized. Such values in objects will be
75
+ dropped; in arrays they will be replaced with null. You can use
76
+ a replacer function to replace those with JSON values.
77
+ JSON.stringify(undefined) returns undefined.
78
+
79
+ The optional space parameter produces a stringification of the
80
+ value that is filled with line breaks and indentation to make it
81
+ easier to read.
82
+
83
+ If the space parameter is a non-empty string, then that string will
84
+ be used for indentation. If the space parameter is a number, then
85
+ the indentation will be that many spaces.
86
+
87
+ Example:
88
+
89
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
90
+ // text is '["e",{"pluribus":"unum"}]'
91
+
92
+
93
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95
+
96
+ text = JSON.stringify([new Date()], function (key, value) {
97
+ return this[key] instanceof Date ?
98
+ 'Date(' + this[key] + ')' : value;
99
+ });
100
+ // text is '["Date(---current time---)"]'
101
+
102
+
103
+ JSON.parse(text, reviver)
104
+ This method parses a JSON text to produce an object or array.
105
+ It can throw a SyntaxError exception.
106
+
107
+ The optional reviver parameter is a function that can filter and
108
+ transform the results. It receives each of the keys and values,
109
+ and its return value is used instead of the original value.
110
+ If it returns what it received, then the structure is not modified.
111
+ If it returns undefined then the member is deleted.
112
+
113
+ Example:
114
+
115
+ // Parse the text. Values that look like ISO date strings will
116
+ // be converted to Date objects.
117
+
118
+ myData = JSON.parse(text, function (key, value) {
119
+ var a;
120
+ if (typeof value === 'string') {
121
+ a =
122
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123
+ if (a) {
124
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125
+ +a[5], +a[6]));
126
+ }
127
+ }
128
+ return value;
129
+ });
130
+
131
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132
+ var d;
133
+ if (typeof value === 'string' &&
134
+ value.slice(0, 5) === 'Date(' &&
135
+ value.slice(-1) === ')') {
136
+ d = new Date(value.slice(5, -1));
137
+ if (d) {
138
+ return d;
139
+ }
140
+ }
141
+ return value;
142
+ });
143
+
144
+
145
+ This is a reference implementation. You are free to copy, modify, or
146
+ redistribute.
147
+ */
148
+
149
+ /*jslint evil: true, strict: false */
150
+
151
+ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152
+ call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154
+ lastIndex, length, parse, prototype, push, replace, slice, stringify,
155
+ test, toJSON, toString, valueOf
156
+ */
157
+
158
+
159
+ // Create a JSON object only if one does not already exist. We create the
160
+ // methods in a closure to avoid creating global variables.
161
+
162
+ if (!this.JSON) {
163
+ this.JSON = {};
164
+ }
165
+
166
+ (function () {
167
+
168
+ function f(n) {
169
+ // Format integers to have at least two digits.
170
+ return n < 10 ? '0' + n : n;
171
+ }
172
+
173
+ if (typeof Date.prototype.toJSON !== 'function') {
174
+
175
+ Date.prototype.toJSON = function (key) {
176
+
177
+ return isFinite(this.valueOf()) ?
178
+ this.getUTCFullYear() + '-' +
179
+ f(this.getUTCMonth() + 1) + '-' +
180
+ f(this.getUTCDate()) + 'T' +
181
+ f(this.getUTCHours()) + ':' +
182
+ f(this.getUTCMinutes()) + ':' +
183
+ f(this.getUTCSeconds()) + 'Z' : null;
184
+ };
185
+
186
+ String.prototype.toJSON =
187
+ Number.prototype.toJSON =
188
+ Boolean.prototype.toJSON = function (key) {
189
+ return this.valueOf();
190
+ };
191
+ }
192
+
193
+ var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
194
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
195
+ gap,
196
+ indent,
197
+ meta = { // table of character substitutions
198
+ '\b': '\\b',
199
+ '\t': '\\t',
200
+ '\n': '\\n',
201
+ '\f': '\\f',
202
+ '\r': '\\r',
203
+ '"' : '\\"',
204
+ '\\': '\\\\'
205
+ },
206
+ rep;
207
+
208
+
209
+ function quote(string) {
210
+
211
+ // If the string contains no control characters, no quote characters, and no
212
+ // backslash characters, then we can safely slap some quotes around it.
213
+ // Otherwise we must also replace the offending characters with safe escape
214
+ // sequences.
215
+
216
+ escapable.lastIndex = 0;
217
+ return escapable.test(string) ?
218
+ '"' + string.replace(escapable, function (a) {
219
+ var c = meta[a];
220
+ return typeof c === 'string' ? c :
221
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
222
+ }) + '"' :
223
+ '"' + string + '"';
224
+ }
225
+
226
+
227
+ function str(key, holder) {
228
+
229
+ // Produce a string from holder[key].
230
+
231
+ var i, // The loop counter.
232
+ k, // The member key.
233
+ v, // The member value.
234
+ length,
235
+ mind = gap,
236
+ partial,
237
+ value = holder[key];
238
+
239
+ // If the value has a toJSON method, call it to obtain a replacement value.
240
+
241
+ if (value && typeof value === 'object' &&
242
+ typeof value.toJSON === 'function') {
243
+ value = value.toJSON(key);
244
+ }
245
+
246
+ // If we were called with a replacer function, then call the replacer to
247
+ // obtain a replacement value.
248
+
249
+ if (typeof rep === 'function') {
250
+ value = rep.call(holder, key, value);
251
+ }
252
+
253
+ // What happens next depends on the value's type.
254
+
255
+ switch (typeof value) {
256
+ case 'string':
257
+ return quote(value);
258
+
259
+ case 'number':
260
+
261
+ // JSON numbers must be finite. Encode non-finite numbers as null.
262
+
263
+ return isFinite(value) ? String(value) : 'null';
264
+
265
+ case 'boolean':
266
+ case 'null':
267
+
268
+ // If the value is a boolean or null, convert it to a string. Note:
269
+ // typeof null does not produce 'null'. The case is included here in
270
+ // the remote chance that this gets fixed someday.
271
+
272
+ return String(value);
273
+
274
+ // If the type is 'object', we might be dealing with an object or an array or
275
+ // null.
276
+
277
+ case 'object':
278
+
279
+ // Due to a specification blunder in ECMAScript, typeof null is 'object',
280
+ // so watch out for that case.
281
+
282
+ if (!value) {
283
+ return 'null';
284
+ }
285
+
286
+ // Make an array to hold the partial results of stringifying this object value.
287
+
288
+ gap += indent;
289
+ partial = [];
290
+
291
+ // Is the value an array?
292
+
293
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
294
+
295
+ // The value is an array. Stringify every element. Use null as a placeholder
296
+ // for non-JSON values.
297
+
298
+ length = value.length;
299
+ for (i = 0; i < length; i += 1) {
300
+ partial[i] = str(i, value) || 'null';
301
+ }
302
+
303
+ // Join all of the elements together, separated with commas, and wrap them in
304
+ // brackets.
305
+
306
+ v = partial.length === 0 ? '[]' :
307
+ gap ? '[\n' + gap +
308
+ partial.join(',\n' + gap) + '\n' +
309
+ mind + ']' :
310
+ '[' + partial.join(',') + ']';
311
+ gap = mind;
312
+ return v;
313
+ }
314
+
315
+ // If the replacer is an array, use it to select the members to be stringified.
316
+
317
+ if (rep && typeof rep === 'object') {
318
+ length = rep.length;
319
+ for (i = 0; i < length; i += 1) {
320
+ k = rep[i];
321
+ if (typeof k === 'string') {
322
+ v = str(k, value);
323
+ if (v) {
324
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
325
+ }
326
+ }
327
+ }
328
+ } else {
329
+
330
+ // Otherwise, iterate through all of the keys in the object.
331
+
332
+ for (k in value) {
333
+ if (Object.hasOwnProperty.call(value, k)) {
334
+ v = str(k, value);
335
+ if (v) {
336
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ // Join all of the member texts together, separated with commas,
343
+ // and wrap them in braces.
344
+
345
+ v = partial.length === 0 ? '{}' :
346
+ gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
347
+ mind + '}' : '{' + partial.join(',') + '}';
348
+ gap = mind;
349
+ return v;
350
+ }
351
+ }
352
+
353
+ // If the JSON object does not yet have a stringify method, give it one.
354
+
355
+ if (typeof JSON.stringify !== 'function') {
356
+ JSON.stringify = function (value, replacer, space) {
357
+
358
+ // The stringify method takes a value and an optional replacer, and an optional
359
+ // space parameter, and returns a JSON text. The replacer can be a function
360
+ // that can replace values, or an array of strings that will select the keys.
361
+ // A default replacer method can be provided. Use of the space parameter can
362
+ // produce text that is more easily readable.
363
+
364
+ var i;
365
+ gap = '';
366
+ indent = '';
367
+
368
+ // If the space parameter is a number, make an indent string containing that
369
+ // many spaces.
370
+
371
+ if (typeof space === 'number') {
372
+ for (i = 0; i < space; i += 1) {
373
+ indent += ' ';
374
+ }
375
+
376
+ // If the space parameter is a string, it will be used as the indent string.
377
+
378
+ } else if (typeof space === 'string') {
379
+ indent = space;
380
+ }
381
+
382
+ // If there is a replacer, it must be a function or an array.
383
+ // Otherwise, throw an error.
384
+
385
+ rep = replacer;
386
+ if (replacer && typeof replacer !== 'function' &&
387
+ (typeof replacer !== 'object' ||
388
+ typeof replacer.length !== 'number')) {
389
+ throw new Error('JSON.stringify');
390
+ }
391
+
392
+ // Make a fake root object containing our value under the key of ''.
393
+ // Return the result of stringifying the value.
394
+
395
+ return str('', {'': value});
396
+ };
397
+ }
398
+
399
+
400
+ // If the JSON object does not yet have a parse method, give it one.
401
+
402
+ if (typeof JSON.parse !== 'function') {
403
+ JSON.parse = function (text, reviver) {
404
+
405
+ // The parse method takes a text and an optional reviver function, and returns
406
+ // a JavaScript value if the text is a valid JSON text.
407
+
408
+ var j;
409
+
410
+ function walk(holder, key) {
411
+
412
+ // The walk method is used to recursively walk the resulting structure so
413
+ // that modifications can be made.
414
+
415
+ var k, v, value = holder[key];
416
+ if (value && typeof value === 'object') {
417
+ for (k in value) {
418
+ if (Object.hasOwnProperty.call(value, k)) {
419
+ v = walk(value, k);
420
+ if (v !== undefined) {
421
+ value[k] = v;
422
+ } else {
423
+ delete value[k];
424
+ }
425
+ }
426
+ }
427
+ }
428
+ return reviver.call(holder, key, value);
429
+ }
430
+
431
+
432
+ // Parsing happens in four stages. In the first stage, we replace certain
433
+ // Unicode characters with escape sequences. JavaScript handles many characters
434
+ // incorrectly, either silently deleting them, or treating them as line endings.
435
+
436
+ text = String(text);
437
+ cx.lastIndex = 0;
438
+ if (cx.test(text)) {
439
+ text = text.replace(cx, function (a) {
440
+ return '\\u' +
441
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
442
+ });
443
+ }
444
+
445
+ // In the second stage, we run the text against regular expressions that look
446
+ // for non-JSON patterns. We are especially concerned with '()' and 'new'
447
+ // because they can cause invocation, and '=' because it can cause mutation.
448
+ // But just to be safe, we want to reject all unexpected forms.
449
+
450
+ // We split the second stage into 4 regexp operations in order to work around
451
+ // crippling inefficiencies in IE's and Safari's regexp engines. First we
452
+ // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
453
+ // replace all simple value tokens with ']' characters. Third, we delete all
454
+ // open brackets that follow a colon or comma or that begin the text. Finally,
455
+ // we look to see that the remaining characters are only whitespace or ']' or
456
+ // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
457
+
458
+ if (/^[\],:{}\s]*$/.
459
+ test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
460
+ replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
461
+ replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
462
+
463
+ // In the third stage we use the eval function to compile the text into a
464
+ // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
465
+ // in JavaScript: it can begin a block or an object literal. We wrap the text
466
+ // in parens to eliminate the ambiguity.
467
+
468
+ j = eval('(' + text + ')');
469
+
470
+ // In the optional fourth stage, we recursively walk the new structure, passing
471
+ // each name/value pair to a reviver function for possible transformation.
472
+
473
+ return typeof reviver === 'function' ?
474
+ walk({'': j}, '') : j;
475
+ }
476
+
477
+ // If the text is not JSON parseable, then a SyntaxError is thrown.
478
+
479
+ throw new SyntaxError('JSON.parse');
480
+ };
481
+ }
482
+ }());
@@ -0,0 +1,54 @@
1
+ module CouchPotato
2
+ module RSpec
3
+ class ListAsProxy
4
+ def initialize(results_ruby)
5
+ @results_ruby = results_ruby
6
+ end
7
+
8
+ def as(expected_ruby)
9
+ ListAsMatcher.new(expected_ruby, @results_ruby)
10
+ end
11
+ end
12
+
13
+ class ListAsMatcher
14
+ include RunJS
15
+
16
+ def initialize(expected_ruby, results_ruby)
17
+ @expected_ruby = expected_ruby
18
+ @results_ruby = results_ruby
19
+ end
20
+
21
+ def matches?(view_spec)
22
+ js = <<-JS
23
+ #{File.read(File.dirname(__FILE__) + '/print_r.js')}
24
+ #{File.read(File.dirname(__FILE__) + '/json2.js')}
25
+ var results = #{@results_ruby.to_json};
26
+ var listed = '';
27
+ var list = #{view_spec.list_function};
28
+
29
+ var getRow = function() {
30
+ return results.rows.shift();
31
+ };
32
+ var send = function(text) {
33
+ listed = listed + text;
34
+ };
35
+ list();
36
+ print(print_r(JSON.parse(listed)));
37
+ JS
38
+
39
+ @actual_ruby = JSON.parse(run_js(js))
40
+
41
+ @expected_ruby == @actual_ruby
42
+ end
43
+
44
+ def failure_message_for_should
45
+ "Expected to list as #{@expected_ruby.inspect} but got #{@actual_ruby.inspect}."
46
+ end
47
+
48
+ def failure_message_for_should_not
49
+ "Expected to not list as #{@expected_ruby.inspect} but did."
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -15,6 +15,7 @@ end
15
15
 
16
16
  require 'couch_potato/rspec/matchers/map_to_matcher'
17
17
  require 'couch_potato/rspec/matchers/reduce_to_matcher'
18
+ require 'couch_potato/rspec/matchers/list_as_matcher'
18
19
 
19
20
  module Spec
20
21
  module Matchers
@@ -29,6 +30,10 @@ module Spec
29
30
  def rereduce(docs, keys)
30
31
  CouchPotato::RSpec::ReduceToProxy.new(docs, keys, true)
31
32
  end
33
+
34
+ def list(results)
35
+ CouchPotato::RSpec::ListAsProxy.new(results)
36
+ end
32
37
  end
33
38
  end
34
39
 
@@ -1,16 +1,21 @@
1
1
  module CouchPotato
2
2
  module View
3
3
  class BaseViewSpec
4
- attr_reader :reduce_function, :design_document, :view_name, :view_parameters, :klass, :options
4
+ attr_reader :reduce_function, :list_name, :list_function, :design_document, :view_name, :view_parameters, :klass, :options
5
5
  private :klass, :options
6
6
 
7
7
  def initialize(klass, view_name, options, view_parameters)
8
8
  normalized_view_parameters = normalize_view_parameters view_parameters
9
+
10
+ @list_name = normalized_view_parameters.delete(:list) || options[:list]
11
+
9
12
  assert_valid_view_parameters normalized_view_parameters
10
13
  @klass = klass
11
14
  @design_document = translate_to_design_doc_name(klass.to_s)
12
15
  @view_name = view_name
13
16
  @options = options
17
+
18
+ @list_function = klass.lists(@list_name) if @list_name
14
19
  @view_parameters = {}
15
20
  [:group, :include_docs, :descending, :group_level, :limit].each do |key|
16
21
  @view_parameters[key] = options[key] if options.include?(key)
@@ -44,8 +44,7 @@ module CouchPotato
44
44
  end
45
45
 
46
46
  def _find_view(view) #:nodoc:
47
- return @views[view] if @views && @views[view]
48
- superclass._find_view(view) if superclass && superclass.respond_to?(:_find_view)
47
+ (@views && @views[view]) || (superclass._find_view(view) if superclass.respond_to?(:_find_view))
49
48
  end
50
49
  end
51
50
  end
@@ -0,0 +1,23 @@
1
+ module CouchPotato
2
+ module View
3
+ module Lists
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def list(name, function)
10
+ lists[name] = function
11
+ end
12
+
13
+ def lists(name = nil)
14
+ if name.nil?
15
+ @lists ||= {}
16
+ else
17
+ (@lists && @lists[name]) || (superclass.lists(name) if superclass.respond_to?(:lists))
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,12 +2,16 @@ module CouchPotato
2
2
  module View
3
3
  # Used to query views (and create them if they don't exist). Usually you won't have to use this class directly. Instead it is used internally by the CouchPotato::Database.view method.
4
4
  class ViewQuery
5
- def initialize(couchrest_database, design_document_name, view_name, map_function, reduce_function = nil)
5
+ def initialize(couchrest_database, design_document_name, view, list = nil)
6
6
  @database = couchrest_database
7
7
  @design_document_name = design_document_name
8
- @view_name = view_name
9
- @map_function = map_function
10
- @reduce_function = reduce_function
8
+ @view_name = view.keys[0]
9
+ @map_function = view.values[0][:map]
10
+ @reduce_function = view.values[0][:reduce]
11
+ if list
12
+ @list_function = list.values[0]
13
+ @list_name = list.keys[0]
14
+ end
11
15
  end
12
16
 
13
17
  def query_view!(parameters = {})
@@ -25,10 +29,15 @@ module CouchPotato
25
29
  def update_view
26
30
  design_doc = @database.get "_design/#{@design_document_name}" rescue nil
27
31
  original_views = design_doc && design_doc['views'].dup
32
+ original_lists = design_doc && design_doc['lists'] && design_doc['lists'].dup
28
33
  view_updated unless design_doc.nil?
29
34
  design_doc ||= empty_design_document
30
35
  design_doc['views'][@view_name.to_s] = view_functions
31
- @database.save_doc(design_doc) unless original_views == design_doc['views']
36
+ if @list_function
37
+ design_doc['lists'] ||= {}
38
+ design_doc['lists'][@list_name.to_s] = @list_function
39
+ end
40
+ @database.save_doc(design_doc) if original_views != design_doc['views'] || original_lists != design_doc['lists']
32
41
  end
33
42
 
34
43
  def view_functions
@@ -36,7 +45,7 @@ module CouchPotato
36
45
  end
37
46
 
38
47
  def empty_design_document
39
- {'views' => {}, "_id" => "_design/#{@design_document_name}", "language" => "javascript"}
48
+ {'views' => {}, 'lists' => {}, "_id" => "_design/#{@design_document_name}", "language" => "javascript"}
40
49
  end
41
50
 
42
51
  def view_has_been_updated?
@@ -53,7 +62,11 @@ module CouchPotato
53
62
  end
54
63
 
55
64
  def query_view(parameters)
56
- @database.view view_url, parameters
65
+ if @list_name
66
+ CouchRest.get CouchRest.paramify_url(CouchPotato.full_url_to_database + "/_design/#{@design_document_name}/_list/#{@list_name}/#{@view_name}", parameters)
67
+ else
68
+ @database.view view_url, parameters
69
+ end
57
70
  end
58
71
 
59
72
  def view_url
@@ -160,7 +160,11 @@ describe 'view' do
160
160
 
161
161
  describe "with array as key" do
162
162
  it "should create a map function with the composite key" do
163
- CouchPotato::View::ViewQuery.should_receive(:new).with(anything, anything, anything, string_matching(/emit\(\[doc\['time'\], doc\['state'\]\]/), anything).and_return(stub('view query', :query_view! => {'rows' => []}))
163
+ CouchPotato::View::ViewQuery.should_receive(:new) do |db, design_name, view, list|
164
+ view['key_array_timeline'][:map].should match(/emit\(\[doc\['time'\], doc\['state'\]\]/)
165
+
166
+ stub('view query', :query_view! => {'rows' => []})
167
+ end
164
168
  @db.view Build.key_array_timeline
165
169
  end
166
170
  end
@@ -191,4 +195,38 @@ describe 'view' do
191
195
  @db.view(CustomBuild.timeline).first.should be_kind_of(CustomBuild)
192
196
  end
193
197
  end
198
+
199
+ describe "list functions" do
200
+ class Coworker
201
+ include CouchPotato::Persistence
202
+
203
+ property :name
204
+
205
+ view :all_with_list, :key => :name, :list => :append_doe
206
+ view :all, :key => :name
207
+
208
+ list :append_doe, <<-JS
209
+ function(head, req) {
210
+ var row;
211
+ send('{"rows": [');
212
+ while(row = getRow()) {
213
+ row.doc.name = row.doc.name + ' doe';
214
+ send(JSON.stringify(row));
215
+ };
216
+ send(']}');
217
+ }
218
+ JS
219
+ end
220
+
221
+ it "should use the list function declared at class level" do
222
+ @db.save! Coworker.new(:name => 'joe')
223
+ @db.view(Coworker.all_with_list).first.name.should == 'joe doe'
224
+ end
225
+
226
+ it "should use the list function passed at runtime" do
227
+ @db.save! Coworker.new(:name => 'joe')
228
+ @db.view(Coworker.all(:list => :append_doe)).first.name.should == 'joe doe'
229
+ end
230
+
231
+ end
194
232
  end
@@ -39,10 +39,32 @@ describe CouchPotato::View::BaseViewSpec, 'initialize' do
39
39
  spec.view_parameters.should == {:key => '2'}
40
40
  end
41
41
 
42
- it "generates the design document path by snake_casing the class name but keeping double colons" do
42
+ it "should generate the design document path by snake_casing the class name but keeping double colons" do
43
43
  spec = CouchPotato::View::BaseViewSpec.new 'Foo::BarBaz', '', {}, ''
44
44
  spec.design_document.should == 'foo::bar_baz'
45
45
  end
46
+
47
+ it "should extract the list name from the options" do
48
+ spec = CouchPotato::View::BaseViewSpec.new stub(:lists => nil), 'all', {:list => 'test_list'}, {}
49
+ spec.list_name.should == 'test_list'
50
+ end
51
+
52
+ it "should extract the list from the view parameters" do
53
+ spec = CouchPotato::View::BaseViewSpec.new stub(:lists => nil), 'all', {}, {:list => 'test_list'}
54
+ spec.list_name.should == 'test_list'
55
+ end
56
+
57
+ it "should prefer the list name from the view parameters over the one from the options" do
58
+ spec = CouchPotato::View::BaseViewSpec.new stub(:lists => nil), 'all', {:list => 'my_list'}, {:list => 'test_list'}
59
+ spec.list_name.should == 'test_list'
60
+ end
61
+
62
+ it "should return the list function" do
63
+ klass = stub 'class'
64
+ klass.stub(:lists).with('test_list').and_return('<list_code>')
65
+ spec = CouchPotato::View::BaseViewSpec.new klass, 'all', {:list => 'test_list'}, {}
66
+ spec.list_function.should == '<list_code>'
67
+ end
46
68
 
47
69
  end
48
70
 
@@ -4,15 +4,23 @@ describe 'callbacks' do
4
4
  class Tree
5
5
  include CouchPotato::Persistence
6
6
 
7
- before_validation :grow_leaf, lambda {|tree| tree.root_count ||= 0; tree.root_count += 1 }
7
+ before_validation :grow_leaf, 'grow_branch', lambda {|tree| tree.root_count ||= 0; tree.root_count += 1 }
8
8
 
9
9
  property :leaf_count
10
10
  property :root_count
11
+ property :branch_count
12
+ property :watered
13
+
11
14
 
12
15
  def grow_leaf
13
16
  self.leaf_count ||= 0
14
17
  self.leaf_count += 1
15
18
  end
19
+
20
+ def grow_branch
21
+ self.branch_count ||= 0
22
+ self.branch_count += 1
23
+ end
16
24
  end
17
25
 
18
26
  class AppleTree < Tree
@@ -29,11 +37,17 @@ describe 'callbacks' do
29
37
  end
30
38
  end
31
39
 
32
- it "should call a method when validated" do
40
+ it "should call a method from a symbol when validated" do
33
41
  tree = Tree.new(:leaf_count => 1, :root_count => 1)
34
42
  tree.valid?
35
43
  tree.leaf_count.should == 2
36
44
  end
45
+
46
+ it "should call a method from a string when validated" do
47
+ tree = Tree.new(:branch_count => 0)
48
+ tree.valid?
49
+ tree.branch_count.should == 1
50
+ end
37
51
 
38
52
  it "should call a lambda when validated" do
39
53
  tree = Tree.new(:leaf_count => 1, :root_count => 1)
@@ -208,12 +208,42 @@ end
208
208
 
209
209
  describe CouchPotato::Database, 'view' do
210
210
  before(:each) do
211
- @db = CouchPotato::Database.new(stub('couchrest db').as_null_object)
211
+ @couchrest_db = stub('couchrest db').as_null_object
212
+ @db = CouchPotato::Database.new(@couchrest_db)
212
213
  @result = stub('result')
213
214
  @spec = stub('view spec', :process_results => [@result]).as_null_object
214
215
  CouchPotato::View::ViewQuery.stub(:new => stub('view query', :query_view! => {'rows' => [@result]}))
215
216
  end
216
217
 
218
+ it "should initialze a view query with map/reduce/list funtions" do
219
+ @spec.stub(:design_document => 'design_doc', :view_name => 'my_view',
220
+ :map_function => '<map_code>', :reduce_function => '<reduce_code>',
221
+ :list_name => 'my_list', :list_function => '<list_code>')
222
+ CouchPotato::View::ViewQuery.should_receive(:new).with(
223
+ @couchrest_db,
224
+ 'design_doc',
225
+ {'my_view' => {
226
+ :map => '<map_code>',
227
+ :reduce => '<reduce_code>'
228
+ }},
229
+ {'my_list' => '<list_code>'})
230
+ @db.view(@spec)
231
+ end
232
+
233
+ it "should initialze a view query with only map/reduce functions" do
234
+ @spec.stub(:design_document => 'design_doc', :view_name => 'my_view',
235
+ :map_function => '<map_code>', :reduce_function => '<reduce_code>',
236
+ :list_name => nil, :list_function => nil)
237
+ CouchPotato::View::ViewQuery.should_receive(:new).with(
238
+ @couchrest_db,
239
+ 'design_doc',
240
+ {'my_view' => {
241
+ :map => '<map_code>',
242
+ :reduce => '<reduce_code>'
243
+ }}, nil)
244
+ @db.view(@spec)
245
+ end
246
+
217
247
  it "should set itself on returned results that have an accessor" do
218
248
  @result.stub(:respond_to?).with(:database=).and_return(true)
219
249
  @result.should_receive(:database=).with(@db)
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe CouchPotato::View::Lists, '.list' do
4
+ it "should make the list function available via .lists" do
5
+ clazz = Class.new
6
+ clazz.send :include, CouchPotato::View::Lists
7
+ clazz.list 'my_list', '<list_code>'
8
+
9
+ clazz.lists('my_list').should == '<list_code>'
10
+ end
11
+
12
+ it "should make the list available to subclasses" do
13
+ clazz = Class.new
14
+ clazz.send :include, CouchPotato::View::Lists
15
+ clazz.list 'my_list', '<list_code>'
16
+ sub_clazz = Class.new clazz
17
+
18
+ sub_clazz.lists('my_list').should == '<list_code>'
19
+ end
20
+ end
@@ -1,12 +1,11 @@
1
1
  require 'spec_helper'
2
2
  require 'couch_potato/rspec'
3
- require 'ostruct'
4
3
 
5
4
  describe CouchPotato::RSpec::MapToMatcher do
6
5
 
7
6
  describe "basic map function" do
8
7
  before(:each) do
9
- @view_spec = OpenStruct.new(:map_function => "function(doc) {emit(doc.name, doc.tags.length);}")
8
+ @view_spec = stub(:map_function => "function(doc) {emit(doc.name, doc.tags.length);}")
10
9
  end
11
10
 
12
11
  it "should pass if the given function emits the expected javascript" do
@@ -20,7 +19,7 @@ describe CouchPotato::RSpec::MapToMatcher do
20
19
 
21
20
  describe "functions emitting multiple times" do
22
21
  before(:each) do
23
- @view_spec = OpenStruct.new(:map_function => "function(doc) {emit(doc.name, doc.tags.length); emit(doc.tags[0], doc.tags[1])};")
22
+ @view_spec = stub(:map_function => "function(doc) {emit(doc.name, doc.tags.length); emit(doc.tags[0], doc.tags[1])};")
24
23
  end
25
24
 
26
25
  it "should pass if the given function emits the expected javascript" do
@@ -34,7 +33,7 @@ describe CouchPotato::RSpec::MapToMatcher do
34
33
 
35
34
  describe "failing specs" do
36
35
  before(:each) do
37
- @view_spec = OpenStruct.new(:map_function => "function(doc) {emit(doc.name, null)}")
36
+ @view_spec = stub(:map_function => "function(doc) {emit(doc.name, null)}")
38
37
  end
39
38
 
40
39
  it "should have a nice error message for failing should" do
@@ -53,7 +52,7 @@ end
53
52
 
54
53
  describe CouchPotato::RSpec::ReduceToMatcher do
55
54
  before(:each) do
56
- @view_spec = OpenStruct.new(:reduce_function => "function(docs, keys, rereduce) {
55
+ @view_spec = stub(:reduce_function => "function(docs, keys, rereduce) {
57
56
  if(rereduce) {
58
57
  return(sum(keys) * 2);
59
58
  } else {
@@ -94,4 +93,32 @@ describe CouchPotato::RSpec::ReduceToMatcher do
94
93
  }.should raise_error('Expected not to reduce to 6 but did.')
95
94
  end
96
95
  end
96
+ end
97
+
98
+ describe CouchPotato::RSpec::ListAsMatcher do
99
+ before(:each) do
100
+ @view_spec = stub(:list_function => "function() {var row = getRow(); send(JSON.stringify([{text: row.text + ' world'}]));}")
101
+ end
102
+
103
+ it "should pass if the function return the expected json" do
104
+ @view_spec.should list({'rows' => [{:text => 'hello'}]}).as([{'text' => 'hello world'}])
105
+ end
106
+
107
+ it "should not pass if the function does not return the expected json" do
108
+ @view_spec.should_not list({'rows' => [{:text => 'hello'}]}).as([{'text' => 'hello there'}])
109
+ end
110
+
111
+ describe "failing specs" do
112
+ it "should have a nice error message for failing should" do
113
+ lambda {
114
+ @view_spec.should list({'rows' => [{:text => 'hello'}]}).as([{'text' => 'hello there'}])
115
+ }.should raise_error('Expected to list as [{"text"=>"hello there"}] but got [{"text"=>"hello world"}].')
116
+ end
117
+
118
+ it "should have a nice error message for failing should not" do
119
+ lambda {
120
+ @view_spec.should_not list({'rows' => [{:text => 'hello'}]}).as([{'text' => 'hello world'}])
121
+ }.should raise_error('Expected to not list as [{"text"=>"hello world"}] but did.')
122
+ end
123
+ end
97
124
  end
@@ -1,21 +1,78 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe CouchPotato::View::ViewQuery, 'query_view' do
3
+ describe CouchPotato::View::ViewQuery, 'query_view!' do
4
+ before(:each) do
5
+ CouchRest.stub(:get => nil)
6
+ end
7
+
4
8
  it "should not pass a key if conditions are empty" do
5
9
  db = mock 'db', :get => nil, :save_doc => nil
6
10
  db.should_receive(:view).with(anything, {})
7
- CouchPotato::View::ViewQuery.new(db, '', '', '', '').query_view!
11
+ CouchPotato::View::ViewQuery.new(db, '', {:view0 => {}}).query_view!
12
+ end
13
+
14
+ it "should not update a view when the map/reduce functions haven't changed" do
15
+ db = mock 'db', :get => {'views' => {'view' => {'map' => '<map_code>', 'reduce' => '<reduce_code>'}}}, :view => nil
16
+ db.should_not_receive(:save_doc)
17
+ CouchPotato::View::ViewQuery.new(db, 'design', :view => {:map => '<map_code>', :reduce => '<reduce_code>'}).query_view!
8
18
  end
9
19
 
10
- it "should not update a view when the functions haven't changed" do
11
- db = mock 'db', :get => {'views' => {'view' => {'map' => 'map', 'reduce' => 'reduce'}}}, :view => nil
20
+ it "should not update a view when the list function hasn't changed" do
21
+ db = mock 'db', :get => {'views' => {'view' => {'map' => '<map_code>', 'reduce' => '<reduce_code>'}}, 'lists' => {'list0' => '<list_code>'}}, :view => nil
12
22
  db.should_not_receive(:save_doc)
13
- CouchPotato::View::ViewQuery.new(db, 'design', 'view', 'map', 'reduce').query_view!
23
+ CouchPotato::View::ViewQuery.new(db, 'design', {:view => {:map => '<map_code>', :reduce => '<reduce_code>'}}, :list0 => '<list_code>').query_view!
24
+ end
25
+
26
+ it "should update a view when the map function has changed" do
27
+ db = mock 'db', :get => {'views' => {'view2' => {'map' => '<map_code>', 'reduce' => '<reduce_code>'}}}, :view => nil
28
+ db.should_receive(:save_doc)
29
+ CouchPotato::View::ViewQuery.new(db, 'design', :view2 => {:map => '<new map_code>', :recude => '<reduce_code>'}).query_view!
30
+ end
31
+
32
+ it "should update a view when the reduce function has changed" do
33
+ db = mock 'db', :get => {'views' => {'view3' => {'map' => '<map_code>', 'reduce' => '<reduce_code>'}}}, :view => nil
34
+ db.should_receive(:save_doc)
35
+ CouchPotato::View::ViewQuery.new(db, 'design', :view3 => {:map => '<map_code>', :reduce => '<new reduce_code>'}).query_view!
14
36
  end
15
37
 
16
- it "should update a view when the functions have changed" do
17
- db = mock 'db', :get => {'views' => {'view2' => {'map' => 'map', 'reduce' => 'reduce'}}}, :view => nil
38
+ it "should update a view when the list function has changed" do
39
+ db = mock 'db', :get => {
40
+ 'views' => {'view4' => {'map' => '<map_code>', 'reduce' => '<reduce_code>'}},
41
+ 'lists' => {'list1' => '<list_code>'}
42
+ }, :view => nil
18
43
  db.should_receive(:save_doc)
19
- CouchPotato::View::ViewQuery.new(db, 'design', 'view2', 'mapnew', 'reduce').query_view!
44
+ CouchPotato::View::ViewQuery.new(db, 'design', {:view4 => {:map => '<map_code>', :reduce => '<reduce_code>'}}, :list1 => '<new_list_code>').query_view!
20
45
  end
46
+
47
+ it "should update a view when there wasn't a list function but now there is one" do
48
+ db = mock 'db', :get => {
49
+ 'views' => {'view5' => {'map' => '<map_code>', 'reduce' => '<reduce_code>'}}
50
+ }, :view => nil
51
+ db.should_receive(:save_doc)
52
+ CouchPotato::View::ViewQuery.new(db, 'design', {:view5 => {:map => '<map_code>', :reduce => '<reduce_code>'}}, :list1 => '<new_list_code>').query_view!
53
+ end
54
+
55
+ it "should not update a view when there is a list function but no list function is passed" do
56
+ db = mock 'db', :get => {
57
+ 'views' => {'view6' => {'map' => '<map_code>', 'reduce' => '<reduce_code>'}},
58
+ 'lists' => {'list1' => '<list_code>'}
59
+ }, :view => nil
60
+ db.should_not_receive(:save_doc)
61
+ CouchPotato::View::ViewQuery.new(db, 'design', {:view6 => {:map => '<map_code>', :reduce => '<reduce_code>'}}, {}).query_view!
62
+ end
63
+
64
+ it "should not update a view when there were no lists before and no list function is passed" do
65
+ db = mock 'db', :get => {
66
+ 'views' => {'view6' => {'map' => '<map_code>', 'reduce' => '<reduce_code>'}}
67
+ }, :view => nil
68
+ db.should_not_receive(:save_doc)
69
+ CouchPotato::View::ViewQuery.new(db, 'design', {:view6 => {:map => '<map_code>', :reduce => '<reduce_code>'}}, {}).query_view!
70
+ end
71
+
72
+ it "should query CouchRest directly when querying a list" do
73
+ db = stub('db').as_null_object
74
+ CouchRest.should_receive(:get).with('http://127.0.0.1:5984/couch_potato_test/_design/my_design/_list/list1/view7?key=1')
75
+ CouchPotato::View::ViewQuery.new(db, 'my_design', {:view7 => {:map => '<map_code>', :reduce => '<reduce_code>'}}, :list1 => '<new_list_code>').query_view!(:key => 1)
76
+ end
77
+
21
78
  end
@@ -7,8 +7,8 @@ describe "automatic view updates" do
7
7
  end
8
8
 
9
9
  it "should update a view that doesn't match the given functions" do
10
- CouchPotato::View::ViewQuery.new(@db, 'test_design1', 'test_view', 'function(doc) {}', 'function() {}').query_view! # create view
11
- CouchPotato::View::ViewQuery.new(@db, 'test_design1', 'test_view', 'function(doc) {emit(doc.id, null)}', 'function(key, values) {return sum(values)}').query_view!
10
+ CouchPotato::View::ViewQuery.new(@db, 'test_design1', {'test_view' => {:map => 'function(doc) {}', :reduce => 'function() {}'}}, nil).query_view! # create view
11
+ CouchPotato::View::ViewQuery.new(@db, 'test_design1', {'test_view' => {:map => 'function(doc) {emit(doc.id, null)}', :reduce => 'function(key, values) {return sum(values)}'}}, nil).query_view!
12
12
  CouchPotato.database.load('_design/test_design1')['views']['test_view'].should == {
13
13
  'map' => 'function(doc) {emit(doc.id, null)}',
14
14
  'reduce' => 'function(key, values) {return sum(values)}'
@@ -16,9 +16,9 @@ describe "automatic view updates" do
16
16
  end
17
17
 
18
18
  it "should only update a view once to avoid writing the view for every request" do
19
- CouchPotato::View::ViewQuery.new(@db, 'test_design2', 'test_view', 'function(doc) {}', 'function() {}').query_view! # create view
20
- CouchPotato::View::ViewQuery.new(@db, 'test_design2', 'test_view', 'function(doc) {emit(doc.id, null)}', 'function(key, values) {return sum(values)}').query_view!
21
- CouchPotato::View::ViewQuery.new(@db, 'test_design2', 'test_view', 'function(doc) {}', 'function() {}').query_view!
19
+ CouchPotato::View::ViewQuery.new(@db, 'test_design2', {'test_view' => {:map => 'function(doc) {}', :reduce => 'function() {}'}}, nil).query_view! # create view
20
+ CouchPotato::View::ViewQuery.new(@db, 'test_design2', {'test_view' => {:map => 'function(doc) {emit(doc.id, null)}', :reduce => 'function(key, values) {return sum(values)}'}}, nil).query_view!
21
+ CouchPotato::View::ViewQuery.new(@db, 'test_design2', {'test_view' => {:map => 'function(doc) {}', :reduce => 'function() {}'}}, nil).query_view!
22
22
  CouchPotato.database.load('_design/test_design2')['views']['test_view'].should == {
23
23
  'map' => 'function(doc) {emit(doc.id, null)}',
24
24
  'reduce' => 'function(key, values) {return sum(values)}'
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 2
8
- - 32
9
- version: 0.2.32
7
+ - 3
8
+ - 0
9
+ version: 0.3.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Alexander Lang
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-07-13 00:00:00 +02:00
17
+ date: 2010-07-23 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -77,6 +77,8 @@ files:
77
77
  - lib/couch_potato/railtie.rb
78
78
  - lib/couch_potato/rspec.rb
79
79
  - lib/couch_potato/rspec/matchers.rb
80
+ - lib/couch_potato/rspec/matchers/json2.js
81
+ - lib/couch_potato/rspec/matchers/list_as_matcher.rb
80
82
  - lib/couch_potato/rspec/matchers/map_to_matcher.rb
81
83
  - lib/couch_potato/rspec/matchers/print_r.js
82
84
  - lib/couch_potato/rspec/matchers/reduce_to_matcher.rb
@@ -87,6 +89,7 @@ files:
87
89
  - lib/couch_potato/view/base_view_spec.rb
88
90
  - lib/couch_potato/view/custom_view_spec.rb
89
91
  - lib/couch_potato/view/custom_views.rb
92
+ - lib/couch_potato/view/lists.rb
90
93
  - lib/couch_potato/view/model_view_spec.rb
91
94
  - lib/couch_potato/view/properties_view_spec.rb
92
95
  - lib/couch_potato/view/raw_view_spec.rb
@@ -116,6 +119,7 @@ files:
116
119
  - spec/unit/date_spec.rb
117
120
  - spec/unit/dirty_attributes_spec.rb
118
121
  - spec/unit/json_create_id_spec.rb
122
+ - spec/unit/lists_spec.rb
119
123
  - spec/unit/model_view_spec_spec.rb
120
124
  - spec/unit/properties_view_spec_spec.rb
121
125
  - spec/unit/rspec_matchers_spec.rb
@@ -179,6 +183,7 @@ test_files:
179
183
  - spec/unit/date_spec.rb
180
184
  - spec/unit/dirty_attributes_spec.rb
181
185
  - spec/unit/json_create_id_spec.rb
186
+ - spec/unit/lists_spec.rb
182
187
  - spec/unit/model_view_spec_spec.rb
183
188
  - spec/unit/properties_view_spec_spec.rb
184
189
  - spec/unit/rspec_matchers_spec.rb