couch_potato 0.2.32 → 0.3.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.
- data/CHANGES.md +3 -0
- data/README.md +35 -2
- data/VERSION.yml +2 -2
- data/lib/couch_potato/database.rb +9 -3
- data/lib/couch_potato/persistence/callbacks.rb +2 -2
- data/lib/couch_potato/persistence.rb +2 -1
- data/lib/couch_potato/rspec/matchers/json2.js +482 -0
- data/lib/couch_potato/rspec/matchers/list_as_matcher.rb +54 -0
- data/lib/couch_potato/rspec/matchers.rb +5 -0
- data/lib/couch_potato/view/base_view_spec.rb +6 -1
- data/lib/couch_potato/view/custom_views.rb +1 -2
- data/lib/couch_potato/view/lists.rb +23 -0
- data/lib/couch_potato/view/view_query.rb +20 -7
- data/spec/custom_view_spec.rb +39 -1
- data/spec/unit/base_view_spec_spec.rb +23 -1
- data/spec/unit/callbacks_spec.rb +16 -2
- data/spec/unit/database_spec.rb +31 -1
- data/spec/unit/lists_spec.rb +20 -0
- data/spec/unit/rspec_matchers_spec.rb +32 -5
- data/spec/unit/view_query_spec.rb +65 -8
- data/spec/view_updates_spec.rb +5 -5
- metadata +9 -4
data/CHANGES.md
CHANGED
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
@@ -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(
|
48
|
-
|
49
|
-
spec.
|
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
|
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 #{
|
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 ' '),
|
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
|
-
|
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,
|
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 =
|
9
|
-
@map_function =
|
10
|
-
@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
|
-
@
|
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
|
-
@
|
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
|
data/spec/custom_view_spec.rb
CHANGED
@@ -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)
|
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 "
|
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
|
|
data/spec/unit/callbacks_spec.rb
CHANGED
@@ -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)
|
data/spec/unit/database_spec.rb
CHANGED
@@ -208,12 +208,42 @@ end
|
|
208
208
|
|
209
209
|
describe CouchPotato::Database, 'view' do
|
210
210
|
before(:each) do
|
211
|
-
@
|
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 =
|
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 =
|
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 =
|
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 =
|
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, '',
|
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
|
11
|
-
db = mock 'db', :get => {'views' => {'view' => {'map' => '
|
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',
|
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
|
17
|
-
db = mock 'db', :get => {
|
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', '
|
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
|
data/spec/view_updates_spec.rb
CHANGED
@@ -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'
|
11
|
-
CouchPotato::View::ViewQuery.new(@db, 'test_design1', 'test_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'
|
20
|
-
CouchPotato::View::ViewQuery.new(@db, 'test_design2', 'test_view'
|
21
|
-
CouchPotato::View::ViewQuery.new(@db, 'test_design2', 'test_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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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-
|
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
|