paulcarey-relaxdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,106 @@
1
+ module RelaxDB
2
+
3
+ @@db = nil
4
+
5
+ class <<self
6
+
7
+ def configure(config)
8
+ @@db = CouchDB.new(config)
9
+ end
10
+
11
+ def db
12
+ @@db
13
+ end
14
+
15
+ # Creates the named database if it doesn't already exist
16
+ def use_db(name)
17
+ db.use_db(name)
18
+ end
19
+
20
+ def delete_db(name)
21
+ db.delete_db(name)
22
+ end
23
+
24
+ def list_dbs
25
+ db.list_dbs
26
+ end
27
+
28
+ def bulk_save(*objs)
29
+ docs = {}
30
+ objs.each { |o| docs[o._id] = o }
31
+
32
+ resp = db.post("_bulk_docs", { "docs" => objs }.to_json )
33
+ data = JSON.parse(resp.body)
34
+
35
+ data["new_revs"].each do |new_rev|
36
+ docs[ new_rev["id"] ]._rev = new_rev["rev"]
37
+ end
38
+
39
+ data["ok"]
40
+ end
41
+
42
+ def load(id)
43
+ resp = db.get("#{id}")
44
+ data = JSON.parse(resp.body)
45
+ create_object(data)
46
+ end
47
+
48
+ def retrieve(view_path, design_doc, view_name, map_function)
49
+ begin
50
+ resp = db.get(view_path)
51
+ rescue => e
52
+ DesignDocument.get(design_doc).add_map_view(view_name, map_function).save
53
+ resp = db.get(view_path)
54
+ end
55
+
56
+ data = JSON.parse(resp.body)
57
+ create_from_hash(data)
58
+ end
59
+
60
+ def view(design_doc, view_name, default_ret_val=[])
61
+ q = Query.new(design_doc, view_name)
62
+ yield q if block_given?
63
+
64
+ resp = db.get(q.view_path)
65
+ data = JSON.parse(resp.body)
66
+
67
+ # presence of total_rows tells us a map function was invoked
68
+ # if it's absent a map reduce invocation occured
69
+ if data["total_rows"]
70
+ create_from_hash(data)
71
+ else
72
+ obj = data["rows"][0] && data["rows"][0]["value"]
73
+ obj ? ViewObject.create(obj) : default_ret_val
74
+ end
75
+ end
76
+
77
+ def create_from_hash(data)
78
+ @objects = []
79
+ data["rows"].each do |row|
80
+ @objects << create_object(row["value"])
81
+ end
82
+ @objects
83
+ end
84
+
85
+ def create_object(data)
86
+ # revise use of string 'class' - it's a reserved word in JavaScript
87
+ klass = data.delete("class")
88
+ if klass
89
+ k = Module.const_get(klass)
90
+ k.new(data)
91
+ else
92
+ # data is not of a known class
93
+ ViewObject.create(data)
94
+ end
95
+ end
96
+
97
+ # Convenience methods - should be in a diffent module?
98
+
99
+ def pp_get(uri=nil)
100
+ resp = db.get(uri)
101
+ pp(JSON.parse(resp.body))
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,112 @@
1
+ module RelaxDB
2
+
3
+ class Server
4
+
5
+ def initialize(host, port)
6
+ @host = host
7
+ @port = port
8
+ end
9
+
10
+ def delete(uri)
11
+ request(Net::HTTP::Delete.new(uri))
12
+ end
13
+
14
+ def get(uri)
15
+ request(Net::HTTP::Get.new(uri))
16
+ end
17
+
18
+ def put(uri, json)
19
+ req = Net::HTTP::Put.new(uri)
20
+ req["content-type"] = "application/json"
21
+ req.body = json
22
+ request(req)
23
+ end
24
+
25
+ def post(uri, json)
26
+ req = Net::HTTP::Post.new(uri)
27
+ req["content-type"] = "application/json"
28
+ req.body = json
29
+ request(req)
30
+ end
31
+
32
+ def request(req)
33
+ res = Net::HTTP.start(@host, @port) {|http|
34
+ http.request(req)
35
+ }
36
+ if (not res.kind_of?(Net::HTTPSuccess))
37
+ handle_error(req, res)
38
+ end
39
+ res
40
+ end
41
+
42
+ def to_s
43
+ "http://#{@host}:#{@port}/"
44
+ end
45
+
46
+ private
47
+
48
+ def handle_error(req, res)
49
+ e = RuntimeError.new("#{res.code}:#{res.message}\nMETHOD:#{req.method}\nURI:#{req.path}\n#{res.body}")
50
+ raise e
51
+ end
52
+ end
53
+
54
+ class CouchDB
55
+
56
+ def initialize(config)
57
+ @server = RelaxDB::Server.new(config[:host], config[:port])
58
+ @logger = config[:logger] ? config[:logger] : Logger.new(Tempfile.new('couchdb.log'))
59
+ end
60
+
61
+ def use_db(name)
62
+ begin
63
+ @server.get("/#{name}")
64
+ rescue
65
+ @server.put("/#{name}", "")
66
+ end
67
+ @db = name
68
+ end
69
+
70
+ def delete_db(name)
71
+ @server.delete("/#{name}")
72
+ end
73
+
74
+ def list_dbs
75
+ JSON.parse(@server.get("/_all_dbs").body)
76
+ end
77
+
78
+ def delete(path=nil)
79
+ @logger.info("DELETE /#{@db}/#{unesc(path)}")
80
+ @server.delete("/#{@db}/#{path}")
81
+ end
82
+
83
+ def get(path=nil)
84
+ @logger.info("GET /#{@db}/#{unesc(path)}")
85
+ @server.get("/#{@db}/#{path}")
86
+ end
87
+
88
+ def post(path=nil, json=nil)
89
+ @logger.info("POST /#{@db}/#{unesc(path)} #{json}")
90
+ @server.post("/#{@db}/#{path}", json)
91
+ end
92
+
93
+ def put(path=nil, json=nil)
94
+ @logger.info("PUT /#{@db}/#{unesc(path)} #{json}")
95
+ @server.put("/#{@db}/#{path}", json)
96
+ end
97
+
98
+ def unesc(path)
99
+ path ? ::CGI::unescape(path) : ""
100
+ end
101
+
102
+ def uri
103
+ "#@server" / @db
104
+ end
105
+
106
+ def name
107
+ @db
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -0,0 +1,42 @@
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 view_name
13
+ name = "all_sorted_by"
14
+
15
+ @atts.each do |att|
16
+ name += "_#{att}_and"
17
+ end
18
+ name[0, name.size-4]
19
+ end
20
+
21
+ def map_function
22
+ # To guard against non existing attributes in older documents, an OR with an object literal
23
+ # is inserted for each emitted key
24
+ # The object literal is the lowest sorting JSON category
25
+
26
+ # Create the key from the attributes, wrapping it in [] if the key is composite
27
+ raw = @atts.inject("") { |m,v| m << "(doc.#{v}||{}), " }
28
+ refined = raw[0, raw.size-2]
29
+ pure = @atts.size > 1 ? refined.sub(/^/, "[").sub(/$/, "]") : refined
30
+
31
+ <<-QUERY
32
+ function(doc) {
33
+ if(doc.class == "#{@class_name}") {
34
+ emit(#{pure}, doc);
35
+ }
36
+ }
37
+ QUERY
38
+ end
39
+
40
+ end
41
+
42
+ 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
@@ -0,0 +1,34 @@
1
+ module RelaxDB
2
+
3
+ # An immuntable object typically used to display the results of a view
4
+ class ViewObject
5
+
6
+ def initialize(hash)
7
+ hash.each do |k, v|
8
+
9
+ if k.to_s =~ /_at$/
10
+ v = Time.local(*ParseDate.parsedate(v)) rescue v
11
+ end
12
+
13
+ instance_variable_set("@#{k}", v)
14
+ meta_class.instance_eval do
15
+ define_method(k.to_sym) do
16
+ instance_variable_get("@#{k}".to_sym)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.create(obj)
23
+ if obj.instance_of? Array
24
+ obj.inject([]) { |arr, o| arr << ViewObject.new(o) }
25
+ elsif obj.instance_of? Hash
26
+ ViewObject.new(obj)
27
+ else
28
+ obj
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,47 @@
1
+ module RelaxDB
2
+
3
+ class ViewUploader
4
+
5
+ class << self
6
+
7
+ # Methods must start and finish on different lines
8
+ # The function declaration must start at the beginning of a line
9
+ # As '-' is used as a delimiter, neither design doc nor view name may contain '-'
10
+ # Exepcted function declaration form is
11
+ # function DesignDoc-funcname-functype(doc) {
12
+ # For example
13
+ # function Users-followers-map(doc) {
14
+ #
15
+ def upload(filename)
16
+ lines = File.readlines(filename)
17
+ extract(lines) do |dd, vn, t, f|
18
+ RelaxDB::DesignDocument.get(dd).add_view(vn, t, f).save
19
+ end
20
+ end
21
+
22
+ def extract(lines)
23
+ # Index of function declaration matches
24
+ m = []
25
+
26
+ 0.upto(lines.size-1) do |p|
27
+ line = lines[p]
28
+ m << p if line =~ /^function[^\{]+\{/
29
+ end
30
+ # Add one beyond the last line number as the final terminator
31
+ m << lines.size
32
+
33
+ 0.upto(m.size-2) do |i|
34
+ declr = lines[m[i]]
35
+ declr =~ /(\w)+-(\w)+-(\w)+/
36
+ declr.sub!($&, '')
37
+ design_doc, view_name, type = $&.split('-')
38
+ func = lines[m[i]...m[i+1]].join
39
+ yield design_doc, view_name, type, func
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,42 @@
1
+ module RelaxDB
2
+
3
+ class ViewCreator
4
+
5
+ def self.all(target_class)
6
+ template = <<-QUERY
7
+ function(doc) {
8
+ if(doc.class == "${target_class}")
9
+ emit(null, doc);
10
+ }
11
+ QUERY
12
+ template.sub!("${target_class}", target_class.to_s)
13
+ end
14
+
15
+ def self.has_n(target_class, relationship_to_client)
16
+ template = <<-MAP_FUNC
17
+ function(doc) {
18
+ if(doc.class == "${target_class}")
19
+ emit(doc.${relationship_to_client}_id, doc);
20
+ }
21
+ MAP_FUNC
22
+ template.sub!("${target_class}", target_class)
23
+ template.sub("${relationship_to_client}", relationship_to_client)
24
+ end
25
+
26
+ def self.has_many_through(target_class, peers)
27
+ template = <<-MAP_FUNC
28
+ function(doc) {
29
+ if(doc.class == "${target_class}" && doc.${peers}) {
30
+ var i;
31
+ for(i = 0; i < doc.${peers}.length; i++) {
32
+ emit(doc.${peers}[i], doc);
33
+ }
34
+ }
35
+ }
36
+ MAP_FUNC
37
+ template.sub!("${target_class}", target_class).gsub!("${peers}", peers)
38
+ end
39
+
40
+ end
41
+
42
+ end
data/lib/relaxdb.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'rubygems'
2
+ require 'extlib'
3
+ require 'json'
4
+ require 'uuid'
5
+
6
+ require 'cgi'
7
+ require 'net/http'
8
+ require 'logger'
9
+ require 'parsedate'
10
+ require 'pp'
11
+ require 'tempfile'
12
+
13
+ $:.unshift(File.dirname(__FILE__)) unless
14
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
15
+
16
+ require 'relaxdb/all_delegator'
17
+ require 'relaxdb/belongs_to_proxy'
18
+ require 'relaxdb/design_doc'
19
+ require 'relaxdb/document'
20
+ require 'relaxdb/has_many_proxy'
21
+ require 'relaxdb/has_one_proxy'
22
+ require 'relaxdb/query'
23
+ require 'relaxdb/references_many_proxy'
24
+ require 'relaxdb/relaxdb'
25
+ require 'relaxdb/server'
26
+ require 'relaxdb/sorted_by_view'
27
+ require 'relaxdb/uuid_generator'
28
+ require 'relaxdb/view_object'
29
+ require 'relaxdb/view_uploader'
30
+ require 'relaxdb/views'
31
+
32
+ module RelaxDB
33
+ end
@@ -0,0 +1,80 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+ require File.dirname(__FILE__) + '/spec_models.rb'
3
+
4
+ describe RelaxDB::BelongsToProxy do
5
+
6
+ before(:all) do
7
+ RelaxDB.configure(:host => "localhost", :port => 5984)
8
+ end
9
+
10
+ before(:each) do
11
+ RelaxDB.delete_db "relaxdb_spec_db" rescue "ok"
12
+ RelaxDB.use_db "relaxdb_spec_db"
13
+ end
14
+
15
+ describe "belongs_to" do
16
+
17
+ it "should return nil when accessed before assignment" do
18
+ r = Rating.new
19
+ r.photo.should == nil
20
+ end
21
+
22
+ it "should be establishable via constructor attribute" do
23
+ p = Photo.new
24
+ r = Rating.new :photo => p
25
+ r.photo.should == p
26
+ end
27
+
28
+ it "should be establishable via constructor id" do
29
+ p = Photo.new.save
30
+ r = Rating.new(:photo_id => p._id).save
31
+ r.photo.should == p
32
+ end
33
+
34
+ it "should establish the parent relationship when supplied a parent and saved" do
35
+ p = Photo.new.save
36
+ r = Rating.new
37
+ r.photo = p
38
+ # I'm not saying the following is correct or desired - merely codifying how things stand
39
+ p.rating.should be_nil
40
+ r.save
41
+ p.rating.should == r
42
+ end
43
+
44
+ it "should establish the parent relationship when supplied a parent id and saved" do
45
+ p = Photo.new.save
46
+ r = Rating.new(:photo_id => p._id).save
47
+ p.rating.should == r
48
+ end
49
+
50
+ it "should return the same object on repeated invocations" do
51
+ p = Photo.new.save
52
+ r = Rating.new(:photo => p).save
53
+ r = RelaxDB.load(r._id)
54
+ r.photo.object_id.should == r.photo.object_id
55
+ end
56
+
57
+ it "should be nullified when the parent is destroyed" do
58
+ r = Rating.new
59
+ p = Photo.new(:rating => r).save
60
+ p.destroy!
61
+ RelaxDB.load(r._id).photo.should be_nil
62
+ end
63
+
64
+ it "should be preserved across save / load boundary" do
65
+ r = Rating.new
66
+ p = Photo.new(:rating => r).save
67
+ r = RelaxDB.load r._id
68
+ r.photo.should == p
69
+ end
70
+
71
+ it "should be able to reference itself via its parent" do
72
+ r = Rating.new
73
+ p = Photo.new(:rating => r).save
74
+ r = RelaxDB.load r._id
75
+ r.photo.rating.should == r
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+ require File.dirname(__FILE__) + '/spec_models.rb'
3
+
4
+ describe RelaxDB::DesignDocument do
5
+
6
+ before(:all) do
7
+ RelaxDB.configure(:host => "localhost", :port => 5984)
8
+ end
9
+
10
+ before(:each) do
11
+ RelaxDB.delete_db "relaxdb_spec_db" rescue "ok"
12
+ RelaxDB.use_db "relaxdb_spec_db"
13
+ end
14
+
15
+ describe "#save" do
16
+
17
+ it "should create a corresponding document in CouchDB" do
18
+ RelaxDB::DesignDocument.get("foo").save
19
+ RelaxDB.load("_design/foo").should_not be_nil
20
+ end
21
+
22
+ end
23
+
24
+ describe "#destroy" do
25
+
26
+ it "should delete the corresponding document from CouchDB" do
27
+ dd = RelaxDB::DesignDocument.get("foo").save
28
+ dd.destroy!
29
+ lambda { RelaxDB.load("_design/foo") }.should raise_error
30
+ end
31
+
32
+ end
33
+
34
+ end