cohitre-relaxdb 0.2.2
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 +164 -0
- data/Rakefile +52 -0
- data/docs/spec_results.html +604 -0
- data/lib/more/grapher.rb +48 -0
- data/lib/relaxdb.rb +38 -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 +386 -0
- data/lib/relaxdb/extlib.rb +3 -0
- data/lib/relaxdb/has_many_proxy.rb +81 -0
- data/lib/relaxdb/has_one_proxy.rb +45 -0
- data/lib/relaxdb/paginate_params.rb +54 -0
- data/lib/relaxdb/paginator.rb +78 -0
- data/lib/relaxdb/query.rb +74 -0
- data/lib/relaxdb/references_many_proxy.rb +99 -0
- data/lib/relaxdb/relaxdb.rb +157 -0
- data/lib/relaxdb/server.rb +132 -0
- data/lib/relaxdb/sorted_by_view.rb +62 -0
- data/lib/relaxdb/uuid_generator.rb +21 -0
- data/lib/relaxdb/view_object.rb +34 -0
- data/lib/relaxdb/view_result.rb +18 -0
- data/lib/relaxdb/view_uploader.rb +47 -0
- data/lib/relaxdb/views.rb +42 -0
- data/spec/belongs_to_spec.rb +80 -0
- data/spec/callbacks_spec.rb +64 -0
- data/spec/denormalisation_spec.rb +49 -0
- data/spec/design_doc_spec.rb +34 -0
- data/spec/document_spec.rb +364 -0
- data/spec/has_many_spec.rb +147 -0
- data/spec/has_one_spec.rb +128 -0
- data/spec/query_spec.rb +80 -0
- data/spec/references_many_spec.rb +141 -0
- data/spec/relaxdb_spec.rb +137 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/spec_models.rb +130 -0
- data/spec/view_object_spec.rb +47 -0
- metadata +119 -0
@@ -0,0 +1,132 @@
|
|
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
|
+
create_db_if_non_existant(name)
|
63
|
+
@db = name
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete_db(name)
|
67
|
+
@logger.info("Deleting database #{name}")
|
68
|
+
@server.delete("/#{name}")
|
69
|
+
end
|
70
|
+
|
71
|
+
def list_dbs
|
72
|
+
JSON.parse(@server.get("/_all_dbs").body)
|
73
|
+
end
|
74
|
+
|
75
|
+
def replicate_db(source, target)
|
76
|
+
@logger.info("Replicating from #{source} to #{target}")
|
77
|
+
create_db_if_non_existant target
|
78
|
+
data = { "source" => source, "target" => target}
|
79
|
+
@server.post("/_replicate", data.to_json)
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete(path=nil)
|
83
|
+
@logger.info("DELETE /#{@db}/#{unesc(path)}")
|
84
|
+
@server.delete("/#{@db}/#{path}")
|
85
|
+
end
|
86
|
+
|
87
|
+
# *ignored allows methods to invoke get or post indifferently
|
88
|
+
def get(path=nil, *ignored)
|
89
|
+
@logger.info("GET /#{@db}/#{unesc(path)}")
|
90
|
+
@server.get("/#{@db}/#{path}")
|
91
|
+
end
|
92
|
+
|
93
|
+
def post(path=nil, json=nil)
|
94
|
+
@logger.info("POST /#{@db}/#{unesc(path)} #{json}")
|
95
|
+
@server.post("/#{@db}/#{path}", json)
|
96
|
+
end
|
97
|
+
|
98
|
+
def put(path=nil, json=nil)
|
99
|
+
@logger.info("PUT /#{@db}/#{unesc(path)} #{json}")
|
100
|
+
@server.put("/#{@db}/#{path}", json)
|
101
|
+
end
|
102
|
+
|
103
|
+
def unesc(path)
|
104
|
+
# path
|
105
|
+
path ? ::CGI::unescape(path) : ""
|
106
|
+
end
|
107
|
+
|
108
|
+
def uri
|
109
|
+
"#@server" / @db
|
110
|
+
end
|
111
|
+
|
112
|
+
def name
|
113
|
+
@db
|
114
|
+
end
|
115
|
+
|
116
|
+
def logger
|
117
|
+
@logger
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def create_db_if_non_existant(name)
|
123
|
+
begin
|
124
|
+
@server.get("/#{name}")
|
125
|
+
rescue
|
126
|
+
@server.put("/#{name}", "")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
@@ -0,0 +1,62 @@
|
|
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
|
+
return values.length;
|
29
|
+
}
|
30
|
+
QUERY
|
31
|
+
end
|
32
|
+
|
33
|
+
def view_name
|
34
|
+
"all_sorted_by_" << @atts.join("_and_")
|
35
|
+
end
|
36
|
+
|
37
|
+
def query(query)
|
38
|
+
# If a view contains both a map and reduce function, CouchDB will by default return
|
39
|
+
# the result of the reduce function when queried.
|
40
|
+
# This class automatically creates both map and reduce functions so it can be used by the paginator.
|
41
|
+
# In normal usage, this class will be used with map functions, hence reduce is explicitly set
|
42
|
+
# to false if it hasn't already been set.
|
43
|
+
query.reduce(false) if query.reduce.nil?
|
44
|
+
|
45
|
+
# I hope the query.group(true) should be temporary only (given that reduce has been set to false)
|
46
|
+
method = query.keys ? (query.group(true) && :post) : :get
|
47
|
+
|
48
|
+
begin
|
49
|
+
resp = RelaxDB.db.send(method, query.view_path, query.keys)
|
50
|
+
rescue => e
|
51
|
+
design_doc = DesignDocument.get(@class_name)
|
52
|
+
design_doc.add_map_view(view_name, map_function).add_reduce_view(view_name, reduce_function).save
|
53
|
+
resp = RelaxDB.db.send(method, query.view_path, query.keys)
|
54
|
+
end
|
55
|
+
|
56
|
+
data = JSON.parse(resp.body)
|
57
|
+
ViewResult.new(data)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
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,18 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class ViewResult < DelegateClass(Array)
|
4
|
+
|
5
|
+
attr_reader :offset, :total_rows
|
6
|
+
|
7
|
+
def initialize(result_hash)
|
8
|
+
objs = RelaxDB.create_from_hash(result_hash)
|
9
|
+
|
10
|
+
@offset = result_hash["offset"]
|
11
|
+
@total_rows = result_hash["total_rows"]
|
12
|
+
|
13
|
+
super(objs)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
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
|
@@ -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,64 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
# A little naiive, needs quite a bit more thought and work
|
4
|
+
|
5
|
+
describe RelaxDB::Document, "callbacks" do
|
6
|
+
|
7
|
+
before(:all) do
|
8
|
+
RelaxDB.configure(:host => "localhost", :port => 5984)
|
9
|
+
end
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
RelaxDB.delete_db "relaxdb_spec_db" rescue "ok"
|
13
|
+
RelaxDB.use_db "relaxdb_spec_db"
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "before_save" do
|
17
|
+
|
18
|
+
it "should be run before the object is saved" do
|
19
|
+
c = Class.new(RelaxDB::Document) do
|
20
|
+
before_save lambda { |s| s.gem += 1 if s.unsaved? }
|
21
|
+
property :gem
|
22
|
+
end
|
23
|
+
p = c.new(:gem => 5).save
|
24
|
+
p.gem.should == 6
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should prevent the object from being saved if it returns false" do
|
28
|
+
c = Class.new(RelaxDB::Document) do
|
29
|
+
before_save lambda { false }
|
30
|
+
end
|
31
|
+
c.new.save.should == false
|
32
|
+
end
|
33
|
+
|
34
|
+
it "may be a proc" do
|
35
|
+
c = Class.new(RelaxDB::Document) do
|
36
|
+
before_save lambda { false }
|
37
|
+
end
|
38
|
+
c.new.save.should == false
|
39
|
+
end
|
40
|
+
|
41
|
+
it "may be a method" do
|
42
|
+
c = Class.new(RelaxDB::Document) do
|
43
|
+
before_save :never
|
44
|
+
def never; false; end
|
45
|
+
end
|
46
|
+
c.new.save.should == false
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "after_save" do
|
52
|
+
|
53
|
+
it "should be run after the object is saved" do
|
54
|
+
c = Class.new(RelaxDB::Document) do
|
55
|
+
after_save lambda { |s| s.gem +=1 unless s.unsaved? }
|
56
|
+
property :gem
|
57
|
+
end
|
58
|
+
p = c.new(:gem => 5).save
|
59
|
+
p.gem.should == 6
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|