paulcarey-relaxdb 0.2.6 → 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +16 -2
- data/Rakefile +1 -1
- data/lib/relaxdb/document.rb +101 -29
- data/lib/relaxdb/has_many_proxy.rb +17 -0
- data/lib/relaxdb/relaxdb.rb +43 -20
- data/lib/relaxdb/server.rb +15 -7
- data/spec/derived_properties_spec.rb +4 -8
- data/spec/document_spec.rb +60 -4
- data/spec/has_many_spec.rb +35 -6
- data/spec/relaxdb_spec.rb +56 -7
- data/spec/spec_models.rb +101 -90
- metadata +2 -2
data/README.textile
CHANGED
@@ -1,9 +1,23 @@
|
|
1
1
|
h3. What's New?
|
2
2
|
|
3
|
-
*
|
3
|
+
* Potentially breaking change. Skipping validations is now done by adding attribute symbols to an object's list rather than passing them to @save@. For example @my_obj.validation_skip_list << :foo@. This offers per object granularity over validations when working with bulk_save.
|
4
|
+
|
5
|
+
* Potentially breaking change. @load@ now returns an array if passed an array of size one. Previously it would have returned a single object.
|
6
|
+
|
7
|
+
* Update conflict hook and property
|
8
|
+
|
9
|
+
* Semantic consistency for bulk_save and bulk_save! wrt to save and save!
|
10
|
+
|
11
|
+
* Multiple exception handling improvements
|
12
|
+
|
13
|
+
* @save_all@ that issues a bulk_save for an object and its has_one and has_many children
|
14
|
+
|
15
|
+
* assignment of @has_many@ relationships
|
4
16
|
|
5
17
|
* Validations may be skipped by passing the attribute symbol(s) to @save@ or @save!@.
|
6
18
|
|
19
|
+
* Denormalisation via derived properties. Examples in spec/derived_properties_spec.rb.
|
20
|
+
|
7
21
|
* Semantic changes for @ has_many#<< @. The parent object is now assigned to the child object *prior* to validation. This potentially breaking change was made to allow child objects to derive properties from a parent object.
|
8
22
|
|
9
23
|
* Semantic consistency for load, load!, save and save!. The bang versions raise an exception when their more relaxed siblings would simply return nil.
|
@@ -18,7 +32,7 @@ h3. What's New?
|
|
18
32
|
** For example, @ Numbers.all.sorted_by(:val) { |q| q.keys([1,2,3,5]) } @
|
19
33
|
* Works with CouchDB 0.9 trunk as of 2009/01/02. Note that pagination won't work correctly on trunk until issue "COUCHDB-135":http://issues.apache.org/jira/browse/COUCHDB-135 is fixed.
|
20
34
|
|
21
|
-
*Note*:
|
35
|
+
*Note*: Current versions require CouchDB 0.9 trunk. If you're working with CouchDB 0.8 or 0.8.1, please build from commit @ a8a2d496462 @.
|
22
36
|
|
23
37
|
h2. Overview
|
24
38
|
|
data/Rakefile
CHANGED
data/lib/relaxdb/document.rb
CHANGED
@@ -7,6 +7,9 @@ module RelaxDB
|
|
7
7
|
# Used to store validation messages
|
8
8
|
attr_accessor :errors
|
9
9
|
|
10
|
+
# Attribute symbols added to this list won't be validated on save
|
11
|
+
attr_accessor :validation_skip_list
|
12
|
+
|
10
13
|
# Define properties and property methods
|
11
14
|
|
12
15
|
def self.property(prop, opts={})
|
@@ -62,13 +65,13 @@ module RelaxDB
|
|
62
65
|
end
|
63
66
|
end
|
64
67
|
|
65
|
-
def self.create_validation_msg(
|
68
|
+
def self.create_validation_msg(att, validation_msg)
|
66
69
|
if validation_msg.is_a?(Proc)
|
67
70
|
validation_msg.arity == 1 ?
|
68
|
-
define_method("#{
|
69
|
-
define_method("#{
|
71
|
+
define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val) } :
|
72
|
+
define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val, self) }
|
70
73
|
else
|
71
|
-
define_method("#{
|
74
|
+
define_method("#{att}_validation_msg") { validation_msg }
|
72
75
|
end
|
73
76
|
end
|
74
77
|
|
@@ -84,12 +87,22 @@ module RelaxDB
|
|
84
87
|
@derived_prop_writers ||= {}
|
85
88
|
end
|
86
89
|
|
90
|
+
#
|
91
|
+
# The rationale for rescuing the send below is that the lambda for a derived
|
92
|
+
# property shouldn't need to concern itself with checking the validity of
|
93
|
+
# the underlying property. Nor, IMO, should clients be exposed to the
|
94
|
+
# possibility of a writer raising an exception.
|
95
|
+
#
|
87
96
|
def write_derived_props(source)
|
88
97
|
writers = self.class.derived_prop_writers[source]
|
89
98
|
if writers
|
90
99
|
writers.each do |prop, writer|
|
91
100
|
current_val = send(prop)
|
92
|
-
|
101
|
+
begin
|
102
|
+
send("#{prop}=", writer.call(current_val, self))
|
103
|
+
rescue => e
|
104
|
+
RelaxDB.logger.warn "Deriving #{prop} from #{source} raised #{e}"
|
105
|
+
end
|
93
106
|
end
|
94
107
|
end
|
95
108
|
end
|
@@ -110,6 +123,7 @@ module RelaxDB
|
|
110
123
|
self._id = UuidGenerator.uuid
|
111
124
|
|
112
125
|
@errors = Errors.new
|
126
|
+
@validation_skip_list = []
|
113
127
|
|
114
128
|
# Set default properties if this object has not known CouchDB
|
115
129
|
unless hash["_rev"]
|
@@ -201,31 +215,76 @@ module RelaxDB
|
|
201
215
|
data.to_json
|
202
216
|
end
|
203
217
|
|
204
|
-
# Order changed as of 30/10/2008 to be consistent with ActiveRecord
|
205
218
|
# Not yet sure of final implemention for hooks - may lean more towards DM than AR
|
206
|
-
def save
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
219
|
+
def save
|
220
|
+
if pre_save && save_to_couch
|
221
|
+
after_save
|
222
|
+
self
|
223
|
+
else
|
224
|
+
false
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def save_to_couch
|
229
|
+
begin
|
230
|
+
resp = RelaxDB.db.put(_id, to_json)
|
231
|
+
self._rev = JSON.parse(resp.body)["rev"]
|
232
|
+
rescue HTTP_412
|
233
|
+
on_update_conflict
|
234
|
+
@update_conflict = true
|
235
|
+
return false
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def on_update_conflict
|
240
|
+
# override with any behaviour you want to happen when
|
241
|
+
# CouchDB returns DocumentConflict on an attempt to save
|
242
|
+
end
|
243
|
+
|
244
|
+
def pre_save
|
245
|
+
return false unless validates?
|
246
|
+
return false unless before_save
|
247
|
+
set_created_at if new_document?
|
248
|
+
true
|
249
|
+
end
|
250
|
+
|
251
|
+
def post_save
|
215
252
|
after_save
|
253
|
+
end
|
254
|
+
|
255
|
+
def save!
|
256
|
+
if save
|
257
|
+
self
|
258
|
+
elsif update_conflict?
|
259
|
+
raise UpdateConflict, self
|
260
|
+
else
|
261
|
+
raise ValidationFailure, self.errors.to_json
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def save_all
|
266
|
+
RelaxDB.bulk_save(self, *all_children)
|
267
|
+
end
|
216
268
|
|
217
|
-
|
218
|
-
|
269
|
+
def save_all!
|
270
|
+
RelaxDB.bulk_save!(self, *all_children)
|
271
|
+
end
|
272
|
+
|
273
|
+
def all_children
|
274
|
+
ho = self.class.has_one_rels.map { |r| send(r) }
|
275
|
+
hm = self.class.has_many_rels.inject([]) { |m,r| m += send(r).children }
|
276
|
+
ho + hm
|
277
|
+
end
|
219
278
|
|
220
|
-
def
|
221
|
-
|
279
|
+
def update_conflict?
|
280
|
+
@update_conflict
|
222
281
|
end
|
223
282
|
|
224
|
-
def validates?
|
225
|
-
props = properties -
|
283
|
+
def validates?
|
284
|
+
props = properties - validation_skip_list
|
226
285
|
prop_vals = props.map { |prop| instance_variable_get("@#{prop}") }
|
227
286
|
|
228
|
-
rels = self.class.belongs_to_rels.keys -
|
287
|
+
rels = self.class.belongs_to_rels.keys - validation_skip_list
|
229
288
|
rel_vals = rels.map { |rel| instance_variable_get("@#{rel}_id") }
|
230
289
|
|
231
290
|
att_names = props + rels
|
@@ -246,7 +305,7 @@ module RelaxDB
|
|
246
305
|
begin
|
247
306
|
success = send("validate_#{att_name}", att_val)
|
248
307
|
rescue => e
|
249
|
-
RelaxDB.logger.warn
|
308
|
+
RelaxDB.logger.warn "Validating #{att_name} with #{att_val} raised #{e}"
|
250
309
|
succes = false
|
251
310
|
end
|
252
311
|
|
@@ -255,8 +314,8 @@ module RelaxDB
|
|
255
314
|
begin
|
256
315
|
@errors[att_name] = send("#{att_name}_validation_msg", att_val)
|
257
316
|
rescue => e
|
258
|
-
RelaxDB.logger.warn
|
259
|
-
@errors[att_name] = "validation_msg_exception:invalid:#{
|
317
|
+
RelaxDB.logger.warn "Validation_msg for #{att_name} with #{att_val} raised #{e}"
|
318
|
+
@errors[att_name] = "validation_msg_exception:invalid:#{att_val}"
|
260
319
|
end
|
261
320
|
else
|
262
321
|
@errors[att_name] = "invalid:#{att_val}"
|
@@ -322,7 +381,6 @@ module RelaxDB
|
|
322
381
|
end
|
323
382
|
|
324
383
|
def self.references_many_rels
|
325
|
-
# Don't force clients to check its instantiated
|
326
384
|
@references_many_rels ||= []
|
327
385
|
end
|
328
386
|
|
@@ -334,8 +392,10 @@ module RelaxDB
|
|
334
392
|
create_or_get_proxy(HasManyProxy, relationship, opts)
|
335
393
|
end
|
336
394
|
|
337
|
-
define_method("#{relationship}=") do
|
338
|
-
|
395
|
+
define_method("#{relationship}=") do |children|
|
396
|
+
create_or_get_proxy(HasManyProxy, relationship, opts).children = children
|
397
|
+
write_derived_props(relationship)
|
398
|
+
children
|
339
399
|
end
|
340
400
|
end
|
341
401
|
|
@@ -354,6 +414,8 @@ module RelaxDB
|
|
354
414
|
|
355
415
|
define_method("#{relationship}=") do |new_target|
|
356
416
|
create_or_get_proxy(HasOneProxy, relationship).target = new_target
|
417
|
+
write_derived_props(relationship)
|
418
|
+
new_target
|
357
419
|
end
|
358
420
|
end
|
359
421
|
|
@@ -378,6 +440,7 @@ module RelaxDB
|
|
378
440
|
define_method("#{relationship}_id=") do |id|
|
379
441
|
instance_variable_set("@#{relationship}_id".to_sym, id)
|
380
442
|
write_derived_props(relationship)
|
443
|
+
id
|
381
444
|
end
|
382
445
|
|
383
446
|
# Allows belongs_to relationships to be used by the paginator
|
@@ -386,10 +449,16 @@ module RelaxDB
|
|
386
449
|
end
|
387
450
|
|
388
451
|
create_validator(relationship, opts[:validator]) if opts[:validator]
|
452
|
+
|
453
|
+
# Untested below
|
454
|
+
create_validation_msg(relationship, opts[:validation_msg]) if opts[:validation_msg]
|
455
|
+
end
|
456
|
+
|
457
|
+
class << self
|
458
|
+
alias_method :references, :belongs_to
|
389
459
|
end
|
390
460
|
|
391
461
|
def self.belongs_to_rels
|
392
|
-
# Don't force clients to check that it's instantiated
|
393
462
|
@belongs_to_rels ||= {}
|
394
463
|
end
|
395
464
|
|
@@ -404,6 +473,9 @@ module RelaxDB
|
|
404
473
|
# destroy! nullifies all relationships with peers and children before deleting
|
405
474
|
# itself in CouchDB
|
406
475
|
# The nullification and deletion are not performed in a transaction
|
476
|
+
#
|
477
|
+
# TODO: Current implemention may be inappropriate - causing CouchDB to try to JSON
|
478
|
+
# encode undefined. Ensure nil is serialized? See has_many_spec#should nullify its child relationships
|
407
479
|
def destroy!
|
408
480
|
self.class.references_many_rels.each do |rel|
|
409
481
|
send(rel).clear
|
@@ -3,6 +3,8 @@ module RelaxDB
|
|
3
3
|
class HasManyProxy
|
4
4
|
|
5
5
|
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :children
|
6
8
|
|
7
9
|
def initialize(client, relationship, opts)
|
8
10
|
@client = client
|
@@ -57,6 +59,14 @@ module RelaxDB
|
|
57
59
|
def [](*args)
|
58
60
|
@children[*args]
|
59
61
|
end
|
62
|
+
|
63
|
+
def first
|
64
|
+
@children[0]
|
65
|
+
end
|
66
|
+
|
67
|
+
def last
|
68
|
+
@children[size-1]
|
69
|
+
end
|
60
70
|
|
61
71
|
def each(&blk)
|
62
72
|
@children.each(&blk)
|
@@ -73,6 +83,13 @@ module RelaxDB
|
|
73
83
|
map_function = ViewCreator.has_n(@target_class, @relationship_as_viewed_by_target)
|
74
84
|
@children = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
|
75
85
|
end
|
86
|
+
|
87
|
+
def children=(children)
|
88
|
+
children.each do |obj|
|
89
|
+
obj.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
|
90
|
+
end
|
91
|
+
@children = children
|
92
|
+
end
|
76
93
|
|
77
94
|
def inspect
|
78
95
|
@children.inspect
|
data/lib/relaxdb/relaxdb.rb
CHANGED
@@ -2,6 +2,8 @@ module RelaxDB
|
|
2
2
|
|
3
3
|
class NotFound < StandardError; end
|
4
4
|
class DocumentNotSaved < StandardError; end
|
5
|
+
class UpdateConflict < DocumentNotSaved; end
|
6
|
+
class ValidationFailure < DocumentNotSaved; end
|
5
7
|
|
6
8
|
@@db = nil
|
7
9
|
|
@@ -21,11 +23,15 @@ module RelaxDB
|
|
21
23
|
|
22
24
|
# Creates the named database if it doesn't already exist
|
23
25
|
def use_db(name)
|
24
|
-
db.use_db
|
26
|
+
db.use_db name
|
27
|
+
end
|
28
|
+
|
29
|
+
def db_exists?(name)
|
30
|
+
db.db_exists? name
|
25
31
|
end
|
26
32
|
|
27
33
|
def delete_db(name)
|
28
|
-
db.delete_db
|
34
|
+
db.delete_db name
|
29
35
|
end
|
30
36
|
|
31
37
|
def list_dbs
|
@@ -36,41 +42,58 @@ module RelaxDB
|
|
36
42
|
db.replicate_db source, target
|
37
43
|
end
|
38
44
|
|
39
|
-
def bulk_save(*objs)
|
45
|
+
def bulk_save!(*objs)
|
46
|
+
pre_save_success = objs.inject(true) { |s, o| s &= o.pre_save }
|
47
|
+
raise ValidationFailure, objs unless pre_save_success
|
48
|
+
|
40
49
|
docs = {}
|
41
50
|
objs.each { |o| docs[o._id] = o }
|
51
|
+
|
52
|
+
begin
|
53
|
+
resp = db.post("_bulk_docs", { "docs" => objs }.to_json )
|
54
|
+
data = JSON.parse(resp.body)
|
42
55
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
56
|
+
data["new_revs"].each do |new_rev|
|
57
|
+
obj = docs[ new_rev["id"] ]
|
58
|
+
obj._rev = new_rev["rev"]
|
59
|
+
obj.post_save
|
60
|
+
end
|
61
|
+
rescue HTTP_412
|
62
|
+
raise UpdateConflict, objs
|
48
63
|
end
|
49
64
|
|
50
|
-
|
65
|
+
objs
|
66
|
+
end
|
67
|
+
|
68
|
+
def bulk_save(*objs)
|
69
|
+
begin
|
70
|
+
bulk_save!(*objs)
|
71
|
+
rescue ValidationFailure, UpdateConflict
|
72
|
+
false
|
73
|
+
end
|
51
74
|
end
|
52
75
|
|
53
|
-
def load(
|
54
|
-
if ids.
|
76
|
+
def load(ids)
|
77
|
+
if ids.is_a? Array
|
78
|
+
resp = db.post("_all_docs?include_docs=true", {:keys => ids}.to_json)
|
79
|
+
data = JSON.parse(resp.body)
|
80
|
+
data["rows"].map { |row| row["doc"] ? create_object(row["doc"]) : nil }
|
81
|
+
else
|
55
82
|
begin
|
56
|
-
resp = db.get(ids
|
83
|
+
resp = db.get(ids)
|
57
84
|
data = JSON.parse(resp.body)
|
58
85
|
create_object(data)
|
59
86
|
rescue HTTP_404
|
60
87
|
nil
|
61
88
|
end
|
62
|
-
else
|
63
|
-
resp = db.post("_all_docs?include_docs=true", {:keys => ids}.to_json)
|
64
|
-
data = JSON.parse(resp.body)
|
65
|
-
data["rows"].map { |row| row["doc"] ? create_object(row["doc"]) : nil }
|
66
89
|
end
|
67
90
|
end
|
68
91
|
|
69
|
-
def load!(
|
70
|
-
res = load(
|
92
|
+
def load!(ids)
|
93
|
+
res = load(ids)
|
71
94
|
|
72
|
-
raise NotFound if res == nil
|
73
|
-
raise NotFound if res.respond_to?(:include?) && res.include?(nil)
|
95
|
+
raise NotFound, ids if res == nil
|
96
|
+
raise NotFound, ids if res.respond_to?(:include?) && res.include?(nil)
|
74
97
|
|
75
98
|
res
|
76
99
|
end
|
data/lib/relaxdb/server.rb
CHANGED
@@ -2,6 +2,7 @@ module RelaxDB
|
|
2
2
|
|
3
3
|
class HTTP_404 < StandardError; end
|
4
4
|
class HTTP_409 < StandardError; end
|
5
|
+
class HTTP_412 < StandardError; end
|
5
6
|
|
6
7
|
class Server
|
7
8
|
|
@@ -62,13 +63,15 @@ module RelaxDB
|
|
62
63
|
end
|
63
64
|
|
64
65
|
class CouchDB
|
66
|
+
|
67
|
+
attr_reader :logger
|
65
68
|
|
66
69
|
# Used for test instrumentation only i.e. to assert that
|
67
|
-
# an expected number of
|
68
|
-
attr_accessor :get_count
|
70
|
+
# an expected number of requests have been issued
|
71
|
+
attr_accessor :get_count, :put_count
|
69
72
|
|
70
73
|
def initialize(config)
|
71
|
-
@get_count = 0
|
74
|
+
@get_count, @put_count = 0, 0
|
72
75
|
@server = RelaxDB::Server.new(config[:host], config[:port])
|
73
76
|
@logger = config[:logger] ? config[:logger] : Logger.new(Tempfile.new('couchdb.log'))
|
74
77
|
end
|
@@ -78,6 +81,10 @@ module RelaxDB
|
|
78
81
|
@db = name
|
79
82
|
end
|
80
83
|
|
84
|
+
def db_exists?(name)
|
85
|
+
@server.get("/#{name}") rescue false
|
86
|
+
end
|
87
|
+
|
81
88
|
def delete_db(name)
|
82
89
|
@logger.info("Deleting database #{name}")
|
83
90
|
@server.delete("/#{name}")
|
@@ -99,7 +106,7 @@ module RelaxDB
|
|
99
106
|
@server.delete("/#{@db}/#{path}")
|
100
107
|
end
|
101
108
|
|
102
|
-
# *ignored allows methods to invoke get or post indifferently
|
109
|
+
# *ignored allows methods to invoke get or post indifferently via send
|
103
110
|
def get(path=nil, *ignored)
|
104
111
|
@get_count += 1
|
105
112
|
@logger.info("GET /#{@db}/#{unesc(path)}")
|
@@ -112,6 +119,7 @@ module RelaxDB
|
|
112
119
|
end
|
113
120
|
|
114
121
|
def put(path=nil, json=nil)
|
122
|
+
@put_count += 1
|
115
123
|
@logger.info("PUT /#{@db}/#{unesc(path)} #{json}")
|
116
124
|
@server.put("/#{@db}/#{path}", json)
|
117
125
|
end
|
@@ -129,10 +137,10 @@ module RelaxDB
|
|
129
137
|
@db
|
130
138
|
end
|
131
139
|
|
132
|
-
def
|
133
|
-
@
|
140
|
+
def name=(name)
|
141
|
+
@db = name
|
134
142
|
end
|
135
|
-
|
143
|
+
|
136
144
|
private
|
137
145
|
|
138
146
|
def create_db_if_non_existant(name)
|
@@ -1,9 +1,6 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
2
|
require File.dirname(__FILE__) + '/spec_models.rb'
|
3
3
|
|
4
|
-
# These tests would ideally instrument server.rb, asserting that no
|
5
|
-
# HTTP requests are made when retrieving the derived values
|
6
|
-
|
7
4
|
class DpInvite < RelaxDB::Document
|
8
5
|
property :event_name, :derived => [:event, lambda { |en, i| i.event.name }]
|
9
6
|
belongs_to :event
|
@@ -46,12 +43,11 @@ describe RelaxDB::Document, "derived properties" do
|
|
46
43
|
i.event_name.should == "shindig"
|
47
44
|
end
|
48
45
|
|
49
|
-
it "will
|
50
|
-
#
|
51
|
-
|
52
|
-
lambda { DpInvite.new(:event_id => e._id) }.should raise_error
|
46
|
+
it "will not raise an exception when the source is nil" do
|
47
|
+
# See the rationale in Document.write_derived_props
|
48
|
+
DpInvite.new(:event => nil).save!
|
53
49
|
end
|
54
|
-
|
50
|
+
|
55
51
|
it "should only be updated for registered properties" do
|
56
52
|
invite = Class.new(RelaxDB::Document) do
|
57
53
|
property :event_name, :derived => [:foo, lambda { |en, i| i.event.name }]
|
data/spec/document_spec.rb
CHANGED
@@ -36,6 +36,10 @@ describe RelaxDB::Document do
|
|
36
36
|
Post.new(:foo => "").save
|
37
37
|
end
|
38
38
|
|
39
|
+
it "should create a document with a non conflicing state" do
|
40
|
+
Atom.new.should_not be_update_conflict
|
41
|
+
end
|
42
|
+
|
39
43
|
end
|
40
44
|
|
41
45
|
describe "#initialize" do
|
@@ -92,6 +96,14 @@ describe RelaxDB::Document do
|
|
92
96
|
p = Post.new(:created_at => back_then).save
|
93
97
|
p.created_at.should be_close(back_then, 1)
|
94
98
|
end
|
99
|
+
|
100
|
+
it "should set document conflict state on conflicting save" do
|
101
|
+
a1 = Atom.new
|
102
|
+
a2 = a1.dup
|
103
|
+
a1.save!
|
104
|
+
a2.save
|
105
|
+
a2.should be_update_conflict
|
106
|
+
end
|
95
107
|
|
96
108
|
end
|
97
109
|
|
@@ -102,15 +114,57 @@ describe RelaxDB::Document do
|
|
102
114
|
RelaxDB.load(a._id).should == a
|
103
115
|
end
|
104
116
|
|
105
|
-
it "should
|
117
|
+
it "should raise ValidationFailure on validation failure" do
|
106
118
|
r = Class.new(RelaxDB::Document) do
|
107
119
|
property :thumbs_up, :validator => lambda { false }
|
108
120
|
end
|
109
121
|
lambda do
|
110
122
|
r.new.save!
|
111
|
-
end.should raise_error(RelaxDB::
|
123
|
+
end.should raise_error(RelaxDB::ValidationFailure)
|
112
124
|
end
|
113
125
|
|
126
|
+
it "should raise UpdateConflict on an update conflict" do
|
127
|
+
a1 = Atom.new
|
128
|
+
a2 = a1.dup
|
129
|
+
a1.save!
|
130
|
+
lambda { a2.save! }.should raise_error(RelaxDB::UpdateConflict)
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "#save_all" do
|
136
|
+
|
137
|
+
before(:each) do
|
138
|
+
# Create the underlying views
|
139
|
+
User.new(:items => [], :invites_received => [], :invites_sent => [])
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should issue only a single PUT request" do
|
143
|
+
RelaxDB.db.put_count = 0
|
144
|
+
RelaxDB.db.get_count = 0
|
145
|
+
|
146
|
+
i1, i2 = Item.new(:name => "i1"), Item.new(:name => "i2")
|
147
|
+
u = User.new(:items => [i1, i2])
|
148
|
+
u.save_all!
|
149
|
+
|
150
|
+
RelaxDB.db.put_count.should == 0
|
151
|
+
RelaxDB.db.get_count.should == 3
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
describe "#all_children" do
|
157
|
+
|
158
|
+
it "should return an array containing all children" do
|
159
|
+
r = Rating.new
|
160
|
+
p = Photo.new(:rating => r)
|
161
|
+
t = Tag.new
|
162
|
+
t1, t2 = Tagging.new(:photo => p, :tag => t), Tagging.new(:photo => p, :tag => t)
|
163
|
+
p.taggings = [t1, t2]
|
164
|
+
p.all_children.size.should == 3
|
165
|
+
[r, t1, t2].each { |c| p.all_children.should include(c) }
|
166
|
+
end
|
167
|
+
|
114
168
|
end
|
115
169
|
|
116
170
|
describe "user defined property reader" do
|
@@ -171,7 +225,7 @@ describe RelaxDB::Document do
|
|
171
225
|
|
172
226
|
it "should prevent the object from being resaved" do
|
173
227
|
p = Atom.new.save.destroy!
|
174
|
-
lambda { p.save }.should raise_error
|
228
|
+
lambda { p.save! }.should raise_error
|
175
229
|
end
|
176
230
|
|
177
231
|
it "will result in undefined behaviour when invoked on unsaved objects" do
|
@@ -434,7 +488,9 @@ describe RelaxDB::Document do
|
|
434
488
|
r = Class.new(RelaxDB::Document) do
|
435
489
|
property :thumbs_up, :validator => lambda { raise }
|
436
490
|
end
|
437
|
-
r.new
|
491
|
+
x = r.new
|
492
|
+
x.validation_skip_list << :thumbs_up
|
493
|
+
x.save!
|
438
494
|
end
|
439
495
|
|
440
496
|
end
|
data/spec/has_many_spec.rb
CHANGED
@@ -16,7 +16,7 @@ describe RelaxDB::HasManyProxy do
|
|
16
16
|
|
17
17
|
it "should be considered enumerable" do
|
18
18
|
u = User.new.save
|
19
|
-
u.items.should be_a_kind_of(
|
19
|
+
u.items.should be_a_kind_of(Enumerable)
|
20
20
|
end
|
21
21
|
|
22
22
|
it "should actually be enumerable" do
|
@@ -41,7 +41,7 @@ describe RelaxDB::HasManyProxy do
|
|
41
41
|
m = RelaxDB.load m._id
|
42
42
|
m.multi_word_children[0].should == c
|
43
43
|
end
|
44
|
-
|
44
|
+
|
45
45
|
describe "#<<" do
|
46
46
|
|
47
47
|
it "should link the added item to the parent" do
|
@@ -74,10 +74,39 @@ describe RelaxDB::HasManyProxy do
|
|
74
74
|
end
|
75
75
|
|
76
76
|
describe "#=" do
|
77
|
-
|
78
|
-
|
79
|
-
#
|
80
|
-
|
77
|
+
|
78
|
+
before(:each) do
|
79
|
+
# Create the underlying views
|
80
|
+
User.new(:items => [], :invites_received => [], :invites_sent => [])
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should not attempt to save the child objects when the relationship is established" do
|
84
|
+
RelaxDB.db.put_count = 0
|
85
|
+
i1, i2 = Item.new(:name => "i1"), Item.new(:name => "i2")
|
86
|
+
User.new(:items => [i1, i2])
|
87
|
+
RelaxDB.db.put_count.should == 0
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should preserve given relationships across save/load boundary" do
|
91
|
+
i1, i2 = Item.new(:name => "i1"), Item.new(:name => "i2")
|
92
|
+
u = User.new(:items => [i1, i2])
|
93
|
+
u.save_all!
|
94
|
+
u = RelaxDB.load u._id
|
95
|
+
u.items.map { |i| i.name }.sort.join.should == "i1i2"
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should invoke the derived properties writer" do
|
99
|
+
P = Class.new(RelaxDB::Document) do
|
100
|
+
property :foo, :derived => [:zongs, lambda {|f, o| o.zongs.first.z / 2 }]
|
101
|
+
has_many :zongs, :class => "Zong"
|
102
|
+
end
|
103
|
+
Zong = Class.new(RelaxDB::Document) do
|
104
|
+
property :z
|
105
|
+
belongs_to :p
|
106
|
+
end
|
107
|
+
oz = Zong.new(:z => 10)
|
108
|
+
op = P.new(:zongs => [oz])
|
109
|
+
op.foo.should == 5
|
81
110
|
end
|
82
111
|
|
83
112
|
end
|
data/spec/relaxdb_spec.rb
CHANGED
@@ -28,20 +28,69 @@ describe RelaxDB do
|
|
28
28
|
end
|
29
29
|
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
|
+
# bulk_save and bulk_save! should match Document#save and Document#save! semantics
|
32
33
|
describe ".bulk_save" do
|
33
34
|
|
34
35
|
it "should be invokable multiple times" do
|
35
|
-
t1 = Tag.new
|
36
|
-
t2 = Tag.new(:name => "t2")
|
36
|
+
t1, t2 = Tag.new, Tag.new
|
37
37
|
RelaxDB.bulk_save(t1, t2)
|
38
38
|
RelaxDB.bulk_save(t1, t2)
|
39
39
|
end
|
40
40
|
|
41
|
+
it "should return the objects it was passed" do
|
42
|
+
t1, t2 = Tag.new, Tag.new
|
43
|
+
ta, tb = RelaxDB.bulk_save(t1, t2)
|
44
|
+
ta.should == t1
|
45
|
+
tb.should == t2
|
46
|
+
end
|
47
|
+
|
41
48
|
it "should succeed when passed no args" do
|
42
49
|
RelaxDB.bulk_save
|
43
50
|
end
|
44
51
|
|
52
|
+
it "should return false on failure" do
|
53
|
+
c = Class.new(RelaxDB::Document) do
|
54
|
+
property :foo, :validator => lambda { false }
|
55
|
+
end
|
56
|
+
x = c.new
|
57
|
+
RelaxDB.bulk_save(x).should be_false
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should not attempt to save if a pre-save stage fails" do
|
61
|
+
c = Class.new(RelaxDB::Document) do
|
62
|
+
property :foo, :validator => lambda { false }
|
63
|
+
end
|
64
|
+
x = c.new
|
65
|
+
RelaxDB.bulk_save(x)
|
66
|
+
x.should be_new_document
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should invoke the after-save stage after a successful save" do
|
70
|
+
c = Class.new(RelaxDB::Document) do
|
71
|
+
attr_accessor :foo
|
72
|
+
after_save lambda { |c| c.foo = :bar }
|
73
|
+
end
|
74
|
+
x = c.new
|
75
|
+
RelaxDB.bulk_save(x).first.foo.should == :bar
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
describe ".bulk_save!" do
|
81
|
+
|
82
|
+
it "should raise an exception if a obj fails validation" do
|
83
|
+
c = Class.new(RelaxDB::Document) do
|
84
|
+
property :foo, :validator => lambda { false }
|
85
|
+
end
|
86
|
+
lambda { RelaxDB.bulk_save!(c.new) }.should raise_error(RelaxDB::ValidationFailure)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should raise an exception if a document update conflict occurs on save" do
|
90
|
+
Atom.new(:_id => "a1").save!
|
91
|
+
lambda { RelaxDB.bulk_save! Atom.new(:_id => "a1") }.should raise_error(RelaxDB::UpdateConflict)
|
92
|
+
end
|
93
|
+
|
45
94
|
end
|
46
95
|
|
47
96
|
describe ".replicate_db" do
|
@@ -68,7 +117,7 @@ describe RelaxDB do
|
|
68
117
|
|
69
118
|
it "should load an arbitrary number of documents" do
|
70
119
|
a1, a2 = Atom.new.save, Atom.new.save
|
71
|
-
ar1, ar2 = RelaxDB.load a1._id, a2._id
|
120
|
+
ar1, ar2 = RelaxDB.load [a1._id, a2._id]
|
72
121
|
ar1.should == a1
|
73
122
|
ar2.should == a2
|
74
123
|
end
|
@@ -79,7 +128,7 @@ describe RelaxDB do
|
|
79
128
|
|
80
129
|
it "should return an array with correctly placed nils when given a list containing non existant doc ids" do
|
81
130
|
a1, a2 = Atom.new.save, Atom.new.save
|
82
|
-
res = RelaxDB.load nil, a1._id, nil, a2._id, nil
|
131
|
+
res = RelaxDB.load [nil, a1._id, nil, a2._id, nil]
|
83
132
|
res.should == [nil, a1, nil, a2, nil]
|
84
133
|
end
|
85
134
|
|
@@ -95,7 +144,7 @@ describe RelaxDB do
|
|
95
144
|
|
96
145
|
it "should load multiple documents" do
|
97
146
|
a1, a2 = Atom.new.save, Atom.new.save
|
98
|
-
ar1, ar2 = RelaxDB.load! a1._id, a2._id
|
147
|
+
ar1, ar2 = RelaxDB.load! [a1._id, a2._id]
|
99
148
|
ar1.should == a1
|
100
149
|
ar2.should == a2
|
101
150
|
end
|
@@ -109,7 +158,7 @@ describe RelaxDB do
|
|
109
158
|
it "should throw an exception if any of a list of doc ids is for a non-existant doc" do
|
110
159
|
a = Atom.new.save
|
111
160
|
lambda do
|
112
|
-
RelaxDB.load! nil, a._id
|
161
|
+
RelaxDB.load! [nil, a._id]
|
113
162
|
end.should raise_error(RelaxDB::NotFound)
|
114
163
|
end
|
115
164
|
|
data/spec/spec_models.rb
CHANGED
@@ -1,139 +1,150 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
#
|
2
|
+
# RSpec loads this file multiple times, thus breaking invocations like Document.has_one_rels
|
3
|
+
# The following clause ensures the tests pass, but perhaps Document should raise a warning
|
4
|
+
# if it's loaded more than once...
|
5
|
+
#
|
6
|
+
unless @spec_models_loaded
|
7
|
+
|
8
|
+
class Atom < RelaxDB::Document
|
9
|
+
end
|
3
10
|
|
4
|
-
class Initiative < RelaxDB::Document
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
11
|
+
class Initiative < RelaxDB::Document
|
12
|
+
property :x
|
13
|
+
attr_reader :foo
|
14
|
+
def initialize(*data)
|
15
|
+
super *data
|
16
|
+
@foo = :bar
|
17
|
+
end
|
10
18
|
end
|
11
|
-
end
|
12
19
|
|
13
|
-
class Primitives < RelaxDB::Document
|
20
|
+
class Primitives < RelaxDB::Document
|
14
21
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
22
|
+
property :str
|
23
|
+
property :num
|
24
|
+
property :true_bool
|
25
|
+
property :false_bool
|
26
|
+
property :created_at
|
27
|
+
property :empty
|
21
28
|
|
22
|
-
end
|
29
|
+
end
|
23
30
|
|
24
|
-
class BespokeReader < RelaxDB::Document
|
25
|
-
|
26
|
-
|
27
|
-
end
|
31
|
+
class BespokeReader < RelaxDB::Document
|
32
|
+
property :val
|
33
|
+
def val; @val + 5; end
|
34
|
+
end
|
28
35
|
|
29
|
-
class BespokeWriter < RelaxDB::Document
|
30
|
-
|
31
|
-
|
32
|
-
end
|
36
|
+
class BespokeWriter < RelaxDB::Document
|
37
|
+
property :val
|
38
|
+
def val=(v); @val = v - 10; end
|
39
|
+
end
|
33
40
|
|
34
|
-
class Letter < RelaxDB::Document
|
41
|
+
class Letter < RelaxDB::Document
|
35
42
|
|
36
|
-
|
37
|
-
|
43
|
+
property :letter
|
44
|
+
property :number
|
38
45
|
|
39
|
-
end
|
46
|
+
end
|
40
47
|
|
41
|
-
class Invite < RelaxDB::Document
|
48
|
+
class Invite < RelaxDB::Document
|
42
49
|
|
43
|
-
|
50
|
+
property :message
|
44
51
|
|
45
|
-
|
46
|
-
|
52
|
+
belongs_to :sender
|
53
|
+
belongs_to :recipient
|
47
54
|
|
48
|
-
end
|
55
|
+
end
|
49
56
|
|
50
|
-
class Item < RelaxDB::Document
|
57
|
+
class Item < RelaxDB::Document
|
51
58
|
|
52
|
-
|
53
|
-
|
59
|
+
property :name
|
60
|
+
belongs_to :user
|
54
61
|
|
55
|
-
end
|
62
|
+
end
|
56
63
|
|
57
|
-
class User < RelaxDB::Document
|
64
|
+
class User < RelaxDB::Document
|
58
65
|
|
59
|
-
|
60
|
-
|
66
|
+
property :name, :default => "u"
|
67
|
+
property :age
|
61
68
|
|
62
|
-
|
69
|
+
has_many :items, :class => "Item"
|
63
70
|
|
64
|
-
|
65
|
-
|
71
|
+
has_many :invites_received, :class => "Invite", :known_as => :recipient
|
72
|
+
has_many :invites_sent, :class => "Invite", :known_as => :sender
|
66
73
|
|
67
|
-
end
|
74
|
+
end
|
68
75
|
|
69
|
-
class Post < RelaxDB::Document
|
76
|
+
class Post < RelaxDB::Document
|
70
77
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
78
|
+
property :subject
|
79
|
+
property :content
|
80
|
+
property :created_at
|
81
|
+
property :viewed_at
|
75
82
|
|
76
|
-
end
|
83
|
+
end
|
77
84
|
|
78
|
-
class Rating < RelaxDB::Document
|
85
|
+
class Rating < RelaxDB::Document
|
79
86
|
|
80
|
-
|
81
|
-
|
87
|
+
property :stars, :default => 5
|
88
|
+
belongs_to :photo
|
82
89
|
|
83
|
-
end
|
90
|
+
end
|
84
91
|
|
85
|
-
class Photo < RelaxDB::Document
|
92
|
+
class Photo < RelaxDB::Document
|
86
93
|
|
87
|
-
|
94
|
+
property :name
|
88
95
|
|
89
|
-
|
96
|
+
has_one :rating
|
90
97
|
|
91
|
-
|
98
|
+
references_many :tags, :class => "Tag", :known_as => :photos
|
92
99
|
|
93
|
-
|
100
|
+
has_many :taggings, :class => "Tagging"
|
94
101
|
|
95
|
-
end
|
102
|
+
end
|
96
103
|
|
97
|
-
class Tag < RelaxDB::Document
|
104
|
+
class Tag < RelaxDB::Document
|
98
105
|
|
99
|
-
|
100
|
-
|
106
|
+
property :name
|
107
|
+
references_many :photos, :class => "Photo", :known_as => :tags
|
101
108
|
|
102
|
-
|
109
|
+
has_many :taggings, :class => "Tagging"
|
103
110
|
|
104
|
-
end
|
111
|
+
end
|
105
112
|
|
106
|
-
class Tagging < RelaxDB::Document
|
113
|
+
class Tagging < RelaxDB::Document
|
107
114
|
|
108
|
-
|
109
|
-
|
110
|
-
|
115
|
+
belongs_to :photo
|
116
|
+
belongs_to :tag
|
117
|
+
property :relevance
|
111
118
|
|
112
|
-
end
|
119
|
+
end
|
113
120
|
|
114
|
-
class MultiWordClass < RelaxDB::Document
|
115
|
-
|
116
|
-
|
117
|
-
end
|
121
|
+
class MultiWordClass < RelaxDB::Document
|
122
|
+
has_one :multi_word_child
|
123
|
+
has_many :multi_word_children, :class => "MultiWordChild"
|
124
|
+
end
|
118
125
|
|
119
|
-
class MultiWordChild < RelaxDB::Document
|
120
|
-
|
121
|
-
end
|
126
|
+
class MultiWordChild < RelaxDB::Document
|
127
|
+
belongs_to :multi_word_class
|
128
|
+
end
|
122
129
|
|
123
|
-
class TwitterUser < RelaxDB::Document
|
130
|
+
class TwitterUser < RelaxDB::Document
|
124
131
|
|
125
|
-
|
126
|
-
|
127
|
-
|
132
|
+
property :name
|
133
|
+
references_many :followers, :class => "User", :known_as => :leaders
|
134
|
+
references_many :leaders, :class => "User", :known_as => :followers
|
128
135
|
|
129
|
-
end
|
136
|
+
end
|
130
137
|
|
131
|
-
class Dysfunctional < RelaxDB::Document
|
132
|
-
|
133
|
-
|
134
|
-
end
|
138
|
+
class Dysfunctional < RelaxDB::Document
|
139
|
+
has_one :failure
|
140
|
+
has_many :failures, :class => "Failure"
|
141
|
+
end
|
142
|
+
|
143
|
+
class Failure < RelaxDB::Document
|
144
|
+
property :pathological, :validator => lambda { false }
|
145
|
+
belongs_to :dysfunctional
|
146
|
+
end
|
147
|
+
|
148
|
+
@spec_models_loaded = true
|
135
149
|
|
136
|
-
class Failure < RelaxDB::Document
|
137
|
-
property :pathological, :validator => lambda { false }
|
138
|
-
belongs_to :dysfunctional
|
139
150
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: paulcarey-relaxdb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Carey
|
@@ -9,7 +9,7 @@ autorequire: relaxdb
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-01-
|
12
|
+
date: 2009-01-18 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|