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.
- data/LICENSE +20 -0
- data/README.textile +98 -0
- data/Rakefile +52 -0
- data/docs/spec_results.html +602 -0
- data/lib/relaxdb/all_delegator.rb +48 -0
- data/lib/relaxdb/belongs_to_proxy.rb +29 -0
- data/lib/relaxdb/design_doc.rb +50 -0
- data/lib/relaxdb/document.rb +298 -0
- data/lib/relaxdb/has_many_proxy.rb +81 -0
- data/lib/relaxdb/has_one_proxy.rb +45 -0
- data/lib/relaxdb/query.rb +48 -0
- data/lib/relaxdb/references_many_proxy.rb +99 -0
- data/lib/relaxdb/relaxdb.rb +106 -0
- data/lib/relaxdb/server.rb +112 -0
- data/lib/relaxdb/sorted_by_view.rb +42 -0
- data/lib/relaxdb/uuid_generator.rb +21 -0
- data/lib/relaxdb/view_object.rb +34 -0
- data/lib/relaxdb/view_uploader.rb +47 -0
- data/lib/relaxdb/views.rb +42 -0
- data/lib/relaxdb.rb +33 -0
- data/spec/belongs_to_spec.rb +80 -0
- data/spec/design_doc_spec.rb +34 -0
- data/spec/document_spec.rb +301 -0
- data/spec/has_many_spec.rb +139 -0
- data/spec/has_one_spec.rb +121 -0
- data/spec/query_spec.rb +46 -0
- data/spec/references_many_spec.rb +141 -0
- data/spec/relaxdb_spec.rb +50 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/spec_models.rb +104 -0
- data/spec/view_object_spec.rb +47 -0
- metadata +111 -0
@@ -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
|