thingtank 0.1.0 → 0.2.0
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/README.md +24 -22
- data/Rakefile +1 -6
- data/VERSION +1 -1
- data/examples/bear_julius.rb +7 -7
- data/examples/first_marriage.rb +4 -4
- data/examples/immortal_julius.rb +2 -2
- data/examples/marriage_improvement.rb +11 -11
- data/examples/second_marriage.rb +2 -2
- data/lib/couchrest/extensions/view.rb +3 -3
- data/lib/thingtank/callbacks.rb +1 -1
- data/lib/thingtank/{role.rb → character.rb} +21 -21
- data/lib/thingtank/character_handling.rb +198 -0
- data/lib/thingtank/dependencies.rb +16 -16
- data/lib/thingtank/fakebase.rb +17 -17
- data/lib/thingtank/shared_methods.rb +19 -19
- data/lib/thingtank/shortcuts.rb +6 -6
- data/lib/thingtank/thingtank.rb +2 -2
- data/lib/thingtank.rb +9 -4
- data/test/test_fakebase.rb +115 -115
- data/test/test_helper.rb +4 -4
- data/test/test_thingtank.rb +17 -17
- data/test/test_views.rb +6 -6
- metadata +29 -29
- data/lib/thingtank/role_handling.rb +0 -198
@@ -0,0 +1,198 @@
|
|
1
|
+
class ThingTank
|
2
|
+
module CharacterHandling
|
3
|
+
def self.included(klass)
|
4
|
+
klass.class_eval do
|
5
|
+
|
6
|
+
def properties_of_character(klass)
|
7
|
+
hsh = {}
|
8
|
+
(klass.character_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_character(klass, db)
|
15
|
+
hsh = properties_of_character(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_character(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_character(key, klass)
|
75
|
+
case self[key]
|
76
|
+
when Hash
|
77
|
+
with(key).to_character(klass)
|
78
|
+
when Array
|
79
|
+
with_all(key).collect { |doc| doc.to_character(klass) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# register a character
|
84
|
+
def register_character(klass)
|
85
|
+
self['characters'] ||= []
|
86
|
+
self['characters'] << klass.to_s unless self['characters'].include? klass.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
def unregister_character(character)
|
90
|
+
if has?(character)
|
91
|
+
self["characters"] ||= []
|
92
|
+
self["characters"].delete character.to_s
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_character(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_character(klass, key, &code)
|
103
|
+
else
|
104
|
+
self[key] = {}
|
105
|
+
to_character(klass, key, &code)
|
106
|
+
end
|
107
|
+
else
|
108
|
+
to_character(klass, &code)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def _root
|
113
|
+
@dependencies.find_root_doc
|
114
|
+
end
|
115
|
+
|
116
|
+
def delete_character(character_doc)
|
117
|
+
klass = character_doc.class
|
118
|
+
deleteable_attributes(klass).each { |k| delete(k) ; character_doc[k] = nil }
|
119
|
+
@dependencies.already_saved(character_doc) # prevent an endless loop
|
120
|
+
unregister_character(klass)
|
121
|
+
@dependencies.remove_character(klass)
|
122
|
+
@dependencies.refresh_parent()
|
123
|
+
end
|
124
|
+
|
125
|
+
def deleteable_attributes(klass)
|
126
|
+
character_attrs = attributes_by_characters
|
127
|
+
character = klass.to_s
|
128
|
+
deleteable_attrs = klass.character_properties.select do |prop|
|
129
|
+
character_attrs[prop].empty? || (character_attrs[prop].size == 1 && character_attrs[prop].first == character)
|
130
|
+
end
|
131
|
+
%w| _id _rev type update_me characters|.each{ |k| deleteable_attrs.delete k }
|
132
|
+
return deleteable_attrs
|
133
|
+
end
|
134
|
+
|
135
|
+
def attributes_by_characters()
|
136
|
+
attributes = {}
|
137
|
+
self["characters"].each do |character|
|
138
|
+
character.constantize.character_properties.each do |prop|
|
139
|
+
attributes[prop] ||= []
|
140
|
+
attributes[prop] << character unless attributes[prop].include?(character)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
return attributes
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_character(klass, key=nil, add_character=true, &code)
|
147
|
+
@dependencies.add_character(klass) if add_character && key.nil?
|
148
|
+
character = key ? property_to_character(key, klass) : FakeBase.new(self, klass, add_character).get()
|
149
|
+
(code.call(character) ; character.flush_to_doc) if code
|
150
|
+
character
|
151
|
+
end
|
152
|
+
|
153
|
+
alias :as :to_character
|
154
|
+
alias :has :to_character
|
155
|
+
alias :act_as :to_character
|
156
|
+
alias :be :to_character
|
157
|
+
alias :have :to_character
|
158
|
+
alias :are :to_character
|
159
|
+
alias :is :to_character
|
160
|
+
|
161
|
+
def save_character_attributes(character_doc)
|
162
|
+
@dependencies.already_saved(character_doc) # prevent an endless loop
|
163
|
+
attributes = character_doc.to_character_hash(true)
|
164
|
+
unsavebales = attributes.keys - (character_doc.class.character_properties || [])
|
165
|
+
raise "character #{character_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 character
|
171
|
+
def had?(klass)
|
172
|
+
return false unless self["characters"] and self["characters"].include? klass.name
|
173
|
+
return true
|
174
|
+
end
|
175
|
+
|
176
|
+
# has the document the character, is it supposed to have it and does it have the necessary properties
|
177
|
+
def has?(klass)
|
178
|
+
return true if @dependencies.has_character?(klass)
|
179
|
+
return false unless self["characters"] and self["characters"].include? klass.name
|
180
|
+
could_be? klass
|
181
|
+
end
|
182
|
+
alias :is? :has?
|
183
|
+
|
184
|
+
# could the document have the character
|
185
|
+
def could_have?(klass)
|
186
|
+
to_character(klass, nil, false).valid?
|
187
|
+
end
|
188
|
+
alias :could_be? :could_have?
|
189
|
+
|
190
|
+
# characters that are not valid
|
191
|
+
def invalid_characters
|
192
|
+
(self["characters"] || []).select { |character| !self.as(character.constantize).valid? }
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class ThingTank
|
2
2
|
|
3
|
-
# tracks the dependencies of a ThingTank, such as saving of
|
3
|
+
# tracks the dependencies of a ThingTank, such as saving of characters, children ThingTanks, registration of characters
|
4
4
|
class Dependencies
|
5
5
|
def initialize(doc)
|
6
6
|
@doc = doc
|
@@ -15,49 +15,49 @@ class ThingTank
|
|
15
15
|
{ :registration => @registration, :save => @save, :children => @children }.inspect
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
19
|
-
@registration.each { |
|
18
|
+
def register_characters
|
19
|
+
@registration.each { |character| @doc.register_character(character) }
|
20
20
|
end
|
21
21
|
|
22
22
|
def save_all()
|
23
23
|
refresh true
|
24
|
-
@save.each { |
|
24
|
+
@save.each { |character_instance| character_instance.save }
|
25
25
|
end
|
26
26
|
|
27
|
-
def
|
28
|
-
@registration <<
|
27
|
+
def add_character(character)
|
28
|
+
@registration << character
|
29
29
|
end
|
30
30
|
|
31
|
-
def
|
32
|
-
@registration.delete(
|
31
|
+
def remove_character(character)
|
32
|
+
@registration.delete(character) if has_character?(character)
|
33
33
|
end
|
34
34
|
|
35
|
-
def
|
35
|
+
def has_character?(klass)
|
36
36
|
@registration.include? klass
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
40
|
-
@save <<
|
39
|
+
def save_character(character_instance)
|
40
|
+
@save << character_instance unless @save.include? character_instance
|
41
41
|
end
|
42
42
|
|
43
|
-
def already_saved(
|
44
|
-
@save.delete
|
43
|
+
def already_saved(character_instance)
|
44
|
+
@save.delete character_instance
|
45
45
|
end
|
46
46
|
|
47
47
|
def refresh(save=false, with_parent=true)
|
48
|
-
|
48
|
+
register_characters
|
49
49
|
@children.each do |key,child|
|
50
50
|
@doc[key] = case child
|
51
51
|
when Array
|
52
52
|
child.collect do |d|
|
53
53
|
d.dependencies.refresh(save, false)
|
54
54
|
d.save if save
|
55
|
-
d.
|
55
|
+
d.to_character_hash
|
56
56
|
end
|
57
57
|
else
|
58
58
|
child.dependencies.refresh(save, false)
|
59
59
|
child.save if save
|
60
|
-
child.
|
60
|
+
child.to_character_hash
|
61
61
|
end
|
62
62
|
end
|
63
63
|
refresh_parent() if with_parent
|
data/lib/thingtank/fakebase.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
# a replacement database for
|
1
|
+
# a replacement database for characters
|
2
2
|
class ThingTank::FakeBase
|
3
3
|
|
4
|
-
def initialize(doc,
|
4
|
+
def initialize(doc, character, add_dep = true)
|
5
5
|
@db = doc.database
|
6
6
|
@doc = doc
|
7
|
-
@
|
8
|
-
@doc.dependencies.
|
7
|
+
@character = character
|
8
|
+
@doc.dependencies.add_character(character) if add_dep
|
9
9
|
end
|
10
10
|
|
11
11
|
def method_missing(meth, *args)
|
@@ -17,7 +17,7 @@ class ThingTank::FakeBase
|
|
17
17
|
super
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
20
|
+
def to_character(klass)
|
21
21
|
self.class.new(@doc, klass).get()
|
22
22
|
end
|
23
23
|
|
@@ -25,25 +25,25 @@ class ThingTank::FakeBase
|
|
25
25
|
{"ok" => ok}
|
26
26
|
end
|
27
27
|
|
28
|
-
def save_doc(
|
29
|
-
if
|
28
|
+
def save_doc(character_doc)
|
29
|
+
if character_doc.is_a? ThingTank # I dunno why this happens
|
30
30
|
@doc.dependencies.refresh_parent()
|
31
31
|
else
|
32
|
-
@doc.
|
32
|
+
@doc.save_character_attributes(character_doc) if character_doc.changed?
|
33
33
|
end
|
34
34
|
result
|
35
35
|
end
|
36
36
|
|
37
|
-
def delete_doc(*
|
38
|
-
|
39
|
-
# delete only the attributes from me that are no part of another
|
37
|
+
def delete_doc(*character_docs)
|
38
|
+
character_docs.each do |character_doc|
|
39
|
+
# delete only the attributes from me that are no part of another character, since we will have different doc ids
|
40
40
|
# for each doc we will need to identify the remaining attributes
|
41
|
-
doc, save = (
|
41
|
+
doc, save = (character_doc["_id"].nil? || character_doc["_id"] == @doc.id) ?
|
42
42
|
[@doc, false] :
|
43
|
-
[@db.get(
|
43
|
+
[@db.get(character_doc["_id"]), true]
|
44
44
|
|
45
|
-
doc.
|
46
|
-
doc.save if save # if all
|
45
|
+
doc.delete_character(character_doc)
|
46
|
+
doc.save if save # if all characters are from our doc, only save once: at the end
|
47
47
|
end
|
48
48
|
@doc.save if @doc.changed?
|
49
49
|
result()
|
@@ -52,10 +52,10 @@ class ThingTank::FakeBase
|
|
52
52
|
def get(id=:doc_id)
|
53
53
|
id = @doc.id if id == :doc_id
|
54
54
|
doc = id == @doc.id ? @doc : @db.get(id)
|
55
|
-
doc.
|
55
|
+
doc.get_character(@character, self)
|
56
56
|
end
|
57
57
|
|
58
|
-
def
|
58
|
+
def _character_doc
|
59
59
|
@doc
|
60
60
|
end
|
61
61
|
|
@@ -4,8 +4,8 @@ class ThingTank
|
|
4
4
|
klass.class_eval do
|
5
5
|
|
6
6
|
# only export the changed properties
|
7
|
-
def
|
8
|
-
skips = skip_keys(
|
7
|
+
def changed_to_character_hash(exclude_characters=false)
|
8
|
+
skips = skip_keys(exclude_characters)
|
9
9
|
hsh = {}
|
10
10
|
changes.each do |key,arr|
|
11
11
|
_old, _new = arr
|
@@ -16,22 +16,22 @@ class ThingTank
|
|
16
16
|
hsh
|
17
17
|
end
|
18
18
|
|
19
|
-
def skip_keys(
|
19
|
+
def skip_keys(exclude_characters=false)
|
20
20
|
arr = %w|_id _rev type update_me updated_at created_at|
|
21
|
-
arr << '
|
21
|
+
arr << 'characters' if exclude_characters
|
22
22
|
arr
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
25
|
+
def to_character_hash(exclude_characters=false)
|
26
26
|
hsh = to_hash
|
27
|
-
skip_keys(
|
27
|
+
skip_keys(exclude_characters).each{ |k| hsh.delete k }
|
28
28
|
hsh
|
29
29
|
end
|
30
30
|
|
31
|
-
def
|
32
|
-
|
33
|
-
list = [
|
34
|
-
|
31
|
+
def nth_character(klass, key, n, &code)
|
32
|
+
orig_character = to_character(klass, key)
|
33
|
+
list = [orig_character].flatten
|
34
|
+
character = case n
|
35
35
|
when :last
|
36
36
|
list.last
|
37
37
|
when :first
|
@@ -40,21 +40,21 @@ class ThingTank
|
|
40
40
|
list[n]
|
41
41
|
end
|
42
42
|
if code
|
43
|
-
code.call(
|
44
|
-
|
43
|
+
code.call(character)
|
44
|
+
character.flush_to_doc
|
45
45
|
end
|
46
|
-
if
|
47
|
-
|
46
|
+
if orig_character.is_a?(Array)
|
47
|
+
character._character_doc.dependencies.refresh_parent
|
48
48
|
end
|
49
|
-
|
49
|
+
character
|
50
50
|
end
|
51
51
|
|
52
|
-
def
|
53
|
-
|
52
|
+
def last_character(klass, key, &code)
|
53
|
+
nth_character(klass, key, :last, &code)
|
54
54
|
end
|
55
55
|
|
56
|
-
def
|
57
|
-
|
56
|
+
def first_character(klass, key, &code)
|
57
|
+
nth_character(klass, key, :first, &code)
|
58
58
|
end
|
59
59
|
|
60
60
|
end
|
data/lib/thingtank/shortcuts.rb
CHANGED
@@ -6,27 +6,27 @@ class ThingTank
|
|
6
6
|
def >(klass_or_hash)
|
7
7
|
case klass_or_hash
|
8
8
|
when Array
|
9
|
-
|
9
|
+
last_character(klass_or_hash.first, klass_or_hash.last)
|
10
10
|
else
|
11
|
-
|
11
|
+
to_character(klass_or_hash)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
15
|
def <(klass_or_hash)
|
16
16
|
case klass_or_hash
|
17
17
|
when Array
|
18
|
-
|
18
|
+
first_character(klass_or_hash.first, klass_or_hash.last)
|
19
19
|
else
|
20
|
-
|
20
|
+
to_character(klass_or_hash)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
24
|
def << (arr)
|
25
25
|
klass, key, code= arr.flatten
|
26
26
|
if code
|
27
|
-
|
27
|
+
add_character(klass, key, &code)
|
28
28
|
else
|
29
|
-
|
29
|
+
add_character(klass, key)
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
data/lib/thingtank/thingtank.rb
CHANGED
@@ -7,12 +7,12 @@ class ThingTank < CouchRest::Model::Base
|
|
7
7
|
mass_assign_any_attribute true # should always allow to set non defined arbitrary properties
|
8
8
|
timestamps!
|
9
9
|
|
10
|
-
def
|
10
|
+
def character_view_by(klass, *keys)
|
11
11
|
opts = keys.pop if keys.last.is_a?(Hash)
|
12
12
|
opts ||= {}
|
13
13
|
opts[:guards] ||= []
|
14
14
|
# there is no "inArray" like function in couchdb, see http://stackoverflow.com/questions/3740464/i-have-to-write-every-function-i-need-for-couchdb
|
15
|
-
opts[:guards] << "((doc['
|
15
|
+
opts[:guards] << "((doc['characters'] !== undefined) && (function (item,arr) { for(p=0;p<arr.length;p++) if (item == arr[p]) return true; return false;})('#{klass.to_s}',doc['characters']))"
|
16
16
|
keys << opts
|
17
17
|
view_by(*keys)
|
18
18
|
end
|
data/lib/thingtank.rb
CHANGED
@@ -2,6 +2,11 @@ if RUBY_VERSION =~ /1.8/
|
|
2
2
|
require 'backports'
|
3
3
|
end
|
4
4
|
|
5
|
+
# TODO
|
6
|
+
# - check if we could get useful inspiration from https://github.com/givmo/couch_record
|
7
|
+
# - improve views
|
8
|
+
# - validation for characters
|
9
|
+
|
5
10
|
require_relative File.join('thingtank', 'thingtank.rb')
|
6
11
|
require_relative File.join('couchrest', 'extensions', 'view.rb')
|
7
12
|
require_relative File.join('thingtank', 'dependencies.rb')
|
@@ -10,19 +15,19 @@ require_relative File.join('thingtank', 'force_update.rb')
|
|
10
15
|
require_relative File.join('thingtank', 'shared_methods.rb')
|
11
16
|
require_relative File.join('thingtank', 'instance_methods.rb')
|
12
17
|
require_relative File.join('thingtank', 'shortcuts.rb')
|
13
|
-
require_relative File.join('thingtank', '
|
14
|
-
require_relative File.join('thingtank', '
|
18
|
+
require_relative File.join('thingtank', 'character_handling.rb')
|
19
|
+
require_relative File.join('thingtank', 'character.rb')
|
15
20
|
require_relative File.join('thingtank', 'fakebase.rb')
|
16
21
|
|
17
22
|
# TODO:
|
18
|
-
#
|
23
|
+
# character validator
|
19
24
|
|
20
25
|
class ThingTank
|
21
26
|
|
22
27
|
include ThingTank::ForceUpdate
|
23
28
|
include ThingTank::SharedMethods
|
24
29
|
include ThingTank::InstanceMethods
|
25
|
-
include ThingTank::
|
30
|
+
include ThingTank::CharacterHandling
|
26
31
|
include ThingTank::Shortcuts
|
27
32
|
include ThingTank::Callbacks
|
28
33
|
|