ceritium-relaxdb 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/LICENSE +20 -0
  2. data/README.textile +185 -0
  3. data/Rakefile +58 -0
  4. data/docs/spec_results.html +604 -0
  5. data/lib/more/atomic_bulk_save_support.rb +18 -0
  6. data/lib/more/grapher.rb +48 -0
  7. data/lib/relaxdb.rb +40 -0
  8. data/lib/relaxdb/all_delegator.rb +68 -0
  9. data/lib/relaxdb/belongs_to_proxy.rb +29 -0
  10. data/lib/relaxdb/design_doc.rb +55 -0
  11. data/lib/relaxdb/document.rb +531 -0
  12. data/lib/relaxdb/extlib.rb +15 -0
  13. data/lib/relaxdb/has_many_proxy.rb +104 -0
  14. data/lib/relaxdb/has_one_proxy.rb +45 -0
  15. data/lib/relaxdb/paginate_params.rb +54 -0
  16. data/lib/relaxdb/paginator.rb +78 -0
  17. data/lib/relaxdb/query.rb +75 -0
  18. data/lib/relaxdb/references_many_proxy.rb +101 -0
  19. data/lib/relaxdb/relaxdb.rb +208 -0
  20. data/lib/relaxdb/server.rb +156 -0
  21. data/lib/relaxdb/sorted_by_view.rb +65 -0
  22. data/lib/relaxdb/uuid_generator.rb +21 -0
  23. data/lib/relaxdb/validators.rb +11 -0
  24. data/lib/relaxdb/view_object.rb +34 -0
  25. data/lib/relaxdb/view_result.rb +18 -0
  26. data/lib/relaxdb/view_uploader.rb +47 -0
  27. data/lib/relaxdb/views.rb +54 -0
  28. data/spec/belongs_to_spec.rb +129 -0
  29. data/spec/callbacks_spec.rb +80 -0
  30. data/spec/derived_properties_spec.rb +117 -0
  31. data/spec/design_doc_spec.rb +34 -0
  32. data/spec/document_spec.rb +556 -0
  33. data/spec/has_many_spec.rb +176 -0
  34. data/spec/has_one_spec.rb +128 -0
  35. data/spec/query_spec.rb +80 -0
  36. data/spec/references_many_spec.rb +178 -0
  37. data/spec/relaxdb_spec.rb +226 -0
  38. data/spec/spec.opts +1 -0
  39. data/spec/spec_helper.rb +10 -0
  40. data/spec/spec_models.rb +151 -0
  41. data/spec/view_object_spec.rb +47 -0
  42. metadata +123 -0
