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