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.
@@ -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