cohitre-relaxdb 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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