relaxdb 0.3.5
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 +200 -0
- data/Rakefile +63 -0
- data/docs/spec_results.html +1059 -0
- data/lib/more/atomic_bulk_save_support.rb +18 -0
- data/lib/more/grapher.rb +48 -0
- data/lib/relaxdb.rb +50 -0
- data/lib/relaxdb/all_delegator.rb +44 -0
- data/lib/relaxdb/belongs_to_proxy.rb +29 -0
- data/lib/relaxdb/design_doc.rb +57 -0
- data/lib/relaxdb/document.rb +600 -0
- data/lib/relaxdb/extlib.rb +24 -0
- data/lib/relaxdb/has_many_proxy.rb +101 -0
- data/lib/relaxdb/has_one_proxy.rb +42 -0
- data/lib/relaxdb/migration.rb +40 -0
- data/lib/relaxdb/migration_version.rb +21 -0
- data/lib/relaxdb/net_http_server.rb +61 -0
- data/lib/relaxdb/paginate_params.rb +53 -0
- data/lib/relaxdb/paginator.rb +88 -0
- data/lib/relaxdb/query.rb +76 -0
- data/lib/relaxdb/references_many_proxy.rb +97 -0
- data/lib/relaxdb/relaxdb.rb +250 -0
- data/lib/relaxdb/server.rb +109 -0
- data/lib/relaxdb/taf2_curb_server.rb +63 -0
- data/lib/relaxdb/uuid_generator.rb +21 -0
- data/lib/relaxdb/validators.rb +11 -0
- data/lib/relaxdb/view_object.rb +34 -0
- data/lib/relaxdb/view_result.rb +18 -0
- data/lib/relaxdb/view_uploader.rb +49 -0
- data/lib/relaxdb/views.rb +114 -0
- data/readme.rb +80 -0
- data/spec/belongs_to_spec.rb +124 -0
- data/spec/callbacks_spec.rb +80 -0
- data/spec/derived_properties_spec.rb +112 -0
- data/spec/design_doc_spec.rb +34 -0
- data/spec/doc_inheritable_spec.rb +100 -0
- data/spec/document_spec.rb +545 -0
- data/spec/has_many_spec.rb +202 -0
- data/spec/has_one_spec.rb +123 -0
- data/spec/migration_spec.rb +97 -0
- data/spec/migration_version_spec.rb +28 -0
- data/spec/paginate_params_spec.rb +15 -0
- data/spec/paginate_spec.rb +360 -0
- data/spec/query_spec.rb +90 -0
- data/spec/references_many_spec.rb +173 -0
- data/spec/relaxdb_spec.rb +364 -0
- data/spec/server_spec.rb +32 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/spec_models.rb +199 -0
- data/spec/view_by_spec.rb +76 -0
- data/spec/view_object_spec.rb +47 -0
- data/spec/view_spec.rb +23 -0
- metadata +137 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class Server
|
4
|
+
class Response
|
5
|
+
attr_reader :body
|
6
|
+
def initialize body
|
7
|
+
@body = body
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(host, port)
|
12
|
+
@host, @port = host, port
|
13
|
+
end
|
14
|
+
|
15
|
+
def delete(uri)
|
16
|
+
request(uri, 'delete'){ |c| c.http_delete}
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(uri)
|
20
|
+
request(uri, 'get'){ |c| c.http_get}
|
21
|
+
end
|
22
|
+
|
23
|
+
def put(uri, json)
|
24
|
+
request(uri, 'put') do |c|
|
25
|
+
c.headers['content-type'] = 'application/json'
|
26
|
+
c.headers['X-Couch-Full-Commit'] = "true"
|
27
|
+
c.http_put json
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def post(uri, json)
|
32
|
+
request(uri, 'post') do |c|
|
33
|
+
c.headers['content-type'] = 'application/json'
|
34
|
+
c.http_post json
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def request(uri, method)
|
39
|
+
c = Curl::Easy.new "http://#{@host}:#{@port}#{uri}"
|
40
|
+
yield c
|
41
|
+
|
42
|
+
if c.response_code < 200 || c.response_code >= 300
|
43
|
+
status_line = c.header_str.split('\r\n').first
|
44
|
+
msg = "#{c.response_code}:#{status_line}\nMETHOD:#{method}\nURI:#{uri}\n#{c.body_str}"
|
45
|
+
begin
|
46
|
+
klass = RelaxDB.const_get("HTTP_#{c.response_code}")
|
47
|
+
e = klass.new(msg)
|
48
|
+
rescue
|
49
|
+
e = RuntimeError.new(msg)
|
50
|
+
end
|
51
|
+
|
52
|
+
raise e
|
53
|
+
end
|
54
|
+
Response.new c.body_str
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
"http://#{@host}:#{@port}/"
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
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,49 @@
|
|
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, the view name may not contain '-'
|
10
|
+
# Exepcted function declaration form is
|
11
|
+
# function funcname-functype(doc) {
|
12
|
+
# For example
|
13
|
+
# function Users_followers-map(doc) {
|
14
|
+
#
|
15
|
+
def upload(filename)
|
16
|
+
lines = File.readlines(filename)
|
17
|
+
dd = RelaxDB::DesignDocument.get(RelaxDB.dd)
|
18
|
+
extract(lines) do |vn, t, f|
|
19
|
+
dd.add_view(vn, t, f)
|
20
|
+
end
|
21
|
+
dd.save
|
22
|
+
end
|
23
|
+
|
24
|
+
def extract(lines)
|
25
|
+
# Index of function declaration matches
|
26
|
+
m = []
|
27
|
+
|
28
|
+
0.upto(lines.size-1) do |p|
|
29
|
+
line = lines[p]
|
30
|
+
m << p if line =~ /^function[^\{]+\{/
|
31
|
+
end
|
32
|
+
# Add one beyond the last line number as the final terminator
|
33
|
+
m << lines.size
|
34
|
+
|
35
|
+
0.upto(m.size-2) do |i|
|
36
|
+
declr = lines[m[i]]
|
37
|
+
declr =~ /(\w)+-(\w)+/
|
38
|
+
declr.sub!($&, '')
|
39
|
+
view_name, type = $&.split('-')
|
40
|
+
func = lines[m[i]...m[i+1]].join
|
41
|
+
yield view_name, type, func
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class ViewCreator
|
4
|
+
|
5
|
+
def self.all(kls)
|
6
|
+
class_name = kls[0]
|
7
|
+
map = <<-QUERY
|
8
|
+
function(doc) {
|
9
|
+
var class_match = #{kls_check kls}
|
10
|
+
if (class_match) {
|
11
|
+
emit(doc._id, doc);
|
12
|
+
}
|
13
|
+
}
|
14
|
+
QUERY
|
15
|
+
|
16
|
+
View.new "#{class_name}_all", map, sum_reduce_func
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.by_att_list(kls, *atts)
|
20
|
+
class_name = kls[0]
|
21
|
+
key = atts.map { |a| "doc.#{a}" }.join(", ")
|
22
|
+
key = atts.size > 1 ? key.sub(/^/, "[").sub(/$/, "]") : key
|
23
|
+
prop_check = atts.map { |a| "doc.#{a} !== undefined" }.join(" && ")
|
24
|
+
|
25
|
+
map = <<-QUERY
|
26
|
+
function(doc) {
|
27
|
+
var class_match = #{kls_check kls}
|
28
|
+
if (class_match && #{prop_check}) {
|
29
|
+
emit(#{key}, doc);
|
30
|
+
}
|
31
|
+
}
|
32
|
+
QUERY
|
33
|
+
|
34
|
+
view_name = "#{class_name}_by_" << atts.join("_and_")
|
35
|
+
View.new view_name, map, sum_reduce_func
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def self.has_n(client_class, relationship, target_class, relationship_to_client)
|
40
|
+
map = <<-QUERY
|
41
|
+
function(doc) {
|
42
|
+
if (doc.relaxdb_class == "#{target_class}" && doc.#{relationship_to_client}_id)
|
43
|
+
emit(doc.#{relationship_to_client}_id, doc);
|
44
|
+
}
|
45
|
+
QUERY
|
46
|
+
|
47
|
+
view_name = "#{client_class}_#{relationship}"
|
48
|
+
View.new view_name, map
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.references_many(client_class, relationship, target_class, peers)
|
52
|
+
map = <<-QUERY
|
53
|
+
function(doc) {
|
54
|
+
if (doc.relaxdb_class == "#{target_class}" && doc.#{peers}) {
|
55
|
+
var i;
|
56
|
+
for(i = 0; i < doc.#{peers}.length; i++) {
|
57
|
+
emit(doc.#{peers}[i], doc);
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
QUERY
|
62
|
+
|
63
|
+
view_name = "#{client_class}_#{relationship}"
|
64
|
+
View.new view_name, map
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.kls_check kls
|
68
|
+
kls_names = kls.map{ |k| %Q("#{k}") }.join(",")
|
69
|
+
"[#{kls_names}].indexOf(doc.relaxdb_class) >= 0;"
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.sum_reduce_func
|
73
|
+
<<-QUERY
|
74
|
+
function(keys, values, rereduce) {
|
75
|
+
if (rereduce) {
|
76
|
+
return sum(values);
|
77
|
+
} else {
|
78
|
+
return values.length;
|
79
|
+
}
|
80
|
+
}
|
81
|
+
QUERY
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
class View
|
87
|
+
|
88
|
+
attr_reader :view_name
|
89
|
+
|
90
|
+
def initialize view_name, map_func, reduce_func = nil
|
91
|
+
@view_name = view_name
|
92
|
+
@map_func = map_func
|
93
|
+
@reduce_func = reduce_func
|
94
|
+
end
|
95
|
+
|
96
|
+
def design_doc
|
97
|
+
@design_doc ||= DesignDocument.get(RelaxDB.dd)
|
98
|
+
end
|
99
|
+
|
100
|
+
def save
|
101
|
+
dd = design_doc
|
102
|
+
dd.add_map_view(@view_name, @map_func)
|
103
|
+
dd.add_reduce_view(@view_name, @reduce_func) if @reduce_func
|
104
|
+
dd.save
|
105
|
+
end
|
106
|
+
|
107
|
+
def exists?
|
108
|
+
dd = design_doc
|
109
|
+
dd.data["views"] && dd.data["views"][@view_name]
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
data/readme.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# README code is extracted from this file (pagination code excepted).
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'lib/relaxdb'
|
5
|
+
|
6
|
+
RelaxDB.configure :host => "localhost", :port => 5984, :design_doc => "app" #, :logger => Logger.new(STDOUT)
|
7
|
+
RelaxDB.delete_db "relaxdb_scratch" rescue :ok
|
8
|
+
RelaxDB.use_db "relaxdb_scratch"
|
9
|
+
RelaxDB.enable_view_creation # creates views when class definition is executed
|
10
|
+
|
11
|
+
class User < RelaxDB::Document
|
12
|
+
property :name
|
13
|
+
end
|
14
|
+
|
15
|
+
class Invite < RelaxDB::Document
|
16
|
+
|
17
|
+
property :created_at
|
18
|
+
|
19
|
+
property :event_name
|
20
|
+
|
21
|
+
property :state, :default => "awaiting_response",
|
22
|
+
:validator => lambda { |s| %w(accepted rejected awaiting_response).include? s }
|
23
|
+
|
24
|
+
references :sender, :validator => :required
|
25
|
+
|
26
|
+
references :recipient, :validator => :required
|
27
|
+
|
28
|
+
property :sender_name,
|
29
|
+
:derived => [:sender, lambda { |p, o| o.sender.name } ]
|
30
|
+
|
31
|
+
view_by :sender_name
|
32
|
+
view_by :sender_id
|
33
|
+
view_by :recipient_id, :created_at, :descending => true
|
34
|
+
|
35
|
+
def on_update_conflict
|
36
|
+
puts "conflict!"
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
# Saving objects
|
42
|
+
|
43
|
+
sofa = User.new(:name => "sofa").save!
|
44
|
+
futon = User.new(:name => "futon").save!
|
45
|
+
|
46
|
+
i = Invite.new :sender => sofa, :recipient => futon, :event_name => "CouchCamp"
|
47
|
+
i.save!
|
48
|
+
|
49
|
+
# Loading and querying
|
50
|
+
|
51
|
+
il = RelaxDB.load i._id
|
52
|
+
puts i == il # true
|
53
|
+
|
54
|
+
ir = Invite.by_sender_name "sofa"
|
55
|
+
puts i == ir # true
|
56
|
+
|
57
|
+
ix = Invite.by_sender_name(:key => "sofa").first
|
58
|
+
puts i == ix # true
|
59
|
+
|
60
|
+
# Denormalization
|
61
|
+
|
62
|
+
puts ix.sender_name # prints sofa, no requests to CouchDB made
|
63
|
+
puts ix.sender.name # prints sofa, a single CouchDB request made
|
64
|
+
|
65
|
+
# Saving with conflicts
|
66
|
+
|
67
|
+
idup = i.dup
|
68
|
+
i.save!
|
69
|
+
idup.save # conflict printed
|
70
|
+
|
71
|
+
# Saving with and without validations
|
72
|
+
|
73
|
+
i = Invite.new :sender => sofa, :event_name => "CouchCamp"
|
74
|
+
|
75
|
+
i.save! rescue :ok # save! throws an exception on validation failure or conflict
|
76
|
+
i.save # returns false rather than throwing an exception
|
77
|
+
puts i.errors.inspect # prints {:recipient=>"invalid:"}
|
78
|
+
|
79
|
+
i.validation_skip_list << :recipient # Any and all validations may be skipped
|
80
|
+
i.save # succeeds
|
@@ -0,0 +1,124 @@
|
|
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
|
+
setup_test_db
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "belongs_to" do
|
11
|
+
|
12
|
+
it "should return nil when accessed before assignment" do
|
13
|
+
r = Rating.new
|
14
|
+
r.photo.should == nil
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should be establishable via constructor attribute" do
|
18
|
+
p = Photo.new
|
19
|
+
r = Rating.new :photo => p
|
20
|
+
r.photo.should == p
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should be establishable via constructor id" do
|
24
|
+
p = Photo.new.save
|
25
|
+
r = Rating.new(:photo_id => p._id).save
|
26
|
+
r.photo.should == p
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should establish the parent relationship when supplied a parent and saved" do
|
30
|
+
p = Photo.new.save
|
31
|
+
r = Rating.new
|
32
|
+
r.photo = p
|
33
|
+
# I'm not saying the following is correct or desired - merely codifying how things stand
|
34
|
+
p.rating.should be_nil
|
35
|
+
r.save
|
36
|
+
p.rating.should == r
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should establish the parent relationship when supplied a parent id and saved" do
|
40
|
+
p = Photo.new.save
|
41
|
+
r = Rating.new(:photo_id => p._id).save
|
42
|
+
p.rating.should == r
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should return the same object on repeated invocations" do
|
46
|
+
p = Photo.new.save
|
47
|
+
r = Rating.new(:photo => p).save
|
48
|
+
r = RelaxDB.load(r._id)
|
49
|
+
r.photo.object_id.should == r.photo.object_id
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should be nullified when the parent is destroyed" do
|
53
|
+
r = Rating.new
|
54
|
+
p = Photo.new(:rating => r).save
|
55
|
+
p.destroy!
|
56
|
+
RelaxDB.load(r._id).photo.should be_nil
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should be preserved across save / load boundary" do
|
60
|
+
r = Rating.new
|
61
|
+
p = Photo.new(:rating => r).save
|
62
|
+
r = RelaxDB.load r._id
|
63
|
+
r.photo.should == p
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should be able to reference itself via its parent" do
|
67
|
+
r = Rating.new
|
68
|
+
p = Photo.new(:rating => r).save
|
69
|
+
r = RelaxDB.load r._id
|
70
|
+
r.photo.rating.should == r
|
71
|
+
end
|
72
|
+
|
73
|
+
it "may be used reciprocally" do
|
74
|
+
C1 = Class.new(RelaxDB::Document) do
|
75
|
+
belongs_to :c2
|
76
|
+
end
|
77
|
+
C2 = Class.new(RelaxDB::Document) do
|
78
|
+
belongs_to :c1
|
79
|
+
end
|
80
|
+
i1, i2 = C1.new, C2.new
|
81
|
+
|
82
|
+
i1.c2 = i2
|
83
|
+
i1.save!
|
84
|
+
i2.c1 = i1
|
85
|
+
i2.save!
|
86
|
+
|
87
|
+
i1 = RelaxDB.load i1._id
|
88
|
+
i1.c2.should == i2
|
89
|
+
|
90
|
+
i2 = RelaxDB.load i2._id
|
91
|
+
i2.c1.should == i1
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "validator" do
|
95
|
+
|
96
|
+
it "should be passed the _id and object" do
|
97
|
+
a = Atom.new(:_id => "atom").save!
|
98
|
+
c = Class.new(RelaxDB::Document) do
|
99
|
+
belongs_to :foo, :validator => lambda { |foo_id, obj| foo_id.reverse == obj._id }
|
100
|
+
end
|
101
|
+
c.new(:_id => "mota", :foo => a).save!
|
102
|
+
end
|
103
|
+
|
104
|
+
it "may be used with a predefined validator" do
|
105
|
+
c = Class.new(RelaxDB::Document) do
|
106
|
+
belongs_to :foo, :validator => :required
|
107
|
+
end
|
108
|
+
c.new.save.should be_false
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should be provided with a default error message when validation fails" do
|
112
|
+
c = Class.new(RelaxDB::Document) do
|
113
|
+
belongs_to :foo, :validator => :required
|
114
|
+
end
|
115
|
+
x = c.new
|
116
|
+
x.save
|
117
|
+
x.errors[:foo].should_not be_blank
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|