thingtank 0.0.1

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