relaxdb 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|