@@ -0,0 +1,208 @@
1
+ module RelaxDB
2
+
3
+ class NotFound < StandardError; end
4
+ class DocumentNotSaved < StandardError; end
5
+ class UpdateConflict < DocumentNotSaved; end
6
+ class ValidationFailure < DocumentNotSaved; end
7
+
8
+ @@db = nil
9
+
10
+ class <<self
11
+
12
+ def configure(config)
13
+ @@db = CouchDB.new(config)
14
+ end
15
+
16
+ def db
17
+ @@db
18
+ end
19
+
20
+ def logger
21
+ @@db.logger
22
+ end
23
+
24
+ # Creates the named database if it doesn't already exist
25
+ def use_db(name)
26
+ db.use_db name
27
+ end
28
+
29
+ def db_exists?(name)
30
+ db.db_exists? name
31
+ end
32
+
33
+ def delete_db(name)
34
+ db.delete_db name
35
+ end
36
+
37
+ def list_dbs
38
+ db.list_dbs
39
+ end
40
+
41
+ def replicate_db(source, target)
42
+ db.replicate_db source, target
43
+ end
44
+
45
+ def bulk_save!(*objs)
46
+ pre_save_success = objs.inject(true) { |s, o| s &= o.pre_save }
47
+ raise ValidationFailure, objs unless pre_save_success
48
+
49
+ docs = {}
50
+ objs.each { |o| docs[o._id] = o }
51
+
52
+ begin
53
+ resp = db.post("_bulk_docs", { "docs" => objs }.to_json )
54
+ data = JSON.parse(resp.body)
55
+
56
+ data["new_revs"].each do |new_rev|
57
+ obj = docs[ new_rev["id"] ]
58
+ obj._rev = new_rev["rev"]
59
+ obj.post_save
60
+ end
61
+ rescue HTTP_409
62
+ raise UpdateConflict, objs
63
+ end
64
+
65
+ objs
66
+ end
67
+
68
+ def bulk_save(*objs)
69
+ begin
70
+ bulk_save!(*objs)
71
+ rescue ValidationFailure, UpdateConflict
72
+ false
73
+ end
74
+ end
75
+
76
+ def reload(obj)
77
+ load(obj._id)
78
+ end
79
+
80
+ def load(ids)
81
+ # RelaxDB.logger.debug(caller.inject("#{db.name}/#{ids}\n") { |a, i| a += "#{i}\n" })
82
+
83
+ if ids.is_a? Array
84
+ resp = db.post("_all_docs?include_docs=true", {:keys => ids}.to_json)
85
+ data = JSON.parse(resp.body)
86
+ data["rows"].map { |row| row["doc"] ? create_object(row["doc"]) : nil }
87
+ else
88
+ begin
89
+ resp = db.get(ids)
90
+ data = JSON.parse(resp.body)
91
+ create_object(data)
92
+ rescue HTTP_404
93
+ nil
94
+ end
95
+ end
96
+ end
97
+
98
+ def load!(ids)
99
+ res = load(ids)
100
+
101
+ raise NotFound, ids if res == nil
102
+ raise NotFound, ids if res.respond_to?(:include?) && res.include?(nil)
103
+
104
+ res
105
+ end
106
+
107
+ # Used internally by RelaxDB
108
+ def retrieve(view_path, design_doc=nil, view_name=nil, map_func=nil, reduce_func=nil)
109
+ begin
110
+ resp = db.get(view_path)
111
+ rescue => e
112
+ dd = DesignDocument.get(design_doc).add_map_view(view_name, map_func)
113
+ dd.add_reduce_view(view_name, reduce_func) if reduce_func
114
+ dd.save
115
+ resp = db.get(view_path)
116
+ end
117
+
118
+ data = JSON.parse(resp.body)
119
+ ViewResult.new(data)
120
+ end
121
+
122
+ # Requests the given view from CouchDB and returns a hash.
123
+ # This method should typically be wrapped in one of merge, instantiate, or reduce_result.
124
+ def view(design_doc, view_name)
125
+ q = Query.new(design_doc, view_name)
126
+ yield q if block_given?
127
+
128
+ resp = q.keys ? db.post(q.view_path, q.keys) : db.get(q.view_path)
129
+ JSON.parse(resp.body)
130
+ end
131
+
132
+ # Should be invoked on the result of a join view
133
+ # Merges all rows based on merge_key and returns an array of ViewOject
134
+ def merge(data, merge_key)
135
+ merged = {}
136
+ data["rows"].each do |row|
137
+ value = row["value"]
138
+ merged[value[merge_key]] ||= {}
139
+ merged[value[merge_key]].merge!(value)
140
+ end
141
+
142
+ merged.values.map { |v| ViewObject.create(v) }
143
+ end
144
+
145
+ # Creates RelaxDB::Document objects from the result
146
+ def instantiate(data)
147
+ create_from_hash(data)
148
+ end
149
+
150
+ # Returns a scalar, an object, or an Array of objects
151
+ def reduce_result(data)
152
+ obj = data["rows"][0] && data["rows"][0]["value"]
153
+ ViewObject.create(obj)
154
+ end
155
+
156
+ def paginate_view(page_params, design_doc, view_name, *view_keys)
157
+ paginate_params = PaginateParams.new
158
+ yield paginate_params
159
+ raise paginate_params.error_msg if paginate_params.invalid?
160
+
161
+ paginator = Paginator.new(paginate_params, page_params)
162
+
163
+ query = Query.new(design_doc, view_name)
164
+ query.merge(paginate_params)
165
+
166
+ docs = ViewResult.new(JSON.parse(db.get(query.view_path).body))
167
+ docs.reverse! if paginate_params.order_inverted?
168
+
169
+ paginator.add_next_and_prev(docs, design_doc, view_name, view_keys)
170
+
171
+ docs
172
+ end
173
+
174
+ def create_from_hash(data)
175
+ data["rows"].map { |row| create_object(row["value"]) }
176
+ end
177
+
178
+ def create_object(data)
179
+ # revise use of string 'class' - it's a reserved word in JavaScript
180
+ klass = data ? data.delete("class") : nil
181
+ if klass
182
+ k = Module.const_get(klass)
183
+ k.new(data)
184
+ else
185
+ # data is not of a known class
186
+ ViewObject.create(data)
187
+ end
188
+ end
189
+
190
+ # Convenience methods - should be in a diffent module?
191
+
192
+ def get(uri=nil)
193
+ JSON.parse(db.get(uri).body)
194
+ end
195
+
196
+ def pp_get(uri=nil)
197
+ resp = db.get(uri)
198
+ pp(JSON.parse(resp.body))
199
+ end
200
+
201
+ def pp_post(uri=nil, json=nil)
202
+ resp = db.post(uri, json)
203
+ pp(JSON.parse(resp.body))
204
+ end
205
+
206
+ end
207
+
208
+ end
@@ -0,0 +1,156 @@
1
+ module RelaxDB
2
+
3
+ class HTTP_404 < StandardError; end
4
+ class HTTP_409 < StandardError; end
5
+ class HTTP_412 < StandardError; end
6
+
7
+ class Server
8
+
9
+ def initialize(host, port)
10
+ @host = host
11
+ @port = port
12
+ end
13
+
14
+ def delete(uri)
15
+ request(Net::HTTP::Delete.new(uri))
16
+ end
17
+
18
+ def get(uri)
19
+ request(Net::HTTP::Get.new(uri))
20
+ end
21
+
22
+ def put(uri, json)
23
+ req = Net::HTTP::Put.new(uri)
24
+ req["content-type"] = "application/json"
25
+ req.body = json
26
+ request(req)
27
+ end
28
+
29
+ def post(uri, json)
30
+ req = Net::HTTP::Post.new(uri)
31
+ req["content-type"] = "application/json"
32
+ req.body = json
33
+ request(req)
34
+ end
35
+
36
+ def request(req)
37
+ res = Net::HTTP.start(@host, @port) {|http|
38
+ http.request(req)
39
+ }
40
+ if (not res.kind_of?(Net::HTTPSuccess))
41
+ handle_error(req, res)
42
+ end
43
+ res
44
+ end
45
+
46
+ def to_s
47
+ "http://#{@host}:#{@port}/"
48
+ end
49
+
50
+ private
51
+
52
+ def handle_error(req, res)
53
+ msg = "#{res.code}:#{res.message}\nMETHOD:#{req.method}\nURI:#{req.path}\n#{res.body}"
54
+ begin
55
+ klass = RelaxDB.const_get("HTTP_#{res.code}")
56
+ e = klass.new(msg)
57
+ rescue
58
+ e = RuntimeError.new(msg)
59
+ end
60
+
61
+ raise e
62
+ end
63
+ end
64
+
65
+ class CouchDB
66
+
67
+ attr_reader :logger
68
+
69
+ # Used for test instrumentation only i.e. to assert that
70
+ # an expected number of requests have been issued
71
+ attr_accessor :get_count, :put_count
72
+
73
+ def initialize(config)
74
+ @get_count, @put_count = 0, 0
75
+ @server = RelaxDB::Server.new(config[:host], config[:port])
76
+ @logger = config[:logger] ? config[:logger] : Logger.new(Tempfile.new('couchdb.log'))
77
+ end
78
+
79
+ def use_db(name)
80
+ create_db_if_non_existant(name)
81
+ @db = name
82
+ end
83
+
84
+ def db_exists?(name)
85
+ @server.get("/#{name}") rescue false
86
+ end
87
+
88
+ def delete_db(name)
89
+ @logger.info("Deleting database #{name}")
90
+ @server.delete("/#{name}")
91
+ end
92
+
93
+ def list_dbs
94
+ JSON.parse(@server.get("/_all_dbs").body)
95
+ end
96
+
97
+ def replicate_db(source, target)
98
+ @logger.info("Replicating from #{source} to #{target}")
99
+ create_db_if_non_existant target
100
+ data = { "source" => source, "target" => target}
101
+ @server.post("/_replicate", data.to_json)
102
+ end
103
+
104
+ def delete(path=nil)
105
+ @logger.info("DELETE /#{@db}/#{unesc(path)}")
106
+ @server.delete("/#{@db}/#{path}")
107
+ end
108
+
109
+ # *ignored allows methods to invoke get or post indifferently via send
110
+ def get(path=nil, *ignored)
111
+ @get_count += 1
112
+ @logger.info("GET /#{@db}/#{unesc(path)}")
113
+ @server.get("/#{@db}/#{path}")
114
+ end
115
+
116
+ def post(path=nil, json=nil)
117
+ @logger.info("POST /#{@db}/#{unesc(path)} #{json}")
118
+ @server.post("/#{@db}/#{path}", json)
119
+ end
120
+
121
+ def put(path=nil, json=nil)
122
+ @put_count += 1
123
+ @logger.info("PUT /#{@db}/#{unesc(path)} #{json}")
124
+ @server.put("/#{@db}/#{path}", json)
125
+ end
126
+
127
+ def unesc(path)
128
+ # path
129
+ path ? ::CGI::unescape(path) : ""
130
+ end
131
+
132
+ def uri
133
+ "#@server" / @db
134
+ end
135
+
136
+ def name
137
+ @db
138
+ end
139
+
140
+ def name=(name)
141
+ @db = name
142
+ end
143
+
144
+ private
145
+
146
+ def create_db_if_non_existant(name)
147
+ begin
148
+ @server.get("/#{name}")
149
+ rescue
150
+ @server.put("/#{name}", "")
151
+ end
152
+ end
153
+
154
+ end
155
+
156
+ end
@@ -0,0 +1,65 @@
1
+ module RelaxDB
2
+
3
+ # Represents a CouchDB view, which is implicitly sorted by key
4
+ # The view name is determined by sort attributes
5
+ class SortedByView
6
+
7
+ def initialize(class_name, *atts)
8
+ @class_name = class_name
9
+ @atts = atts
10
+ end
11
+
12
+ def map_function
13
+ key = @atts.map { |a| "doc.#{a}" }.join(", ")
14
+ key = @atts.size > 1 ? key.sub(/^/, "[").sub(/$/, "]") : key
15
+
16
+ <<-QUERY
17
+ function(doc) {
18
+ if(doc.class == "#{@class_name}") {
19
+ emit(#{key}, doc);
20
+ }
21
+ }
22
+ QUERY
23
+ end
24
+
25
+ def reduce_function
26
+ <<-QUERY
27
+ function(keys, values, rereduce) {
28
+ if (rereduce) {
29
+ return sum(values);
30
+ } else {
31
+ return values.length;
32
+ }
33
+ }
34
+ QUERY
35
+ end
36
+
37
+ def view_name
38
+ "all_sorted_by_" << @atts.join("_and_")
39
+ end
40
+
41
+ def query(query)
42
+ # If a view contains both a map and reduce function, CouchDB will by default return
43
+ # the result of the reduce function when queried.
44
+ # This class automatically creates both map and reduce functions so it can be used by the paginator.
45
+ # In normal usage, this class will be used with map functions, hence reduce is explicitly set
46
+ # to false if it hasn't already been set.
47
+ query.reduce(false) if query.reduce.nil?
48
+
49
+ method = query.keys ? :post : :get
50
+
51
+ begin
52
+ resp = RelaxDB.db.send(method, query.view_path, query.keys)
53
+ rescue => e
54
+ design_doc = DesignDocument.get(@class_name)
55
+ design_doc.add_map_view(view_name, map_function).add_reduce_view(view_name, reduce_function).save
56
+ resp = RelaxDB.db.send(method, query.view_path, query.keys)
57
+ end
58
+
59
+ data = JSON.parse(resp.body)
60
+ ViewResult.new(data)
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -0,0 +1,21 @@
1
+ module RelaxDB
2
+
3
+ class UuidGenerator
4
+
5
+ def self.uuid
6
+ unless @length
7
+ @uuid ||= UUID.new
8
+ @uuid.generate
9
+ else
10
+ rand.to_s[2, @length]
11
+ end
12
+ end
13
+
14
+ # Convenience that helps relationship debuggging and model exploration
15
+ def self.id_length=(length)
16
+ @length = length
17
+ end
18
+
19
+ end
20
+
21
+ end