thingtank 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|