thingtank 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ require_relative 'marriage_improvement.rb'
2
+
3
+ class Dead
4
+
5
+ # all callbacks of roles are called and defined like corresponding callbacks of the doc
6
+ before_save do
7
+ if _doc.is?(Spouse) && _doc['married_state'] == 'married'
8
+ Spouse.get(_doc.last_role(Married, 'married').spouse).widowed(self["date_of_death"])
9
+ end
10
+ true
11
+ end
12
+
13
+ end
14
+
15
+ class Person
16
+ def dies(date)
17
+ _doc.is(Dead) do |d|
18
+ d.date_of_death = date
19
+ end
20
+ end
21
+ end
22
+
23
+ class Married
24
+ def widow(date)
25
+ self["state"] = 'widowed'
26
+ self['end'] = date
27
+ _doc.save
28
+ end
29
+ end
30
+
31
+ class Spouse
32
+ def widowed(date)
33
+ self["married_state"] = 'widowed'
34
+ married { |m| m.widow(date) }
35
+ end
36
+ end
37
+
38
+
39
+ def test_second_marriage()
40
+ julius = test_improved_marriage()
41
+ julius.as(Person).marry "68-65 BC", :gender => "f", :name => 'Pompeia'
42
+
43
+ assert_equal 2, julius["married"].size
44
+ assert_equal 'Cornelia', Person.get(julius["married"].first["spouse"]).name
45
+ assert_equal 'Pompeia', Person.get(julius["married"].last["spouse"]).name
46
+ assert_equal 'Pompeia', julius.has(Spouse).name
47
+ assert_equal 'married', julius["married"].first["state"]
48
+ assert_equal 'married', julius["married"].last["state"]
49
+
50
+ return julius
51
+ end
52
+
53
+ def test_second_marriage_after_conny_died()
54
+ julius = test_bear_julius()
55
+ conny = create(:gender => "f", :name => 'Cornelia')
56
+ conny.is(Person)
57
+ julius.as(Person).marry "84 BC", conny
58
+ julius.save
59
+ julius.reload
60
+
61
+ conny = load(julius["married"]["spouse"])
62
+ conny.save
63
+ conny.reload
64
+ conny.as(Person).dies "68-65 BC"
65
+ conny.save
66
+
67
+ julius.reload
68
+
69
+ julius.as(Person).marry "68-65 BC", :gender => "f", :name => 'Pompeia'
70
+
71
+ assert_equal 2, julius["married"].size
72
+ assert_equal 'Cornelia', Person.get(julius["married"].first["spouse"]).name
73
+ assert_equal 'widowed', julius["married"].first["state"]
74
+ assert_equal 'Pompeia', Person.get(julius["married"].last["spouse"]).name
75
+ assert_equal 'married', julius["married"].last["state"]
76
+ assert_equal 'Pompeia', julius.has(Spouse).name
77
+ end
78
+
@@ -0,0 +1,17 @@
1
+ # https://github.com/couchrest/couchrest_model/blob/master/lib/couchrest/model/designs/view.rb overwritten
2
+
3
+ class CouchRest::Model::Designs::DesignMapper
4
+
5
+ # generate a view to show only ThingTanks of a certain role, define them all in a ThingTank subclass (not in a role)
6
+ def role_view(klass, name, opts={})
7
+ name = "#{klass.to_s.downcase}_#{name}"
8
+ opts ||= {}
9
+ opts[:guards] ||= []
10
+ # there is no "inArray" like function in couchdb, see http://stackoverflow.com/questions/3740464/i-have-to-write-every-function-i-need-for-couchdb
11
+ opts[:guards] << "((doc['roles'] !== undefined) && (function (item,arr) { for(p=0;p<arr.length;p++) if (item == arr[p]) return true; return false;})('#{klass.to_s}',doc['roles']))"
12
+ view(name, opts)
13
+ end
14
+
15
+ end
16
+
17
+
@@ -0,0 +1,32 @@
1
+ class ThingTank
2
+ module Callbacks
3
+ def self.included(klass)
4
+ klass.class_eval do
5
+
6
+ before_validation do
7
+ # update_attributes seems to be intentionally broken for mass_assign_any_attribute, see
8
+ # http://groups.google.com/group/couchrest/browse_thread/thread/3b6aeb6469a0ea35?pli=1
9
+ # try to fix it be changing a property (ugly hack), also see https://github.com/couchrest/couchrest_model/issues/114
10
+ #disable_dirty #true
11
+ couchrest_attribute_will_change!('update_me') # or update_me_will_change!
12
+ end
13
+
14
+ before_save do
15
+ @dependencies.save_all()
16
+ end
17
+
18
+ # mimic the destroy_document method from https://github.com/langalex/couch_potato/blob/master/lib/couch_potato/database.rb
19
+ before_destroy do
20
+ ok = true
21
+ (self["roles"] || []).each do |klass|
22
+ document = self.as(klass.constantize)
23
+ (ok = false) if false == document.run_callbacks(:destroy) do
24
+ true
25
+ end
26
+ end
27
+ ok
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,94 @@
1
+ class ThingTank
2
+
3
+ # tracks the dependencies of a ThingTank, such as saving of roles, children ThingTanks, registration of roles
4
+ class Dependencies
5
+ def initialize(doc)
6
+ @doc = doc
7
+ @registration = []
8
+ @save = []
9
+ @children = {}
10
+ end
11
+
12
+ attr_reader :parent
13
+
14
+ def register_roles
15
+ @registration.each { |role| @doc.register_role(role) }
16
+ end
17
+
18
+ def save_all()
19
+ refresh true
20
+ @save.each { |role_instance| role_instance.save }
21
+ end
22
+
23
+ def add_role(role)
24
+ @registration << role
25
+ end
26
+
27
+ def remove_role(role)
28
+ @registration.delete(role) if has_role?(role)
29
+ end
30
+
31
+ def has_role?(klass)
32
+ @registration.include? klass
33
+ end
34
+
35
+ def save_role(role_instance)
36
+ @save << role_instance unless @save.include? role_instance
37
+ end
38
+
39
+ def already_saved(role_instance)
40
+ @save.delete role_instance
41
+ end
42
+
43
+ def refresh(save=false, with_parent=true)
44
+ register_roles
45
+ @children.each do |key,child|
46
+ @doc[key] = case child
47
+ when Array
48
+ child.collect do |d|
49
+ d.dependencies.refresh(save, false)
50
+ d.save if save
51
+ d.to_role_hash
52
+ end
53
+ else
54
+ child.dependencies.refresh(save, false)
55
+ child.save if save
56
+ child.to_role_hash
57
+ end
58
+ end
59
+ refresh_parent() if with_parent
60
+ end
61
+
62
+ def refresh_parent()
63
+ @parent.dependencies.refresh if @parent
64
+ end
65
+
66
+ def add_child(key,child)
67
+ raise "something wrong here #{key} #{child.inspect}" unless child.is_a?(ThingTank) || child.is_a?(Array)
68
+ if child.is_a? Array
69
+ child.each do |c|
70
+ c.dependencies.set_parent(@doc)
71
+ end
72
+ else
73
+ child.dependencies.set_parent(@doc)
74
+ end
75
+ @children[key] = child
76
+ end
77
+
78
+ def find_root_doc()
79
+ return @doc unless @parent
80
+ _d = @doc
81
+ doc = _d
82
+ while doc.dependencies.parent do
83
+ doc = doc.dependencies.parent
84
+ end
85
+ return doc
86
+ end
87
+
88
+ protected
89
+
90
+ def set_parent(parent)
91
+ @parent = parent
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,66 @@
1
+ # a replacement database for roles
2
+ class ThingTank::FakeBase
3
+
4
+ def initialize(doc, role)
5
+ @db = doc.database
6
+ @doc = doc
7
+ @role = role
8
+ @doc.dependencies.add_role(role)
9
+ end
10
+
11
+ def method_missing(meth, *args)
12
+ raise "don't call #{self.class}: #{meth.inspect}(#{args.inspect})"
13
+ end
14
+
15
+ def is_a?(klass)
16
+ return true if klass == CouchRest::Database
17
+ super
18
+ end
19
+
20
+ def to_role(klass)
21
+ self.class.new(@doc, klass).get()
22
+ end
23
+
24
+ def result(ok=true)
25
+ {"ok" => ok}
26
+ end
27
+
28
+ def save_doc(role_doc)
29
+ if role_doc.is_a? ThingTank # I dunno why this happens
30
+ @doc.dependencies.refresh_parent()
31
+ else
32
+ @doc.save_role_attributes(role_doc) if role_doc.changed?
33
+ end
34
+ result
35
+ end
36
+
37
+ def delete_doc(*role_docs)
38
+ role_docs.each do |role_doc|
39
+ # delete only the attributes from me that are no part of another role, since we will have different doc ids
40
+ # for each doc we will need to identify the remaining attributes
41
+ doc, save = (role_doc["_id"].nil? || role_doc["_id"] == @doc.id) ?
42
+ [@doc, false] :
43
+ [@db.get(role_doc["_id"]), true]
44
+
45
+ doc.delete_role(role_doc)
46
+ doc.save if save # if all roles are from our doc, only save once: at the end
47
+ end
48
+ @doc.save if @doc.changed?
49
+ result()
50
+ end
51
+
52
+ def get(id=:doc_id)
53
+ id = @doc.id if id == :doc_id
54
+ doc = id == @doc.id ? @doc : @db.get(id)
55
+ doc.get_role(@role, self)
56
+ end
57
+
58
+ def _role_doc
59
+ @doc
60
+ end
61
+
62
+ def _doc
63
+ @doc._root
64
+ end
65
+ end
66
+
@@ -0,0 +1,24 @@
1
+ class ThingTank
2
+
3
+ # update_attributes seems to be broken for mass_assign_any_attribute, see
4
+ # http://groups.google.com/group/couchrest/browse_thread/thread/3b6aeb6469a0ea35?pli=1
5
+ # try to fix it be changing a property (ugly hack), also see https://github.com/couchrest/couchrest_model/issues/114
6
+ module ForceUpdate
7
+
8
+ def self.included(base)
9
+
10
+ base.class_eval do
11
+
12
+ property :update_me
13
+
14
+ before_validation do
15
+ couchrest_attribute_will_change!('update_me') # or update_me_will_change! will also do
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,45 @@
1
+ class ThingTank
2
+
3
+ module InstanceMethods
4
+
5
+ def self.included(base)
6
+
7
+ base.class_eval do
8
+
9
+ def initialize(*args)
10
+ super
11
+ @dependencies = ThingTank::Dependencies.new(self)
12
+ end
13
+
14
+ def dependencies
15
+ @dependencies
16
+ end
17
+
18
+ def first(key)
19
+ [self[key.to_s]].flatten.last
20
+ end
21
+
22
+ def last(key)
23
+ [self[key.to_s]].flatten.first
24
+ end
25
+
26
+ # export a property to its own document and replaces the original reference with the doc["_id"]
27
+ def export(property)
28
+ raise ArgumentError, "doc.database required for extracting" unless database
29
+ new_doc = self[property.to_s]
30
+ raise ArgumentError, "#{new_doc.inspect} is no CouchRest::Document or Hash" unless new_doc.is_a?(CouchRest::Document) or new_doc.is_a?(Hash)
31
+ result = database.save_doc new_doc
32
+ if result['ok']
33
+ self["#{property}_id"] = result["id"]
34
+ self[property.to_s] = nil
35
+ end
36
+ result['ok']
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,110 @@
1
+
2
+ class ThingTank
3
+
4
+ class Role < CouchRest::Model::Base
5
+
6
+ include ThingTank::ForceUpdate
7
+ include ThingTank::SharedMethods
8
+
9
+ class << self
10
+
11
+ def property(name, *args)
12
+ @role_properties ||= []
13
+ @role_properties << name.to_s
14
+ super
15
+ end
16
+
17
+ def role_properties
18
+ @role_properties
19
+ end
20
+
21
+ def -(key)
22
+ [self,key]
23
+ end
24
+
25
+ def get(id, db = database)
26
+ doc = ThingTank.get(id)
27
+ return nil if doc.nil?
28
+ return doc.to_role(self)
29
+ end
30
+
31
+ def get!(id, db = database)
32
+ doc = ThingTank.get!(id)
33
+ doc.to_role(self)
34
+ end
35
+
36
+ def wants(*modules)
37
+ @wishes ||= []
38
+ @wishes = @wishes.concat modules
39
+ end
40
+
41
+ def wishes
42
+ @wishes
43
+ end
44
+
45
+ def design
46
+ raise "design is not supported in ThingTank::Role, please use the 'role_view' method in a ThingTank subclass design definition"
47
+ end
48
+
49
+ def view_by(*args)
50
+ raise "view_by is not supported in ThingTank::Role, please use the 'role_view_by' method in a ThingTank subclass"
51
+ end
52
+
53
+ end
54
+
55
+ def -(key)
56
+ [self,key]
57
+ end
58
+
59
+ def _doc
60
+ if database.is_a?(FakeBase)
61
+ database._doc
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
67
+ # the virtual _doc that contains me, you should not need it normally
68
+ def _role_doc
69
+ if database.is_a?(FakeBase)
70
+ database._role_doc
71
+ else
72
+ nil
73
+ end
74
+ end
75
+
76
+ def flush_to_doc
77
+ if changed?
78
+ changed_to_role_hash().each do |k,v|
79
+ _role_doc[k] = v
80
+ end
81
+ _role_doc.save
82
+ @changed_attributes.clear
83
+ end
84
+ end
85
+
86
+ def reload
87
+ attrs = _role_doc.as(self.class).to_role_hash
88
+ prepare_all_attributes(attrs, :directly_set_attributes => true)
89
+ @changed_attributes.clear
90
+ self
91
+ end
92
+
93
+ def to_role(klass, key, &code)
94
+ _role_doc.to_role(klass, key, &code)
95
+ end
96
+
97
+ def first(key)
98
+ _role_doc.first(key)
99
+ end
100
+
101
+ def last(key)
102
+ _role_doc.last(key)
103
+ end
104
+
105
+ def add_role(klass, key=nil, &code)
106
+ _role_doc.add_role(klass, key, &code)
107
+ end
108
+ end
109
+
110
+ end
@@ -0,0 +1,198 @@
1
+ class ThingTank
2
+ module RoleHandling
3
+ def self.included(klass)
4
+ klass.class_eval do
5
+
6
+ def properties_of_role(klass)
7
+ hsh = {}
8
+ (klass.role_properties || []).each do |k|
9
+ hsh.update(k.to_s => self[k.to_s]) unless self[k.to_s].nil?
10
+ end
11
+ hsh
12
+ end
13
+
14
+ def get_role(klass, db)
15
+ hsh = properties_of_role(klass)
16
+ hsh.update "_id" => self["_id"], "_rev" => self["_rev"] if (had?(klass) && !hsh.empty?)
17
+ inst = klass.new hsh, :directly_set_attributes => true, :database => db
18
+ @dependencies.save_role(inst)
19
+ inst
20
+ end
21
+
22
+ # get property as pseudo doc to play with
23
+ def with(key, &code)
24
+ self[key] ||= {}
25
+ case self[key]
26
+ when String # assume we got a doc id
27
+ doc = ThingTank.get(self[key])
28
+ (code.call(doc) ; doc.save) if code
29
+ doc
30
+ when Hash
31
+ doc = self.class.new self[key], :directly_set_attributes => true, :database => FakeBase.new(self, nil)
32
+ @dependencies.add_child(key, doc)
33
+ @dependencies.refresh(false)
34
+ (code.call(doc) ; doc.save) if code
35
+ doc
36
+ when Array
37
+ with_all(key)
38
+ else
39
+ raise "not supported: #{self[key].inspect}"
40
+ end
41
+ end
42
+
43
+ def with_all(key, props=nil)
44
+ self[key] ||= []
45
+ props ||= [self[key]].flatten
46
+ docs = props.collect { |prop| self.class.new prop, :directly_set_attributes => true, :database => FakeBase.new(self, nil) }
47
+ @dependencies.add_child(key, docs)
48
+ docs.each { |doc| yield doc ; doc.save } if block_given?
49
+ docs
50
+ end
51
+
52
+ def with_nth(key,n)
53
+ self[key] ||= []
54
+ props = [self[key]].flatten
55
+ n = 0 if n == :first
56
+ n = props.size-1 if n == :last
57
+ n = 0 if n < 0
58
+ while n > props.size - 1
59
+ props << {}
60
+ end
61
+ docs = with_all(key, props)
62
+ (yield docs[n] ; docs[n].save) if block_given?
63
+ docs[n]
64
+ end
65
+
66
+ def with_first(key,&code)
67
+ with_nth(key,:first, &code)
68
+ end
69
+
70
+ def with_last(key,&code)
71
+ with_nth(key,:last, &code)
72
+ end
73
+
74
+ def property_to_role(key, klass)
75
+ case self[key]
76
+ when Hash
77
+ with(key).to_role(klass)
78
+ when Array
79
+ with_all(key).collect { |doc| doc.to_role(klass) }
80
+ end
81
+ end
82
+
83
+ # register a role
84
+ def register_role(klass)
85
+ self['roles'] ||= []
86
+ self['roles'] << klass.to_s unless self['roles'].include? klass.to_s
87
+ end
88
+
89
+ def unregister_role(role)
90
+ if has?(role)
91
+ self["roles"] ||= []
92
+ self["roles"].delete role.to_s
93
+ end
94
+ end
95
+
96
+ def add_role(klass, key=nil, &code)
97
+ if key
98
+ key = key.to_s
99
+ if val = self[key]
100
+ self[key] = [val] unless val.is_a? Array
101
+ self[key] << {}
102
+ last_role(klass, key, &code)
103
+ else
104
+ self[key] = {}
105
+ to_role(klass, key, &code)
106
+ end
107
+ else
108
+ to_role(klass, &code)
109
+ end
110
+ end
111
+
112
+ def _root
113
+ @dependencies.find_root_doc
114
+ end
115
+
116
+ def delete_role(role_doc)
117
+ klass = role_doc.class
118
+ deleteable_attributes(klass).each { |k| delete(k) ; role_doc[k] = nil }
119
+ @dependencies.already_saved(role_doc) # prevent an endless loop
120
+ unregister_role(klass)
121
+ @dependencies.remove_role(klass)
122
+ @dependencies.refresh_parent()
123
+ end
124
+
125
+ def deleteable_attributes(klass)
126
+ role_attrs = attributes_by_roles
127
+ role = klass.to_s
128
+ deleteable_attrs = klass.role_properties.select do |prop|
129
+ role_attrs[prop].empty? || (role_attrs[prop].size == 1 && role_attrs[prop].first == role)
130
+ end
131
+ %w| _id _rev type update_me roles|.each{ |k| deleteable_attrs.delete k }
132
+ return deleteable_attrs
133
+ end
134
+
135
+ def attributes_by_roles()
136
+ attributes = {}
137
+ self["roles"].each do |role|
138
+ role.constantize.role_properties.each do |prop|
139
+ attributes[prop] ||= []
140
+ attributes[prop] << role
141
+ end
142
+ end
143
+ return attributes
144
+ end
145
+
146
+ def to_role(klass, key=nil, add_role=true, &code)
147
+ @dependencies.add_role(klass) if add_role && key.nil?
148
+ role = key ? property_to_role(key, klass) : FakeBase.new(self, klass).get()
149
+ (code.call(role) ; role.flush_to_doc) if code
150
+ role
151
+ end
152
+
153
+ alias :as :to_role
154
+ alias :has :to_role
155
+ alias :act_as :to_role
156
+ alias :be :to_role
157
+ alias :have :to_role
158
+ alias :are :to_role
159
+ alias :is :to_role
160
+
161
+ def save_role_attributes(role_doc)
162
+ @dependencies.already_saved(role_doc) # prevent an endless loop
163
+ attributes = role_doc.to_role_hash(true)
164
+ unsavebales = attributes.keys - (role_doc.class.role_properties || [])
165
+ raise "role #{role_doc.class} tried to save properties that it does not have: #{unsavebales.inspect}" unless unsavebales.empty?
166
+ self.update_attributes attributes
167
+ @dependencies.refresh_parent()
168
+ end
169
+
170
+ # if the doc when coming from db already had this role
171
+ def had?(klass)
172
+ return false unless self["roles"] and self["roles"].include? klass.name
173
+ return true
174
+ end
175
+
176
+ # has the document the role, is it supposed to have it and does it have the necessary properties
177
+ def has?(klass)
178
+ return true if @dependencies.has_role?(klass)
179
+ return false unless self["roles"] and self["roles"].include? klass.name
180
+ could_be? klass
181
+ end
182
+ alias :is? :has?
183
+
184
+ # could the document have the role
185
+ def could_have?(klass)
186
+ to_role(klass, nil, false).valid?
187
+ end
188
+ alias :could_be? :could_have?
189
+
190
+ # roles that are not valid
191
+ def invalid_roles
192
+ (self["roles"] || []).select { |role| !self.as(role.constantize).valid? }
193
+ end
194
+
195
+ end
196
+ end
197
+ end
198
+ end