relaxdb 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/LICENSE +20 -0
  2. data/README.textile +200 -0
  3. data/Rakefile +63 -0
  4. data/docs/spec_results.html +1059 -0
  5. data/lib/more/atomic_bulk_save_support.rb +18 -0
  6. data/lib/more/grapher.rb +48 -0
  7. data/lib/relaxdb.rb +50 -0
  8. data/lib/relaxdb/all_delegator.rb +44 -0
  9. data/lib/relaxdb/belongs_to_proxy.rb +29 -0
  10. data/lib/relaxdb/design_doc.rb +57 -0
  11. data/lib/relaxdb/document.rb +600 -0
  12. data/lib/relaxdb/extlib.rb +24 -0
  13. data/lib/relaxdb/has_many_proxy.rb +101 -0
  14. data/lib/relaxdb/has_one_proxy.rb +42 -0
  15. data/lib/relaxdb/migration.rb +40 -0
  16. data/lib/relaxdb/migration_version.rb +21 -0
  17. data/lib/relaxdb/net_http_server.rb +61 -0
  18. data/lib/relaxdb/paginate_params.rb +53 -0
  19. data/lib/relaxdb/paginator.rb +88 -0
  20. data/lib/relaxdb/query.rb +76 -0
  21. data/lib/relaxdb/references_many_proxy.rb +97 -0
  22. data/lib/relaxdb/relaxdb.rb +250 -0
  23. data/lib/relaxdb/server.rb +109 -0
  24. data/lib/relaxdb/taf2_curb_server.rb +63 -0
  25. data/lib/relaxdb/uuid_generator.rb +21 -0
  26. data/lib/relaxdb/validators.rb +11 -0
  27. data/lib/relaxdb/view_object.rb +34 -0
  28. data/lib/relaxdb/view_result.rb +18 -0
  29. data/lib/relaxdb/view_uploader.rb +49 -0
  30. data/lib/relaxdb/views.rb +114 -0
  31. data/readme.rb +80 -0
  32. data/spec/belongs_to_spec.rb +124 -0
  33. data/spec/callbacks_spec.rb +80 -0
  34. data/spec/derived_properties_spec.rb +112 -0
  35. data/spec/design_doc_spec.rb +34 -0
  36. data/spec/doc_inheritable_spec.rb +100 -0
  37. data/spec/document_spec.rb +545 -0
  38. data/spec/has_many_spec.rb +202 -0
  39. data/spec/has_one_spec.rb +123 -0
  40. data/spec/migration_spec.rb +97 -0
  41. data/spec/migration_version_spec.rb +28 -0
  42. data/spec/paginate_params_spec.rb +15 -0
  43. data/spec/paginate_spec.rb +360 -0
  44. data/spec/query_spec.rb +90 -0
  45. data/spec/references_many_spec.rb +173 -0
  46. data/spec/relaxdb_spec.rb +364 -0
  47. data/spec/server_spec.rb +32 -0
  48. data/spec/spec.opts +1 -0
  49. data/spec/spec_helper.rb +65 -0
  50. data/spec/spec_models.rb +199 -0
  51. data/spec/view_by_spec.rb +76 -0
  52. data/spec/view_object_spec.rb +47 -0
  53. data/spec/view_spec.rb +23 -0
  54. 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,11 @@
1
+ module RelaxDB
2
+
3
+ module Validators
4
+
5
+ def validator_required(att, o)
6
+ !att.blank?
7
+ end
8
+
9
+ end
10
+
11
+ 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