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.
@@ -2,254 +2,456 @@ module ShyCouch
2
2
 
3
3
  module Data
4
4
 
5
- class CouchDocument < Hash
6
- class << self
7
- # allows instance.class.requirements to be called
8
- end
9
- @@needs, @@suggests, @@views = [], [], []
10
-
11
- def initialize(opts={})
12
- # Assumes that the "kind" is the class name unless explicitly stated otherwise
13
- # TODO - maybe just force it to be the class name no matter what tbh
14
-
15
- # this is messy.
16
- # If there's some data given, see whether it has a kind.
17
- if opts[:data]
18
- if !opts[:data]["kind"]
19
- # If there's no kind, give the 'kind' the class name as a value
20
- opts[:data]["kind"] = self.class.to_s.split("::").last
21
- end
22
- else
23
- # if there's no data, give it data and a kind
24
- opts[:data] = {"kind" => self.class.to_s.split("::").last}
25
- end
26
-
27
- merge!(opts[:data])
28
- @database = opts[:push_to] if opts[:push_to]
29
- raise TypeError unless valid?
30
- set_up_views
31
- end
32
-
33
- def self.all
34
-
35
- end
36
-
37
- def self.needs(*requirements)
38
- requirements.map { |requirement| @@needs << requirement } unless requirements.empty?
39
- return @@needs
40
- end
41
-
42
- def self.suggests(*suggestions)
43
- suggestions.map { |suggestion| @@suggests << suggestion } unless suggestions.empty?
44
- return @@suggests
45
- end
46
-
47
- def needs;self.class.needs; end
48
- def suggests; self.class.suggests; end
49
-
50
- def needs?(requirement)
51
- @@needs.include?(requirement) ? true : false
52
- end
53
-
54
- def suggests?(requirement)
55
- @@suggests.include?(requirement) ? true : false
56
- end
57
-
58
- def attr_keys
59
- # returns the keys for all the attrs that aren't the id or rev
60
- attr_keys = []
61
- self.map { |k,v|
62
- attr_keys << k unless k == "_id" or k == "_rev"
63
- }
64
- return attr_keys
65
- end
66
-
67
- def _requirements
68
- #TODO - hm
69
- return self.class.requirements
70
- end
71
-
72
- def pull(db = nil)
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
- def pull_all(db = nil)
178
- db ||= @database
179
- collection = CouchDocumentCollection.new
180
- each do |item|
181
- collection << db.pull_document(item)
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
- return collection
184
- end
185
-
186
- def pull_all!(db = nil)
187
- db ||= @database
188
- each do |item|
189
- item.pull!(db)
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
- end
192
-
193
- def push_all!(db = nil)
194
- db ||= @database
195
- each do |item|
196
- item.push!(db)
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
- end
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
- end
201
-
202
- class Design < CouchDocument
203
- # this is used to manage design documents
204
- # In practise, the Controllers should be a list of classes corresponding to design documents
205
-
206
- def initialize(name, opts = {})
207
- merge! "_id" => "_design/#{name.to_s}"
208
- @parser = ShyRubyJS::ShySexpParser.new
209
- views = opts[:views] if opts[:views]
210
- merge_views(views) if views
211
- @database = opts[:push_to] if opts[:push_to]
212
- end
213
-
214
- def name
215
- return self["_id"].split("_design/").drop(1).join
216
- end
217
-
218
- def add_view(view)
219
- raise TypeError unless view.kind_of?(ShyCouch::Data::View)
220
- @views << view
221
- merge_views
222
- end
223
-
224
- def query_view(view, db = nil)
225
- db ||= @database
226
- raise ShyCouchError, "No CouchDB defined" unless db
227
- view = view.name if view.kind_of?(ShyCouch::Data::View)
228
- #TODO - something
229
- db.query_view(self.name, view)
230
- end
231
-
232
- def view(view_name, &block)
233
- add_view(ShyCouch::Data::View.new(view_name, &block))
234
- end
235
- def push!(db = nil)
236
- db ||= @database
237
- raise ShyCouchError, "No CouchDB defined" unless db
238
- db.add_design_and_push!(self)
239
- end
240
-
241
- private
242
-
243
- def merge_views(views)
244
- h = { "views" => {}}
245
- views.each do |view|
246
- h["views"][view.name] = view.functions
247
- end
248
- merge! h
249
- end
250
-
251
- end
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