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,18 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
2
|
+
require 'relaxdb'
|
3
|
+
require File.dirname(__FILE__) + '/../../spec/spec_models.rb'
|
4
|
+
|
5
|
+
RelaxDB.configure :host => "localhost", :port => 5984
|
6
|
+
RelaxDB.delete_db "relaxdb_spec" rescue :ok
|
7
|
+
RelaxDB.use_db "relaxdb_spec"
|
8
|
+
|
9
|
+
a1 = Atom.new.save!
|
10
|
+
a1_dup = a1.dup
|
11
|
+
a1.save!
|
12
|
+
begin
|
13
|
+
RelaxDB.bulk_save! a1_dup
|
14
|
+
puts "Atomic bulk_save _not_ supported"
|
15
|
+
rescue RelaxDB::UpdateConflict
|
16
|
+
puts "Atomic bulk_save supported"
|
17
|
+
end
|
18
|
+
|
data/lib/more/grapher.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
#
|
4
|
+
# The GraphCreator uses dot to create a graphical model of an entire CouchDB database
|
5
|
+
# It probably only makes sense to run it on a database of a limited size
|
6
|
+
# The created graphs can be very useful for exploring relationships
|
7
|
+
# Run ruby scratch/grapher_demo.rb for an example
|
8
|
+
#
|
9
|
+
class GraphCreator
|
10
|
+
|
11
|
+
def self.create
|
12
|
+
system "mkdir -p graphs"
|
13
|
+
|
14
|
+
data = JSON.parse(RelaxDB.db.get("_all_docs").body)
|
15
|
+
all_ids = data["rows"].map { |r| r["id"] }
|
16
|
+
all_ids = all_ids.reject { |id| id =~ /_/ }
|
17
|
+
|
18
|
+
dot = "digraph G { \nrankdir=LR;\nnode [shape=record];\n"
|
19
|
+
all_ids.each do |id|
|
20
|
+
doc = RelaxDB.load(id)
|
21
|
+
atts = "#{doc.class}\\l|"
|
22
|
+
doc.properties.each do |prop|
|
23
|
+
# we don't care about the revision
|
24
|
+
next if prop == :_rev
|
25
|
+
|
26
|
+
prop_val = doc.instance_variable_get("@#{prop}".to_sym)
|
27
|
+
atts << "#{prop}\\l#{prop_val}|" if prop_val
|
28
|
+
end
|
29
|
+
atts = atts[0, atts.length-1]
|
30
|
+
|
31
|
+
dot << %Q%#{doc._id} [ label ="#{atts}"];\n%
|
32
|
+
|
33
|
+
doc.class.belongs_to_rels.each do |relationship, opts|
|
34
|
+
id = doc.instance_variable_get("@#{relationship}_id".to_sym)
|
35
|
+
dot << %Q%#{id} -> #{doc._id} [ label = "#{relationship}"];\n% if id
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
dot << "}"
|
40
|
+
|
41
|
+
File.open("graphs/data.dot", "w") { |f| f.write(dot) }
|
42
|
+
|
43
|
+
system "dot -Tpng -o graphs/all_docs.png graphs/data.dot"
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/lib/relaxdb.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'extlib'
|
3
|
+
require 'json'
|
4
|
+
require 'uuid'
|
5
|
+
|
6
|
+
require 'cgi'
|
7
|
+
require 'net/http'
|
8
|
+
require 'logger'
|
9
|
+
require 'parsedate'
|
10
|
+
require 'pp'
|
11
|
+
require 'tempfile'
|
12
|
+
|
13
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
14
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
15
|
+
|
16
|
+
require 'relaxdb/validators'
|
17
|
+
|
18
|
+
begin
|
19
|
+
gem 'taf2-curb'
|
20
|
+
require 'curb'
|
21
|
+
require 'relaxdb/taf2_curb_server'
|
22
|
+
rescue LoadError
|
23
|
+
require 'relaxdb/net_http_server'
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'relaxdb/all_delegator'
|
27
|
+
require 'relaxdb/belongs_to_proxy'
|
28
|
+
require 'relaxdb/design_doc'
|
29
|
+
require 'relaxdb/document'
|
30
|
+
require 'relaxdb/extlib'
|
31
|
+
require 'relaxdb/has_many_proxy'
|
32
|
+
require 'relaxdb/has_one_proxy'
|
33
|
+
require 'relaxdb/migration'
|
34
|
+
require 'relaxdb/paginate_params'
|
35
|
+
require 'relaxdb/paginator'
|
36
|
+
require 'relaxdb/query'
|
37
|
+
require 'relaxdb/references_many_proxy'
|
38
|
+
require 'relaxdb/relaxdb'
|
39
|
+
require 'relaxdb/server'
|
40
|
+
require 'relaxdb/uuid_generator'
|
41
|
+
require 'relaxdb/view_object'
|
42
|
+
require 'relaxdb/view_result'
|
43
|
+
require 'relaxdb/view_uploader'
|
44
|
+
require 'relaxdb/views'
|
45
|
+
require 'more/grapher.rb'
|
46
|
+
|
47
|
+
require 'relaxdb/migration_version'
|
48
|
+
|
49
|
+
module RelaxDB
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
#
|
4
|
+
# The AllDelegator allows clients to query CouchDB in a natural way
|
5
|
+
# FooDoc.all - returns all docs in CouchDB of type FooDoc
|
6
|
+
# FooDoc.all.size - issues a query to a reduce function that returns the total number of docs for that class
|
7
|
+
# FooDoc.all.destroy! - TODO - better description
|
8
|
+
#
|
9
|
+
class AllDelegator < Delegator
|
10
|
+
|
11
|
+
def initialize(class_name, params)
|
12
|
+
super([])
|
13
|
+
@class_name = class_name
|
14
|
+
@params = params
|
15
|
+
end
|
16
|
+
|
17
|
+
def __getobj__
|
18
|
+
unless @objs
|
19
|
+
@objs = RelaxDB.rf_view "#{@class_name}_all", @params
|
20
|
+
end
|
21
|
+
@objs
|
22
|
+
end
|
23
|
+
|
24
|
+
def size
|
25
|
+
size = RelaxDB.view "#{@class_name}_all", :reduce => true
|
26
|
+
size || 0
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO: destroy in a bulk_save if feasible
|
30
|
+
def destroy!
|
31
|
+
__getobj__
|
32
|
+
@objs.each do |o|
|
33
|
+
# A reload is required for deleting objects with a self referential references_many relationship
|
34
|
+
# This makes all.destroy! very slow. Change if needed
|
35
|
+
# obj = RelaxDB.load(o._id)
|
36
|
+
# obj.destroy!
|
37
|
+
|
38
|
+
o.destroy!
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class BelongsToProxy
|
4
|
+
|
5
|
+
attr_reader :target
|
6
|
+
|
7
|
+
def initialize(client, relationship)
|
8
|
+
@client = client
|
9
|
+
@relationship = relationship
|
10
|
+
@target = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def target
|
14
|
+
return @target if @target
|
15
|
+
|
16
|
+
id = @client.instance_variable_get("@#{@relationship}_id")
|
17
|
+
@target = RelaxDB.load(id) if id
|
18
|
+
end
|
19
|
+
|
20
|
+
def target=(new_target)
|
21
|
+
id = new_target ? new_target._id : nil
|
22
|
+
@client.instance_variable_set("@#{@relationship}_id", id)
|
23
|
+
|
24
|
+
@target = new_target
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class DesignDocument
|
4
|
+
|
5
|
+
attr_reader :data
|
6
|
+
|
7
|
+
def initialize(design_doc_name, data)
|
8
|
+
@design_doc_name = design_doc_name
|
9
|
+
@data = data
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_map_view(view_name, function)
|
13
|
+
add_view(view_name, "map", function)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_reduce_view(view_name, function)
|
17
|
+
add_view(view_name, "reduce", function)
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_validation_func(function)
|
21
|
+
@data["validate_doc_update"] = function
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_view(view_name, type, function)
|
26
|
+
@data["views"] ||= {}
|
27
|
+
@data["views"][view_name] ||= {}
|
28
|
+
@data["views"][view_name][type] = function
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def save
|
33
|
+
database = RelaxDB.db
|
34
|
+
resp = database.put(@data["_id"], @data.to_json)
|
35
|
+
@data["_rev"] = JSON.parse(resp.body)["rev"]
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.get(design_doc_name)
|
40
|
+
begin
|
41
|
+
database = RelaxDB.db
|
42
|
+
resp = database.get("_design/#{design_doc_name}")
|
43
|
+
DesignDocument.new(design_doc_name, JSON.parse(resp.body))
|
44
|
+
rescue HTTP_404
|
45
|
+
DesignDocument.new(design_doc_name, {"_id" => "_design/#{design_doc_name}"} )
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def destroy!
|
50
|
+
# Implicitly prevent the object from being resaved by failing to update its revision
|
51
|
+
RelaxDB.db.delete("#{@data["_id"]}?rev=#{@data["_rev"]}")
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,600 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class Document
|
4
|
+
|
5
|
+
include RelaxDB::Validators
|
6
|
+
|
7
|
+
# Used to store validation messages
|
8
|
+
attr_accessor :errors
|
9
|
+
|
10
|
+
# A call issued to save_all will save this object and the
|
11
|
+
# contents of the save_list. This allows secondary object to
|
12
|
+
# be saved at the same time as this object.
|
13
|
+
attr_accessor :save_list
|
14
|
+
|
15
|
+
# Attribute symbols added to this list won't be validated on save
|
16
|
+
attr_accessor :validation_skip_list
|
17
|
+
|
18
|
+
class_inheritable_accessor :properties, :reader => true
|
19
|
+
self.properties = []
|
20
|
+
|
21
|
+
class_inheritable_accessor :derived_prop_writers
|
22
|
+
self.derived_prop_writers = {}
|
23
|
+
|
24
|
+
class_inheritable_accessor :__view_by_list__
|
25
|
+
self.__view_by_list__ = []
|
26
|
+
|
27
|
+
class_inheritable_accessor :belongs_to_rels, :reader => true
|
28
|
+
self.belongs_to_rels = {}
|
29
|
+
|
30
|
+
def self.property(prop, opts={})
|
31
|
+
properties << prop
|
32
|
+
|
33
|
+
define_method(prop) do
|
34
|
+
instance_variable_get("@#{prop}".to_sym)
|
35
|
+
end
|
36
|
+
|
37
|
+
define_method("#{prop}=") do |val|
|
38
|
+
instance_variable_set("@#{prop}".to_sym, val)
|
39
|
+
end
|
40
|
+
|
41
|
+
if opts[:default]
|
42
|
+
define_method("set_default_#{prop}") do
|
43
|
+
default = opts[:default]
|
44
|
+
default = default.is_a?(Proc) ? default.call : default
|
45
|
+
instance_variable_set("@#{prop}".to_sym, default)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if opts[:validator]
|
50
|
+
create_validator(prop, opts[:validator])
|
51
|
+
end
|
52
|
+
|
53
|
+
if opts[:validation_msg]
|
54
|
+
create_validation_msg(prop, opts[:validation_msg])
|
55
|
+
end
|
56
|
+
|
57
|
+
if opts[:derived]
|
58
|
+
add_derived_prop(prop, opts[:derived])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
property :_id
|
63
|
+
property :_rev
|
64
|
+
property :_conflicts
|
65
|
+
|
66
|
+
def self.create_validator(att, v)
|
67
|
+
method_name = "validate_#{att}"
|
68
|
+
if v.is_a? Proc
|
69
|
+
v.arity == 1 ?
|
70
|
+
define_method(method_name) { |att_val| v.call(att_val) } :
|
71
|
+
define_method(method_name) { |att_val| v.call(att_val, self) }
|
72
|
+
elsif instance_methods.include? "validator_#{v}"
|
73
|
+
define_method(method_name) { |att_val| send("validator_#{v}", att_val, self) }
|
74
|
+
else
|
75
|
+
define_method(method_name) { |att_val| send(v, att_val) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.create_validation_msg(att, validation_msg)
|
80
|
+
if validation_msg.is_a?(Proc)
|
81
|
+
validation_msg.arity == 1 ?
|
82
|
+
define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val) } :
|
83
|
+
define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val, self) }
|
84
|
+
else
|
85
|
+
define_method("#{att}_validation_msg") { validation_msg }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# See derived_properties_spec.rb for usage
|
90
|
+
def self.add_derived_prop(prop, deriver)
|
91
|
+
source, writer = deriver[0], deriver[1]
|
92
|
+
derived_prop_writers[source] ||= {}
|
93
|
+
derived_prop_writers[source][prop] = writer
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# The rationale for rescuing the send below is that the lambda for a derived
|
98
|
+
# property shouldn't need to concern itself with checking the validity of
|
99
|
+
# the underlying property. Nor, IMO, should clients be exposed to the
|
100
|
+
# possibility of a writer raising an exception.
|
101
|
+
#
|
102
|
+
def write_derived_props(source)
|
103
|
+
writers = self.class.derived_prop_writers
|
104
|
+
writers = writers && writers[source]
|
105
|
+
if writers
|
106
|
+
writers.each do |prop, writer|
|
107
|
+
current_val = send(prop)
|
108
|
+
begin
|
109
|
+
send("#{prop}=", writer.call(current_val, self))
|
110
|
+
rescue => e
|
111
|
+
RelaxDB.logger.error "Deriving #{prop} from #{source} raised #{e}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def initialize(hash={})
|
118
|
+
unless hash["_id"]
|
119
|
+
self._id = UuidGenerator.uuid
|
120
|
+
end
|
121
|
+
|
122
|
+
@errors = Errors.new
|
123
|
+
@save_list = []
|
124
|
+
@validation_skip_list = []
|
125
|
+
|
126
|
+
# Set default properties if this object isn't being loaded from CouchDB
|
127
|
+
unless hash["_rev"]
|
128
|
+
properties.each do |prop|
|
129
|
+
if methods.include?("set_default_#{prop}")
|
130
|
+
send("set_default_#{prop}")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
@set_derived_props = hash["_rev"] ? false : true
|
136
|
+
set_attributes(hash)
|
137
|
+
@set_derived_props = true
|
138
|
+
end
|
139
|
+
|
140
|
+
def set_attributes(data)
|
141
|
+
data.each do |key, val|
|
142
|
+
# Only set instance variables on creation - object references are resolved on demand
|
143
|
+
|
144
|
+
# If the variable name ends in _at, _on or _date try to convert it to a Time
|
145
|
+
if [/_at$/, /_on$/, /_date$/, /_time$/].inject(nil) { |i, r| i ||= (key =~ r) }
|
146
|
+
val = Time.parse(val).utc rescue val
|
147
|
+
end
|
148
|
+
|
149
|
+
# Ignore param keys that don't have a corresponding writer
|
150
|
+
# This allows us to comfortably accept a hash containing superflous data
|
151
|
+
# such as a params hash in a controller
|
152
|
+
send("#{key}=".to_sym, val) if methods.include? "#{key}="
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def inspect
|
157
|
+
s = "#<#{self.class}:#{self.object_id}"
|
158
|
+
properties.each do |prop|
|
159
|
+
prop_val = instance_variable_get("@#{prop}".to_sym)
|
160
|
+
s << ", #{prop}: #{prop_val.inspect}" if prop_val
|
161
|
+
end
|
162
|
+
self.class.belongs_to_rels.each do |relationship, opts|
|
163
|
+
id = instance_variable_get("@#{relationship}_id".to_sym)
|
164
|
+
s << ", #{relationship}_id: #{id}" if id
|
165
|
+
end
|
166
|
+
s << ", errors: #{errors.inspect}" unless errors.empty?
|
167
|
+
s << ", save_list: #{save_list.map { |o| o.inspect }.join ", " }" unless save_list.empty?
|
168
|
+
s << ">"
|
169
|
+
end
|
170
|
+
|
171
|
+
alias_method :to_s, :inspect
|
172
|
+
|
173
|
+
def to_json
|
174
|
+
data = {}
|
175
|
+
self.class.belongs_to_rels.each do |relationship, opts|
|
176
|
+
id = instance_variable_get("@#{relationship}_id".to_sym)
|
177
|
+
data["#{relationship}_id"] = id if id
|
178
|
+
end
|
179
|
+
properties.each do |prop|
|
180
|
+
prop_val = instance_variable_get("@#{prop}".to_sym)
|
181
|
+
data["#{prop}"] = prop_val if prop_val
|
182
|
+
end
|
183
|
+
data["errors"] = errors unless errors.empty?
|
184
|
+
data["relaxdb_class"] = self.class.name
|
185
|
+
data.to_json
|
186
|
+
end
|
187
|
+
|
188
|
+
# Not yet sure of final implemention for hooks - may lean more towards DM than AR
|
189
|
+
def save
|
190
|
+
if pre_save && save_to_couch
|
191
|
+
after_save
|
192
|
+
self
|
193
|
+
else
|
194
|
+
false
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def save_to_couch
|
199
|
+
begin
|
200
|
+
resp = RelaxDB.db.put(_id, to_json)
|
201
|
+
self._rev = JSON.parse(resp.body)["rev"]
|
202
|
+
rescue HTTP_409
|
203
|
+
conflicted
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def conflicted
|
209
|
+
@update_conflict = true
|
210
|
+
on_update_conflict
|
211
|
+
end
|
212
|
+
|
213
|
+
def on_update_conflict
|
214
|
+
# override with any behaviour you want to happen when
|
215
|
+
# CouchDB returns DocumentConflict on an attempt to save
|
216
|
+
end
|
217
|
+
|
218
|
+
def update_conflict?
|
219
|
+
@update_conflict
|
220
|
+
end
|
221
|
+
|
222
|
+
def pre_save
|
223
|
+
set_timestamps
|
224
|
+
return false unless validates?
|
225
|
+
return false unless before_save
|
226
|
+
true
|
227
|
+
end
|
228
|
+
|
229
|
+
def post_save
|
230
|
+
after_save
|
231
|
+
end
|
232
|
+
|
233
|
+
# save_all and save_all! are untested
|
234
|
+
def save_all
|
235
|
+
RelaxDB.bulk_save self, *save_list
|
236
|
+
end
|
237
|
+
|
238
|
+
def save_all!
|
239
|
+
RelaxDB.bulk_save! self, *save_list
|
240
|
+
end
|
241
|
+
|
242
|
+
def save!
|
243
|
+
if save
|
244
|
+
self
|
245
|
+
elsif update_conflict?
|
246
|
+
raise UpdateConflict, self
|
247
|
+
else
|
248
|
+
raise ValidationFailure, self.errors.to_json
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def validates?
|
253
|
+
props = properties - validation_skip_list
|
254
|
+
prop_vals = props.map { |prop| instance_variable_get("@#{prop}") }
|
255
|
+
|
256
|
+
rels = self.class.belongs_to_rels.keys - validation_skip_list
|
257
|
+
rel_vals = rels.map { |rel| instance_variable_get("@#{rel}_id") }
|
258
|
+
|
259
|
+
att_names = props + rels
|
260
|
+
att_vals = prop_vals + rel_vals
|
261
|
+
|
262
|
+
total_success = true
|
263
|
+
att_names.each_index do |i|
|
264
|
+
att_name, att_val = att_names[i], att_vals[i]
|
265
|
+
if methods.include? "validate_#{att_name}"
|
266
|
+
total_success &= validate_att(att_name, att_val)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
total_success
|
271
|
+
end
|
272
|
+
alias_method :validate, :validates?
|
273
|
+
|
274
|
+
def validate_att(att_name, att_val)
|
275
|
+
begin
|
276
|
+
success = send("validate_#{att_name}", att_val)
|
277
|
+
rescue => e
|
278
|
+
RelaxDB.logger.warn "Validating #{att_name} with #{att_val} raised #{e}"
|
279
|
+
succes = false
|
280
|
+
end
|
281
|
+
|
282
|
+
unless success
|
283
|
+
if methods.include? "#{att_name}_validation_msg"
|
284
|
+
begin
|
285
|
+
@errors[att_name] = send("#{att_name}_validation_msg", att_val)
|
286
|
+
rescue => e
|
287
|
+
RelaxDB.logger.warn "Validation_msg for #{att_name} with #{att_val} raised #{e}"
|
288
|
+
@errors[att_name] = "validation_msg_exception:invalid:#{att_val}"
|
289
|
+
end
|
290
|
+
elsif @errors[att_name].nil?
|
291
|
+
# Only set a validation message if a validator hasn't already set one
|
292
|
+
@errors[att_name] = "invalid:#{att_val}"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
success
|
296
|
+
end
|
297
|
+
|
298
|
+
def new_document?
|
299
|
+
@_rev.nil?
|
300
|
+
end
|
301
|
+
alias_method :new_record?, :new_document?
|
302
|
+
alias_method :unsaved?, :new_document?
|
303
|
+
|
304
|
+
def to_param
|
305
|
+
self._id
|
306
|
+
end
|
307
|
+
alias_method :id, :to_param
|
308
|
+
|
309
|
+
def set_timestamps
|
310
|
+
now = Time.now
|
311
|
+
if new_document? && respond_to?(:created_at)
|
312
|
+
# Don't override it if it's already been set
|
313
|
+
@created_at = now if @created_at.nil?
|
314
|
+
end
|
315
|
+
|
316
|
+
@updated_at = now if respond_to?(:updated_at)
|
317
|
+
end
|
318
|
+
|
319
|
+
def create_or_get_proxy(klass, relationship, opts=nil)
|
320
|
+
proxy_sym = "@proxy_#{relationship}".to_sym
|
321
|
+
proxy = instance_variable_get(proxy_sym)
|
322
|
+
unless proxy
|
323
|
+
proxy = opts ? klass.new(self, relationship, opts) : klass.new(self, relationship)
|
324
|
+
instance_variable_set(proxy_sym, proxy)
|
325
|
+
end
|
326
|
+
proxy
|
327
|
+
end
|
328
|
+
|
329
|
+
# Returns true if CouchDB considers other to be the same as self
|
330
|
+
def ==(other)
|
331
|
+
other && _id == other._id
|
332
|
+
end
|
333
|
+
|
334
|
+
# If you're using this method, read the specs and make sure you understand
|
335
|
+
# how it can be used and how it shouldn't be used
|
336
|
+
def self.references_many(relationship, opts={})
|
337
|
+
# Treat the representation as a standard property
|
338
|
+
properties << relationship
|
339
|
+
|
340
|
+
# Keep track of the relationship so peers can be disassociated on destroy
|
341
|
+
@references_many_rels ||= []
|
342
|
+
@references_many_rels << relationship
|
343
|
+
|
344
|
+
id_arr_sym = "@#{relationship}".to_sym
|
345
|
+
|
346
|
+
if RelaxDB.create_views?
|
347
|
+
target_class = opts[:class]
|
348
|
+
relationship_as_viewed_by_target = opts[:known_as].to_s
|
349
|
+
ViewCreator.references_many(self.name, relationship, target_class, relationship_as_viewed_by_target).save
|
350
|
+
end
|
351
|
+
|
352
|
+
define_method(relationship) do
|
353
|
+
instance_variable_set(id_arr_sym, []) unless instance_variable_defined? id_arr_sym
|
354
|
+
create_or_get_proxy(ReferencesManyProxy, relationship, opts)
|
355
|
+
end
|
356
|
+
|
357
|
+
define_method("#{relationship}_ids") do
|
358
|
+
instance_variable_set(id_arr_sym, []) unless instance_variable_defined? id_arr_sym
|
359
|
+
instance_variable_get(id_arr_sym)
|
360
|
+
end
|
361
|
+
|
362
|
+
define_method("#{relationship}=") do |val|
|
363
|
+
# Don't invoke this method unless you know what you're doing
|
364
|
+
instance_variable_set(id_arr_sym, val)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def self.references_many_rels
|
369
|
+
@references_many_rels ||= []
|
370
|
+
end
|
371
|
+
|
372
|
+
def self.has_many(relationship, opts={})
|
373
|
+
@has_many_rels ||= []
|
374
|
+
@has_many_rels << relationship
|
375
|
+
|
376
|
+
if RelaxDB.create_views?
|
377
|
+
target_class = opts[:class] || relationship.to_s.singularize.camel_case
|
378
|
+
relationship_as_viewed_by_target = (opts[:known_as] || self.name.snake_case).to_s
|
379
|
+
ViewCreator.has_n(self.name, relationship, target_class, relationship_as_viewed_by_target).save
|
380
|
+
end
|
381
|
+
|
382
|
+
define_method(relationship) do
|
383
|
+
create_or_get_proxy(HasManyProxy, relationship, opts)
|
384
|
+
end
|
385
|
+
|
386
|
+
define_method("#{relationship}=") do |children|
|
387
|
+
create_or_get_proxy(HasManyProxy, relationship, opts).children = children
|
388
|
+
write_derived_props(relationship) if @set_derived_props
|
389
|
+
children
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def self.has_many_rels
|
394
|
+
# Don't force clients to check its instantiated
|
395
|
+
@has_many_rels ||= []
|
396
|
+
end
|
397
|
+
|
398
|
+
def self.has_one(relationship)
|
399
|
+
@has_one_rels ||= []
|
400
|
+
@has_one_rels << relationship
|
401
|
+
|
402
|
+
if RelaxDB.create_views?
|
403
|
+
target_class = relationship.to_s.camel_case
|
404
|
+
relationship_as_viewed_by_target = self.name.snake_case
|
405
|
+
ViewCreator.has_n(self.name, relationship, target_class, relationship_as_viewed_by_target).save
|
406
|
+
end
|
407
|
+
|
408
|
+
define_method(relationship) do
|
409
|
+
create_or_get_proxy(HasOneProxy, relationship).target
|
410
|
+
end
|
411
|
+
|
412
|
+
define_method("#{relationship}=") do |new_target|
|
413
|
+
create_or_get_proxy(HasOneProxy, relationship).target = new_target
|
414
|
+
write_derived_props(relationship) if @set_derived_props
|
415
|
+
new_target
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
def self.has_one_rels
|
420
|
+
@has_one_rels ||= []
|
421
|
+
end
|
422
|
+
|
423
|
+
def self.belongs_to(relationship, opts={})
|
424
|
+
belongs_to_rels[relationship] = opts
|
425
|
+
|
426
|
+
define_method(relationship) do
|
427
|
+
create_or_get_proxy(BelongsToProxy, relationship).target
|
428
|
+
end
|
429
|
+
|
430
|
+
define_method("#{relationship}=") do |new_target|
|
431
|
+
create_or_get_proxy(BelongsToProxy, relationship).target = new_target
|
432
|
+
write_derived_props(relationship) if @set_derived_props
|
433
|
+
end
|
434
|
+
|
435
|
+
# Allows all writers to be invoked from the hash passed to initialize
|
436
|
+
define_method("#{relationship}_id=") do |id|
|
437
|
+
instance_variable_set("@#{relationship}_id".to_sym, id)
|
438
|
+
write_derived_props(relationship) if @set_derived_props
|
439
|
+
id
|
440
|
+
end
|
441
|
+
|
442
|
+
define_method("#{relationship}_id") do
|
443
|
+
instance_variable_get("@#{relationship}_id")
|
444
|
+
end
|
445
|
+
|
446
|
+
create_validator(relationship, opts[:validator]) if opts[:validator]
|
447
|
+
|
448
|
+
# Untested below
|
449
|
+
create_validation_msg(relationship, opts[:validation_msg]) if opts[:validation_msg]
|
450
|
+
end
|
451
|
+
|
452
|
+
class << self
|
453
|
+
alias_method :references, :belongs_to
|
454
|
+
end
|
455
|
+
|
456
|
+
self.belongs_to_rels = {}
|
457
|
+
|
458
|
+
def self.all_relationships
|
459
|
+
belongs_to_rels + has_one_rels + has_many_rels + references_many_rels
|
460
|
+
end
|
461
|
+
|
462
|
+
def self.all params = {}
|
463
|
+
AllDelegator.new self.name, params
|
464
|
+
end
|
465
|
+
|
466
|
+
# destroy! nullifies all relationships with peers and children before deleting
|
467
|
+
# itself in CouchDB
|
468
|
+
# The nullification and deletion are not performed in a transaction
|
469
|
+
#
|
470
|
+
# TODO: Current implemention may be inappropriate - causing CouchDB to try to JSON
|
471
|
+
# encode undefined. Ensure nil is serialized? See has_many_spec#should nullify its child relationships
|
472
|
+
def destroy!
|
473
|
+
self.class.references_many_rels.each do |rel|
|
474
|
+
send(rel).clear
|
475
|
+
end
|
476
|
+
|
477
|
+
self.class.has_many_rels.each do |rel|
|
478
|
+
send(rel).clear
|
479
|
+
end
|
480
|
+
|
481
|
+
self.class.has_one_rels.each do |rel|
|
482
|
+
send("#{rel}=".to_sym, nil)
|
483
|
+
end
|
484
|
+
|
485
|
+
# Implicitly prevent the object from being resaved by failing to update its revision
|
486
|
+
RelaxDB.db.delete("#{_id}?rev=#{_rev}")
|
487
|
+
self
|
488
|
+
end
|
489
|
+
|
490
|
+
#
|
491
|
+
# Callbacks - define these in a module and mix'em'in ?
|
492
|
+
#
|
493
|
+
def self.before_save(callback)
|
494
|
+
before_save_callbacks << callback
|
495
|
+
end
|
496
|
+
|
497
|
+
def self.before_save_callbacks
|
498
|
+
@before_save ||= []
|
499
|
+
end
|
500
|
+
|
501
|
+
def before_save
|
502
|
+
self.class.before_save_callbacks.each do |callback|
|
503
|
+
resp = callback.is_a?(Proc) ? callback.call(self) : send(callback)
|
504
|
+
if resp == false
|
505
|
+
errors[:before_save] = :failed
|
506
|
+
return false
|
507
|
+
end
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
def self.after_save(callback)
|
512
|
+
after_save_callbacks << callback
|
513
|
+
end
|
514
|
+
|
515
|
+
def self.after_save_callbacks
|
516
|
+
@after_save_callbacks ||= []
|
517
|
+
end
|
518
|
+
|
519
|
+
def after_save
|
520
|
+
self.class.after_save_callbacks.each do |callback|
|
521
|
+
callback.is_a?(Proc) ? callback.call(self) : send(callback)
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
#
|
526
|
+
# Creates the corresponding view and stores it in CouchDB
|
527
|
+
# Adds by_ and paginate_by_ methods to the class
|
528
|
+
#
|
529
|
+
def self.view_by *atts
|
530
|
+
opts = atts.last.is_a?(Hash) ? atts.pop : {}
|
531
|
+
__view_by_list__ << atts
|
532
|
+
|
533
|
+
if RelaxDB.create_views?
|
534
|
+
ViewCreator.by_att_list([self.name], *atts).save
|
535
|
+
end
|
536
|
+
|
537
|
+
by_name = "by_#{atts.join "_and_"}"
|
538
|
+
meta_class.instance_eval do
|
539
|
+
define_method by_name do |*params|
|
540
|
+
view_name = "#{self.name}_#{by_name}"
|
541
|
+
if params.empty?
|
542
|
+
res = RelaxDB.rf_view view_name, opts
|
543
|
+
elsif params[0].is_a? Hash
|
544
|
+
res = RelaxDB.rf_view view_name, opts.merge(params[0])
|
545
|
+
else
|
546
|
+
res = RelaxDB.rf_view(view_name, :key => params[0]).first
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
paginate_by_name = "paginate_by_#{atts.join "_and_"}"
|
552
|
+
meta_class.instance_eval do
|
553
|
+
define_method paginate_by_name do |params|
|
554
|
+
view_name = "#{self.name}_#{by_name}"
|
555
|
+
params[:attributes] = atts
|
556
|
+
params = opts.merge params
|
557
|
+
RelaxDB.paginate_view view_name, params
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
# Create a view allowing all instances of a particular class to be retreived
|
563
|
+
def self.create_all_by_class_view
|
564
|
+
if RelaxDB.create_views?
|
565
|
+
view = ViewCreator.all
|
566
|
+
view.save unless view.exists?
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
def self.inherited subclass
|
571
|
+
chain = subclass.up_chain
|
572
|
+
while k = chain.pop
|
573
|
+
k.create_views chain
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
def self.up_chain
|
578
|
+
k = self
|
579
|
+
kls = [k]
|
580
|
+
kls << k while ((k = k.superclass) != RelaxDB::Document)
|
581
|
+
kls
|
582
|
+
end
|
583
|
+
|
584
|
+
def self.create_views chain
|
585
|
+
# Capture the inheritance hierarchy of this class
|
586
|
+
@hierarchy ||= [self]
|
587
|
+
@hierarchy += chain
|
588
|
+
@hierarchy.uniq!
|
589
|
+
|
590
|
+
if RelaxDB.create_views?
|
591
|
+
ViewCreator.all(@hierarchy).save
|
592
|
+
__view_by_list__.each do |atts|
|
593
|
+
ViewCreator.by_att_list(@hierarchy, *atts).save
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
end
|
599
|
+
|
600
|
+
end
|