ShyCouch 0.6.0 → 0.7.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/Gemfile +4 -3
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/lib/ShyCouch.rb +274 -198
- data/lib/ShyCouch/data.rb +445 -243
- data/readme.md +117 -0
- data/test/test_ShyCouch.rb +17 -25
- data/test/test_couch_document.rb +171 -7
- data/test/test_couchdb_api.rb +45 -6
- data/test/test_couchdb_factory.rb +3 -3
- data/test/test_design_documents.rb +150 -129
- data/test/test_document_validation.rb +85 -0
- data/test/test_fields.rb +12 -12
- data/test/test_views.rb +129 -33
- metadata +49 -42
- data/.document +0 -5
- data/Gemfile.lock +0 -36
- data/README +0 -117
- data/ShyCouch.gemspec +0 -83
- data/design_notes.rb +0 -39
data/lib/ShyCouch/data.rb
CHANGED
@@ -2,254 +2,456 @@ module ShyCouch
|
|
2
2
|
|
3
3
|
module Data
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
db ||= @database
|
74
|
-
new_doc = db.pull_document(self)
|
75
|
-
return new_doc
|
76
|
-
end
|
77
|
-
|
78
|
-
def pull!(db = nil)
|
79
|
-
db ||= @database
|
80
|
-
new_doc = pull(db)
|
81
|
-
if new_doc
|
82
|
-
self.clear
|
83
|
-
self.merge! new_doc
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def push!(database=nil)
|
88
|
-
database ||= @database
|
89
|
-
res = database.push_document!(self)
|
90
|
-
self["_id"] = res["id"] unless self["_id"]
|
91
|
-
self["_rev"] = res["rev"]
|
92
|
-
return res
|
93
|
-
end
|
94
|
-
|
95
|
-
def valid?; to_json ? true : false; end
|
96
|
-
|
97
|
-
def to_json
|
98
|
-
JSON::generate(self)
|
99
|
-
rescue JSON::GeneratorError
|
100
|
-
false
|
101
|
-
end
|
102
|
-
|
103
|
-
def method_missing(m, *a)
|
104
|
-
# Makes the object behave as if the hash keys are instance properties with attr_accessors
|
105
|
-
# Had a dozen lines or so for this and found a one-line implementation of the same thing in Camping.
|
106
|
-
m.to_s =~ /=$/ ? self[$`] = a[0] : a == [] ? self[m.to_s] : super
|
107
|
-
end
|
108
|
-
|
109
|
-
def respond_to?(method)
|
110
|
-
# so that testing for whether it responds to a method is equivalent to testing for the existence of a key
|
111
|
-
self.key?(method.to_s) ? true : super
|
112
|
-
end
|
113
|
-
|
114
|
-
end
|
115
|
-
|
116
|
-
class View
|
117
|
-
attr_accessor :map, :reduce, :name
|
118
|
-
JS_MAP_FUNCTION_HEADER = "function ( doc ) { \n "
|
119
|
-
JS_REDUCE_FUNCTION_HEADER = "function(key, values, rereduce) { \n "
|
120
|
-
JS_FUNCTION_FOOTER = "}"
|
121
|
-
|
122
|
-
def initialize(view_name, &block)
|
123
|
-
#O TODO - oh dear this is a nightmare
|
124
|
-
@parser = ShyRubyJS::ShySexpParser.new
|
125
|
-
sexp_check = block.to_sexp
|
126
|
-
sexp = block.to_sexp(:strip_enclosure=>true)
|
127
|
-
|
128
|
-
# make sure the two blocks inside are calls to "map" and "reduce"
|
129
|
-
|
130
|
-
@name = view_name.to_s
|
131
|
-
if sexp[0] == :block
|
132
|
-
unless sexp_check[3][1][1][2] == :map and sexp_check[3][2][1][2] == :reduce
|
133
|
-
raise ShyCouchError, "view must be called with map block and optional reduce block"
|
134
|
-
end
|
135
|
-
[1,2].each { |num|
|
136
|
-
2.times { sexp[num].delete_at(1) }
|
137
|
-
}
|
138
|
-
@map = JS_MAP_FUNCTION_HEADER + @parser.parse(sexp[1])[0] + JS_FUNCTION_FOOTER
|
139
|
-
@reduce = JS_REDUCE_FUNCTION_HEADER + @parser.parse(sexp[2])[0] + JS_FUNCTION_FOOTER
|
140
|
-
elsif sexp[0] == :iter
|
141
|
-
raise ShyCouchError, "view must be called with map block and optional reduce block" unless sexp[1][2] == :map
|
142
|
-
@map = JS_MAP_FUNCTION_HEADER + @parser.parse(sexp[3]) + JS_FUNCTION_FOOTER
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
def as_hash
|
147
|
-
h = {}
|
148
|
-
h[@name] = {"map" => @map}
|
149
|
-
h.merge!({"reduce" => @reduce}) if @reduce
|
150
|
-
return h
|
151
|
-
end
|
152
|
-
|
153
|
-
def functions
|
154
|
-
return {"map" => @map, "reduce" => @reduce}
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
class ViewResult < Array
|
159
|
-
attr_accessor :total_rows, :offset
|
160
|
-
def initialize(res)
|
161
|
-
@total_rows = res["total_rows"]
|
162
|
-
@offset = res["offset"]
|
163
|
-
concat res["rows"]
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
class CouchDocumentCollection < Array
|
168
|
-
def <<(obj)
|
169
|
-
raise TypeError unless obj.kind_of?(ShyCouch::Data::CouchDocument)
|
170
|
-
super
|
171
|
-
end
|
172
|
-
|
173
|
-
def initialize(opts = {})
|
174
|
-
@database = opts[:push_to] if opts[:push_to]
|
5
|
+
class CouchDocument < Hash
|
6
|
+
class << self
|
7
|
+
# allows instance.class.requirements to be called
|
8
|
+
def constraints; @@constraints; end
|
9
|
+
def target_db; @@target_db[self]; end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Constraints are a class variable hash keyed by subclass, with each of those keyed by "needs" and "suggests"
|
13
|
+
# When there's a validation test, it cycles through each key and applies the constraints if the current class is
|
14
|
+
# kind_of? the object reference stored in that key. This means you can do model inheritence for common required fields.
|
15
|
+
# The way they're implemented might not be performant? But models should be defined at runtime so it should be fine.
|
16
|
+
@@constraints = {}
|
17
|
+
|
18
|
+
# @@target_db is a class variable storing the default database to push a document to if push! is called without an argument
|
19
|
+
# it's keyed by class name to allow different values for subclasses
|
20
|
+
@@target_db = {}
|
21
|
+
|
22
|
+
def initialize(opts={})
|
23
|
+
# Assumes that the "kind" is the class name unless explicitly stated otherwise
|
24
|
+
# TODO - maybe just force it to be the class name no matter what tbh
|
25
|
+
|
26
|
+
# this is messy.
|
27
|
+
# If there's some data given, see whether it has a kind.
|
28
|
+
if opts[:data]
|
29
|
+
if !opts[:data]["kind"]
|
30
|
+
# If there's no kind, give the 'kind' the class name as a value
|
31
|
+
opts[:data]["kind"] = self.class.to_s.split("::").last
|
32
|
+
end
|
33
|
+
else
|
34
|
+
# if there's no data, give it data and a kind
|
35
|
+
opts[:data] = {"kind" => self.class.to_s.split("::").last}
|
36
|
+
end
|
37
|
+
|
38
|
+
merge!(opts[:data])
|
39
|
+
@database = opts[:push_to] if opts[:push_to]
|
40
|
+
raise TypeError unless valid?
|
41
|
+
# set_up_views
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.all
|
45
|
+
# TODO
|
46
|
+
raise Exception, "not yet implemented"
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.needs(*needs)
|
50
|
+
# this is both a setter and a getter
|
51
|
+
# initialize suggests hash for current class if not done already
|
52
|
+
# is there a better way to do this initialization?
|
53
|
+
unless @@constraints.has_key?(self)
|
54
|
+
@@constraints[self] = {}
|
55
|
+
end
|
56
|
+
unless @@constraints[self].has_key?(:needs)
|
57
|
+
@@constraints[self][:needs] = []
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add the needs if any were passed in
|
61
|
+
if needs
|
62
|
+
needs.each do |need|
|
63
|
+
@@constraints[self][:needs] << need
|
64
|
+
end
|
65
|
+
end
|
66
|
+
# Return the array
|
67
|
+
return @@constraints[self][:needs]
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.push_to database
|
71
|
+
raise TypeError, "push_to expects a ShyCouch::CouchDatabase object. Object received was a #{database.class}" unless database.kind_of? ShyCouch::CouchDatabase
|
72
|
+
@@target_db[self] = database
|
175
73
|
end
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
74
|
+
|
75
|
+
def self.suggests(*suggestions)
|
76
|
+
# this is both a setter and a getter
|
77
|
+
# initialize suggests hash for current class if not done already
|
78
|
+
# is there a better way to do this initialization?
|
79
|
+
@@constraints[self] = {} unless @@constraints.has_key?(self)
|
80
|
+
@@constraints[self][:suggests] = [] unless @@constraints[self].has_key?(:suggests)
|
81
|
+
|
82
|
+
# Add the suggestions if any were passed in
|
83
|
+
if !suggestions.empty?
|
84
|
+
suggestions.each do |suggestion|
|
85
|
+
@@constraints[self][:suggests] << suggestion
|
86
|
+
end
|
87
|
+
end
|
88
|
+
# Return the array
|
89
|
+
return @@constraints[self][:suggests]
|
90
|
+
end
|
91
|
+
|
92
|
+
def satisfies_needs?
|
93
|
+
# Check everything in constraints that is this class or a superclass of this class
|
94
|
+
self.class.constraints.each do |classKey, constraints|
|
95
|
+
if self.kind_of?(classKey) and constraints[:needs]
|
96
|
+
constraints[:needs].each do |need|
|
97
|
+
return false unless has_key?(need)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
return true
|
102
|
+
end
|
103
|
+
|
104
|
+
def satisfies_suggestions?(opts = {})
|
105
|
+
# Same as satisfies_needs? but can be passed options for suggestions being ignored.
|
106
|
+
raise ArgumentError if opts.has_key?(:ignore_suggestions) and opts.has_key?(:ignore_suggestion)
|
107
|
+
opts.has_key?(:ignore_suggestions) ? ign = opts[:ignore_suggestions] :
|
108
|
+
opts.has_key?(:ignore_suggestion) ? ign = [opts[:ignore_suggestion]] : ign = []
|
109
|
+
|
110
|
+
@@constraints.each do |classKey, constraints|
|
111
|
+
if self.kind_of?(classKey) and constraints[:suggests]
|
112
|
+
constraints[:suggests].each do |suggestion|
|
113
|
+
return false unless has_key?(suggestion) or ign.include?(suggestion)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
return true
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_hash
|
121
|
+
h = {}
|
122
|
+
self.each do |k,v|
|
123
|
+
h[k] = v
|
124
|
+
end
|
125
|
+
return h
|
126
|
+
end
|
127
|
+
|
128
|
+
def missing_needs
|
129
|
+
missing = []
|
130
|
+
@@constraints.each do |classKey, constraints|
|
131
|
+
if self.kind_of?(classKey)
|
132
|
+
constraints[:needs].each do |need|
|
133
|
+
missing << need unless has_key?(:need)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
return missing
|
138
|
+
end
|
139
|
+
|
140
|
+
def missing_suggestions(opts = {})
|
141
|
+
missing = []
|
142
|
+
raise ArgumentError if opts.has_key?(:ignore_suggestions) and opts.has_key?(:ignore_suggestion)
|
143
|
+
opts.has_key?(:ignore_suggestions) ? ign = opts[:ignore_suggestions] :
|
144
|
+
opts.has_key?(:ignore_suggestion) ? ign = [opts[:ignore_suggestion]] : ign = []
|
145
|
+
|
146
|
+
@@constraints.each do |classKey, constraints|
|
147
|
+
if self.kind_of?(classKey)
|
148
|
+
constraints[:suggests].each do |suggestion|
|
149
|
+
missing << suggestion unless has_key?(:suggests) or ign.include?(suggestion)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
return missing
|
154
|
+
end
|
155
|
+
|
156
|
+
def needs?(need)
|
157
|
+
@@constraints.each do |classKey, constraints|
|
158
|
+
if self.kind_of?(classKey)
|
159
|
+
return true if constraints[:needs].include?(need)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
return false
|
163
|
+
end
|
164
|
+
|
165
|
+
def suggests?(suggestion)
|
166
|
+
@@constraints.each do |classKey, constraints|
|
167
|
+
if self.kind_of?(classKey)
|
168
|
+
return true if constraints[:suggests].include?(suggestion)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
return false
|
172
|
+
end
|
173
|
+
|
174
|
+
def suggests; self.class.suggests; end
|
175
|
+
def needs; self.class.needs; end
|
176
|
+
|
177
|
+
def attr_keys
|
178
|
+
# TODO - is this needed?
|
179
|
+
# returns the keys for all the attrs that aren't the id or rev
|
180
|
+
attr_keys = []
|
181
|
+
self.map { |k,v|
|
182
|
+
attr_keys << k unless k == "_id" or k == "_rev"
|
183
|
+
}
|
184
|
+
return attr_keys
|
185
|
+
end
|
186
|
+
|
187
|
+
def pull(opts = {})
|
188
|
+
raise ShyCouch::DocumentValidationError, "Document has no ID - cannot pull from database" unless has_key? "_id"
|
189
|
+
if opts[:pull_from]
|
190
|
+
db = opts[:pull_from]
|
191
|
+
elsif @database
|
192
|
+
db = @database
|
193
|
+
elsif @@target_db.has_key? self.class
|
194
|
+
db = @@target_db[self.class]
|
195
|
+
else
|
196
|
+
raise ShyCouch::ShyCouchError, "No database defined for document pull operation"
|
182
197
|
end
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
198
|
+
|
199
|
+
new_doc = db.pull_document(self)
|
200
|
+
return new_doc
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
def pull!(opts = {})
|
205
|
+
# TODO - just call pull and merge in the result
|
206
|
+
raise ShyCouch::DocumentValidationError, "Document has no ID - cannot pull from database" unless has_key? "_id"
|
207
|
+
if opts[:pull_from]
|
208
|
+
db = opts[:pull_from]
|
209
|
+
elsif @database
|
210
|
+
db = @database
|
211
|
+
elsif @@target_db.has_key? self.class
|
212
|
+
db = @@target_db[self.class]
|
213
|
+
else
|
214
|
+
raise ShyCouch::ShyCouchError, "No database defined for document pull operation"
|
190
215
|
end
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
216
|
+
new_doc = pull(:pull_from => db)
|
217
|
+
if new_doc
|
218
|
+
self.clear
|
219
|
+
self.merge! new_doc
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def push!(opts = {})
|
224
|
+
if opts[:push_to]
|
225
|
+
db = opts[:push_to]
|
226
|
+
elsif @database
|
227
|
+
db = @database
|
228
|
+
elsif @@target_db.has_key? self.class
|
229
|
+
db = @@target_db[self.class]
|
230
|
+
else
|
231
|
+
raise ShyCouch::ShyCouchError, "No database defined for document push operation"
|
197
232
|
end
|
198
|
-
|
233
|
+
res = db.push_document!(self, opts)
|
234
|
+
self["_id"] = res["id"] unless self["_id"]
|
235
|
+
self["_rev"] = res["rev"]
|
236
|
+
return res
|
237
|
+
end
|
199
238
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
239
|
+
def delete!(opts = {})
|
240
|
+
opts[:from] ? db = opts[:from] : db = @database
|
241
|
+
raise ShyCouchError, "No database specified for delete" unless db
|
242
|
+
res = db.delete_document!(self, opts)
|
243
|
+
self["_id"] = res["id"] unless self["_id"]
|
244
|
+
self["_rev"] = res["rev"]
|
245
|
+
return res
|
246
|
+
end
|
247
|
+
|
248
|
+
def valid?; to_json ? true : false; end
|
249
|
+
|
250
|
+
def to_json
|
251
|
+
JSON::generate(self)
|
252
|
+
rescue JSON::GeneratorError
|
253
|
+
false
|
254
|
+
end
|
255
|
+
|
256
|
+
def method_missing(m, *a)
|
257
|
+
# Makes the object behave as if the hash keys are instance properties with attr_accessors
|
258
|
+
# m.to_s =~ /=$/ ? self[$`] = a[0] : a == [] ? self[m.to_s] : super
|
259
|
+
m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m.to_s]:super
|
260
|
+
if m.to_s =~ /=$/
|
261
|
+
self[$`] = a[0]
|
262
|
+
else
|
263
|
+
if a == []
|
264
|
+
if self[m.to_s]
|
265
|
+
self[m.to_s]
|
266
|
+
else
|
267
|
+
super
|
268
|
+
end
|
269
|
+
else
|
270
|
+
super
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def respond_to?(method)
|
276
|
+
# so that testing for whether it responds to a method is equivalent to testing for the existence of a key
|
277
|
+
self.key?(method.to_s) ? true : super
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
class View
|
283
|
+
attr_accessor :map, :reduce, :name
|
284
|
+
JS_MAP_FUNCTION_HEADER = "function ( doc ) { \n "
|
285
|
+
JS_REDUCE_FUNCTION_HEADER = "function(key, values, rereduce) { \n "
|
286
|
+
JS_FUNCTION_FOOTER = "}"
|
287
|
+
|
288
|
+
def initialize(view_name, &block)
|
289
|
+
#O TODO - oh dear this is a nightmare
|
290
|
+
@parser = ShyRubyJS::ShySexpParser.new
|
291
|
+
sexp_check = block.to_sexp
|
292
|
+
sexp = block.to_sexp(:strip_enclosure=>true)
|
293
|
+
|
294
|
+
# make sure the two blocks inside are calls to "map" and "reduce"
|
295
|
+
|
296
|
+
@name = view_name.to_s
|
297
|
+
if sexp[0] == :block
|
298
|
+
unless sexp_check[3][1][1][2] == :map and sexp_check[3][2][1][2] == :reduce
|
299
|
+
raise ShyCouchError, "view must be called with map block and optional reduce block"
|
300
|
+
end
|
301
|
+
[1,2].each { |num|
|
302
|
+
2.times { sexp[num].delete_at(1) }
|
303
|
+
}
|
304
|
+
@map = JS_MAP_FUNCTION_HEADER + @parser.parse(sexp[1])[0] + JS_FUNCTION_FOOTER
|
305
|
+
@reduce = JS_REDUCE_FUNCTION_HEADER + @parser.parse(sexp[2])[0] + JS_FUNCTION_FOOTER
|
306
|
+
elsif sexp[0] == :iter
|
307
|
+
raise ShyCouchError, "view must be called with map block and optional reduce block" unless sexp[1][2] == :map
|
308
|
+
@map = JS_MAP_FUNCTION_HEADER + @parser.parse(sexp[3]) + JS_FUNCTION_FOOTER
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def functions
|
313
|
+
return {"map" => @map, "reduce" => @reduce}
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
class ViewResultHandler
|
318
|
+
def self.init res
|
319
|
+
if self.includes_docs res["rows"]
|
320
|
+
collection = ShyCouch::Data::CouchDocumentCollection.new
|
321
|
+
res["rows"].each do |row|
|
322
|
+
collection << ShyCouch::Data::CouchDocument.new(:data => row["doc"])
|
323
|
+
end
|
324
|
+
return collection
|
325
|
+
else
|
326
|
+
return ViewResult.new res
|
327
|
+
end
|
328
|
+
end
|
329
|
+
def self.includes_docs rows
|
330
|
+
return rows[0].has_key? "doc" if rows.length > 0
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
class ViewResult < Hash
|
335
|
+
# This is the result of a view query
|
336
|
+
# If the view was called with ?include_docs=true, initializing this object returns a DocumentCollection instead
|
337
|
+
|
338
|
+
attr_accessor :total_rows, :offset
|
339
|
+
def initialize res
|
340
|
+
@total_rows = res["total_rows"]
|
341
|
+
@offset = res["offset"]
|
342
|
+
|
343
|
+
res["rows"].each do |row|
|
344
|
+
merge! row["key"] => ViewResultRow.new(row["value"], row["key"])
|
345
|
+
end
|
346
|
+
end
|
347
|
+
class ViewResultRow
|
348
|
+
attr_reader :id
|
349
|
+
def initialize value, id
|
350
|
+
@_id = id
|
351
|
+
@value = value
|
352
|
+
end
|
353
|
+
def to_s
|
354
|
+
return @value
|
355
|
+
end
|
356
|
+
def inspect
|
357
|
+
return @value
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
|
363
|
+
class CouchDocumentCollection < Array
|
364
|
+
def << obj
|
365
|
+
raise TypeError unless obj.kind_of?(ShyCouch::Data::CouchDocument)
|
366
|
+
super
|
367
|
+
end
|
368
|
+
|
369
|
+
def initialize(opts = {})
|
370
|
+
@database = opts[:push_to] if opts[:push_to]
|
371
|
+
end
|
372
|
+
|
373
|
+
def pull_all(opts = {})
|
374
|
+
opts[:push_to] ? db = opts[:push_to] : db = @database
|
375
|
+
collection = CouchDocumentCollection.new
|
376
|
+
each do |item|
|
377
|
+
collection << db.pull_document(item)
|
378
|
+
end
|
379
|
+
return collection
|
380
|
+
end
|
381
|
+
|
382
|
+
def pull_all!(opts = {})
|
383
|
+
opts[:push_to] ? db = opts[:push_to] : db = @database
|
384
|
+
each do |item|
|
385
|
+
item.pull!(:pull_from => db)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def push_all!(opts = {})
|
390
|
+
opts[:push_to] ? db = opts[:push_to] : db = @database
|
391
|
+
each do |item|
|
392
|
+
item.push!(:push_to => db)
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
end
|
397
|
+
|
398
|
+
class Design < CouchDocument
|
399
|
+
# this is used to manage design documents
|
400
|
+
# In practise, the Controllers should be a list of classes corresponding to design documents
|
401
|
+
|
402
|
+
def initialize(name, opts = {})
|
403
|
+
merge! "_id" => "_design/#{name.to_s}"
|
404
|
+
@parser = ShyRubyJS::ShySexpParser.new
|
405
|
+
views = opts[:views] if opts[:views]
|
406
|
+
merge_views(views) if views
|
407
|
+
@database = opts[:push_to] if opts[:push_to]
|
408
|
+
merge! "kind" => self.class.to_s.split("::").last
|
409
|
+
end
|
410
|
+
|
411
|
+
def name
|
412
|
+
return self["_id"].split("_design/").drop(1).join
|
413
|
+
end
|
414
|
+
|
415
|
+
def add_view(view)
|
416
|
+
raise TypeError unless view.kind_of?(ShyCouch::Data::View)
|
417
|
+
@views << view
|
418
|
+
merge_views
|
419
|
+
end
|
420
|
+
|
421
|
+
def query_view(view, opts = {})
|
422
|
+
if opts[:from]
|
423
|
+
db = opts[:from]
|
424
|
+
else
|
425
|
+
db = @database
|
426
|
+
end
|
427
|
+
raise ShyCouchError, "No CouchDB defined" unless db
|
428
|
+
view = view.name if view.kind_of?(ShyCouch::Data::View)
|
429
|
+
#TODO - something
|
430
|
+
db.query_view(self.name, view, opts)
|
431
|
+
end
|
432
|
+
|
433
|
+
def view(view_name, &block)
|
434
|
+
add_view(ShyCouch::Data::View.new(view_name, &block))
|
435
|
+
end
|
436
|
+
|
437
|
+
def push!(opts = {})
|
438
|
+
opts[:push_to] ? db = opts[:push_to] : db = @database
|
439
|
+
raise ShyCouchError, "No CouchDB defined" unless db
|
440
|
+
db.add_design_and_push!(self)
|
441
|
+
end
|
442
|
+
|
443
|
+
private
|
444
|
+
|
445
|
+
def merge_views(views)
|
446
|
+
h = { "views" => {}}
|
447
|
+
views.each do |view|
|
448
|
+
h["views"][view.name] = view.functions
|
449
|
+
end
|
450
|
+
merge! h
|
451
|
+
end
|
452
|
+
|
453
|
+
end
|
252
454
|
|
253
455
|
end
|
254
456
|
|
255
|
-
end
|
457
|
+
end
|