paulcarey-relaxdb 0.1.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.
@@ -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