thingtank 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'couchrest'
4
+ gem 'couchrest_model'
5
+
6
+ group :development do
7
+ #gem "minitest", ">= 0"
8
+ gem "yard", "~> 0.6.0"
9
+ gem "bundler", "~> 1.0.0"
10
+ gem "jeweler", "~> 1.6.4"
11
+ gem "rcov", ">= 0"
12
+
13
+ gem 'linecache19'
14
+ gem 'ruby-debug19'
15
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Marc Rene Arns
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,329 @@
1
+ thingtank: play different roles
2
+ =====================
3
+
4
+ ThingTank is a library that uses couchrest and couchrest model create arbitrary
5
+ objects that may have different roles. The roles determine the
6
+ properties and they can be mixed and matched at will.
7
+
8
+
9
+ Installation
10
+ ------------
11
+ _thingtank_ is only tested with ruby 1.9.2 and above.
12
+
13
+ Install it as a gem:
14
+
15
+ sudo gem install thingtank
16
+
17
+ or in rvm:
18
+
19
+ gem install thingtank
20
+
21
+
22
+ Examples:
23
+ --------
24
+
25
+ To simplify the class building we let them all inherit from a role class
26
+
27
+ class Born < ThingTank::Role
28
+ property :birth_date
29
+ property :birth_place
30
+
31
+ validates_presence_of :birth_date # make sure a date is given
32
+ end
33
+
34
+
35
+ Imagine a Caesar is born
36
+
37
+ class Born < ThingTank::Role
38
+ property :birth_date, :alias => :born_at
39
+ property :birth_place
40
+
41
+ validates_presence_of :birth_date # make sure a date is given
42
+ end
43
+
44
+ # just in case he might die....we might want to have a date and maybe even a place
45
+ class Dead < ThingTank::Role
46
+ property :date_of_death
47
+ property :place_of_death
48
+
49
+ validates_presence_of :date_of_death
50
+ end
51
+
52
+ class Person < ThingTank::Role
53
+ property :name
54
+ property :gender
55
+
56
+ validates_presence_of :name
57
+ end
58
+
59
+
60
+ julius = ThingTank.create { :gender => :m, :name => 'Gaius Iulius Caesar' }
61
+ julius["birth_date"] = "100 BC"
62
+ julius["birth_place"] = "Rome"
63
+ julius.is(Born)
64
+
65
+ julius.could_be? Person # => true
66
+ julius.is? Person # => false
67
+ julius.is Person
68
+ julius.is? Person # => true
69
+
70
+ julius.is(Born)["gender"] # => nil (gender is not a property of born)
71
+ julius.is(Born)["birth_date"] # => "100BC"
72
+
73
+ id = julius.id
74
+
75
+ later...
76
+ julius = ThingTank.get id
77
+ julius["birth_date"] # => "100BC"
78
+ julius.is? Person # => true
79
+
80
+
81
+ when he is adult, he wants to marry. now things are getting a bit more complicated:
82
+
83
+ # he needs a marriage and a women
84
+ class Married < ThingTank::Role
85
+ property :date # the date of the marriage
86
+ property :end # when the marriage ended
87
+ property :spouse # doc_id of the spouse
88
+ property :state # state of the marriage
89
+
90
+ validates_presence_of :date
91
+ validates_presence_of :spouse
92
+ validate :spouse_should_be_a_person
93
+
94
+ # ensure that 'spouse' is a doc_id of a Person
95
+ def spouse_should_be_a_person
96
+ Person.get(self["spouse"]).valid? # loads doc as role Person and validates
97
+ end
98
+ end
99
+
100
+ # we want easy access to the name of the spouse
101
+ class Spouse < ThingTank::Role
102
+ property :married # doc must have "married" property (should be of class married)
103
+ property :married_state
104
+
105
+ validate :spouse_should_be_married
106
+
107
+ def married
108
+ self["married"] # contains a Married role
109
+ end
110
+
111
+ def spouse_should_be_married
112
+ married["spouse"] == self["_id"]
113
+ end
114
+
115
+ def name
116
+ self["married_state"] == "married" ?
117
+ Person.get(married["spouse"]).name :
118
+ nil
119
+ end
120
+
121
+ def ex
122
+ self["married_state"] == "divorced" ?
123
+ Person.get(married["spouse"]).name :
124
+ nil
125
+ end
126
+ end
127
+
128
+ now we could easily get julius married
129
+
130
+ conny = ThingTank.create :gender => "f", :name => 'Cornelia', :roles => ['Person']
131
+ julius["married"] = {"date" => "84 BC", "spouse" => conny.id}
132
+ julius["married_state"] = "married"
133
+
134
+ julius.with("married").is(Married).valid? # => true # "married" is a property that plays the Married
135
+
136
+ julius.has(Spouse).valid? # #has is an alias of #is
137
+
138
+ julius.save
139
+ julius.reload
140
+
141
+ julius.has(Spouse).name # => 'Cornelia'
142
+
143
+ while that is nice, let see if we could make it more comfortable:
144
+
145
+
146
+ class Married
147
+
148
+ # marry a doc or hash
149
+ def marry(person)
150
+ person = ThingTank.new(person) if person.is_a?(Hash)
151
+ person.save # should have a doc_id
152
+
153
+ # assign the doc_id to spouse
154
+ self["spouse"] = person["_id"]
155
+ self["state"] = 'married'
156
+ _doc["married_state"] = 'married'
157
+
158
+ unless person["married"] && person.last_role(Married, "married").spouse == _doc["_id"]
159
+ person.add_role(Married, "married") do |m|
160
+ m.date = self["date"]
161
+ m.marry _doc
162
+ end
163
+ person.is(Spouse)
164
+ person.save
165
+ end
166
+ end
167
+
168
+ def divorce(date)
169
+ self["state"] = 'divorced'
170
+ self['end'] = date
171
+ _doc.save
172
+ spouse = Spouse.get(self["spouse"])
173
+ if spouse.married_state == "married"
174
+ spouse.divorce(date)
175
+ end
176
+ end
177
+
178
+ end
179
+
180
+ class Spouse
181
+ def married(&code)
182
+ _doc.last_role Married, "married", &code
183
+ end
184
+
185
+ def divorce(date)
186
+ self["married_state"] = 'divorced'
187
+ married { |m| m.divorce(date) }
188
+ end
189
+ end
190
+
191
+ class Person
192
+ def marry(date, person)
193
+ _doc.add_role Married, "married", do |m|
194
+ m.date = "84 BC"
195
+ m.marry person
196
+ end
197
+ end
198
+ end
199
+
200
+ it now becomes much less work and Cornelia also knows that she is married to Julius
201
+
202
+ julius.as(Person).marry "84 BC", :gender => "f", :name => 'Cornelia'
203
+ julius.save
204
+ julius.reload
205
+
206
+ julius.has(Spouse).name # => 'Cornelia'
207
+ julius.has(Spouse).married_state # => 'married'
208
+
209
+ conny_id = julius.last_role(Married,"married").spouse
210
+ conny = ThingTank.get conny_id
211
+ conny.has(Spouse).married_state # => 'married'
212
+
213
+
214
+ julius could even marry a second time, i.e. marriage becomes an Array of Marriage objects
215
+
216
+ julius.as(Person).marry "68-65 BC", :gender => "f", :name => 'Pompeia'
217
+
218
+
219
+
220
+ Person.get(julius["married"].first["spouse"]).name # => 'Cornelia'
221
+ Person.get(julius["married"].last["spouse"]).name # => 'Pompeia'
222
+ julius.has(Spouse).name # => 'Pompeia'
223
+ julius["married"].first["state"] # => 'married'
224
+ julius["married"].last["state"] # => 'married'
225
+ julius["married"].size # => 2
226
+
227
+ # ouch, two women!
228
+
229
+
230
+ we should let Spouse know that Marriage might be an array, so simply overwrite marriage with
231
+ julius is still married with Cornelia but he should not
232
+
233
+ if Cornelia died before his second marriage, it should'nt be a problem:
234
+
235
+ class Dead
236
+
237
+ # all callbacks of roles are called and defined like corresponding callbacks of the doc
238
+ before_save do
239
+ if _doc.is?(Spouse) && _doc['married_state'] == 'married'
240
+ Spouse.get(_doc.last_role(Married, 'married').spouse).widowed(self["date_of_death"])
241
+ end
242
+ true
243
+ end
244
+
245
+ end
246
+
247
+ class Person
248
+ def dies(date)
249
+ _doc.is(Dead) do |d|
250
+ d.date_of_death = date
251
+ end
252
+ end
253
+ end
254
+
255
+ class Married
256
+ def widow(date)
257
+ self["state"] = 'widowed'
258
+ self['end'] = date
259
+ _doc.save
260
+ end
261
+ end
262
+
263
+ class Spouse
264
+ def widowed(date)
265
+ self["married_state"] = 'widowed'
266
+ married { |m| m.widow(date) }
267
+ end
268
+ end
269
+
270
+ julius.as(Person).marry "84 BC", :gender => "f", :name => 'Cornelia'
271
+ julius.save
272
+ julius.reload
273
+
274
+ conny = ThingTank.get julius["married"]["spouse"]
275
+ conny.save
276
+ conny.reload
277
+ conny.as(Person).dies "68-65 BC"
278
+ conny.save
279
+
280
+ julius.reload
281
+
282
+ julius.as(Person).marry "68-65 BC", :gender => "f", :name => 'Pompeia'
283
+
284
+ julius["married"].size # => 2
285
+ Person.get(julius["married"].first["spouse"]).name # => 'Cornelia'
286
+ julius["married"].first["state"] # => 'widowed'
287
+
288
+ since julius is immortal, no one should be able to destroy him:
289
+
290
+
291
+ class Undestroyable < ThingTank::Role
292
+ before_destroy do
293
+ false # never allow to destroy
294
+ end
295
+ end
296
+
297
+ julius.is(Undestroyable)
298
+ julius.save # save the role
299
+
300
+ id = julius.id
301
+ julius = ThingTank.get id
302
+
303
+ julius.as(Undestroyable).destroy
304
+ ThingTank.get(id).nil? # => julius is still there
305
+
306
+ julius.destroy
307
+
308
+ ThingTank.get(id).nil? # => julius is still there
309
+
310
+
311
+ You may subclass Hanswurst to do further separation and mix the
312
+ native properties of Hanswursts / its subclasses with the roles properties.
313
+
314
+
315
+ == Contributing to thingtank
316
+
317
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
318
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
319
+ * Fork the project
320
+ * Start a feature/bugfix branch
321
+ * Commit and push until you are happy with your contribution
322
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
323
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
324
+
325
+ == Copyright
326
+
327
+ Copyright (c) 2012 Marc Rene Arns. See LICENSE.txt for
328
+ further details.
329
+
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "thingtank"
18
+ gem.homepage = "http://github.com/metakeule/thingtank"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{thin wrapper around couchrest}
21
+ gem.email = "Base64.decode64(bGludXhAbWFyY3JlbmVhcm5zLmRl\n)"
22
+ gem.authors = ["Marc Rene Arns"]
23
+ # dependencies defined in Gemfile
24
+ end
25
+ Jeweler::RubygemsDotOrgTasks.new
26
+
27
+ require 'rake/testtask'
28
+ Rake::TestTask.new(:test) do |test|
29
+ test.libs << 'lib' << 'test'
30
+ test.pattern = 'test/**/test_*.rb'
31
+ test.verbose = true
32
+ end
33
+
34
+ require 'rcov/rcovtask'
35
+ Rcov::RcovTask.new do |test|
36
+ test.libs << 'test'
37
+ test.pattern = 'test/**/test_*.rb'
38
+ test.verbose = true
39
+ test.rcov_opts << '--exclude "gems/*"'
40
+ end
41
+
42
+ task :default => :test
43
+
44
+ require 'yard'
45
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,50 @@
1
+ class Born < ThingTank::Role
2
+ property :birth_date, :alias => :born_at
3
+ property :birth_place
4
+
5
+ validates_presence_of :birth_date # make sure a date is given
6
+ end
7
+
8
+ # just in case he might die....we might want to have a date and maybe even a place
9
+ class Dead < ThingTank::Role
10
+ property :date_of_death
11
+ property :place_of_death
12
+
13
+ validates_presence_of :date_of_death
14
+ end
15
+
16
+ class Person < ThingTank::Role
17
+ property :name
18
+ property :gender
19
+
20
+ validates_presence_of :name
21
+ end
22
+
23
+ def test_bear_julius
24
+ julius = create({ :gender => :m, :name => 'Gaius Iulius Caesar' })
25
+ assert julius.could_be? Person
26
+
27
+ julius["birth_date"] = "100 BC"
28
+ julius["birth_place"] = "Rome"
29
+
30
+ assert julius.could_be? Born
31
+
32
+ julius.is(Born)
33
+ julius.is(Person)
34
+ julius.save
35
+ julius.reload
36
+
37
+ assert julius.is? Person
38
+ assert julius.is? Born
39
+
40
+ assert_equal 'Gaius Iulius Caesar', julius["name"]
41
+ assert_equal 'Rome', julius["birth_place"]
42
+
43
+ assert !julius["roles"].nil?
44
+ assert_equal 2, julius["roles"].size
45
+ assert julius["roles"].include? Person.to_s
46
+ assert julius["roles"].include? Born.to_s
47
+ assert_equal "100 BC", julius.is(Born).born_at
48
+
49
+ return julius
50
+ end
@@ -0,0 +1,81 @@
1
+ require_relative 'bear_julius.rb'
2
+
3
+ # he needs a marriage and a women
4
+ class Married < ThingTank::Role
5
+ property :date # the date of the marriage
6
+ property :end # when the marriage ended
7
+ property :spouse # doc_id of the spouse
8
+ property :state # state of the marriage
9
+
10
+ validates_presence_of :date
11
+ validates_presence_of :spouse
12
+ validate :spouse_should_be_a_person
13
+
14
+ # ensure that 'spouse' is a doc_id of a Person
15
+ def spouse_should_be_a_person
16
+ Person.get(self["spouse"]).valid?
17
+ end
18
+ end
19
+
20
+ # we want easy access to the name of the spouse
21
+ class Spouse < ThingTank::Role
22
+ property :married
23
+ property :married_state
24
+
25
+ validate :spouse_should_be_married
26
+
27
+ def married
28
+ self["married"]
29
+ end
30
+
31
+ def spouse_should_be_married
32
+ married["spouse"] == self["_id"]
33
+ end
34
+
35
+ def name
36
+ self["married_state"] == "married" ?
37
+ Person.get(married["spouse"]).name :
38
+ nil
39
+ end
40
+
41
+ def ex
42
+ self["married_state"] == "divorced" ?
43
+ Person.get(married["spouse"]).name :
44
+ nil
45
+ end
46
+ end
47
+
48
+ def test_first_marriage()
49
+ julius = test_bear_julius
50
+ conny = create :gender => "f", :name => 'Cornelia', :roles => ['Person']
51
+
52
+ julius["married"] = {"date" => "84 BC", "spouse" => conny.id}
53
+ julius["married_state"] = "married"
54
+
55
+ assert julius.is(Married, "married").valid?
56
+ assert julius.has(Spouse).valid?
57
+
58
+ julius.save
59
+ julius.reload
60
+
61
+ assert_equal 'Cornelia', julius.has(Spouse).name
62
+
63
+ end
64
+
65
+
66
+ def test_first_marriage_shortcuts()
67
+ julius = test_bear_julius
68
+ conny = create :gender => "f", :name => 'Cornelia', :roles => ['Person']
69
+
70
+ julius["married"] = {"date" => "84 BC", "spouse" => conny.id}
71
+ julius["married_state"] = "married"
72
+
73
+ assert (julius>Married-"married").valid?
74
+ assert (julius>Spouse).valid?
75
+
76
+ julius.save
77
+ julius.reload
78
+
79
+ assert_equal 'Cornelia', (julius>Spouse).name
80
+
81
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'bear_julius.rb'
2
+
3
+ class Undestroyable < ThingTank::Role
4
+ before_destroy do
5
+ false # never allow to destroy
6
+ end
7
+ end
8
+
9
+
10
+
11
+ def test_julius_immortal
12
+ julius = test_bear_julius()
13
+ julius.is(Undestroyable)
14
+ julius.save # save the role
15
+
16
+ id = julius.id
17
+ julius = load id
18
+
19
+ julius.as(Undestroyable).destroy
20
+ assert !load(id).nil? # => julius is still there
21
+
22
+ julius.destroy
23
+
24
+ assert !load(id).nil? # => julius is still there
25
+ end
@@ -0,0 +1,120 @@
1
+ require_relative 'first_marriage.rb'
2
+
3
+ require 'ruby-debug'
4
+ class Married
5
+
6
+ # marry a doc or hash
7
+ def marry(person)
8
+ person = ThingTank.new(person) if person.is_a?(Hash)
9
+ person.save
10
+
11
+ # assign the doc_id to spouse
12
+ self["spouse"] = person["_id"]
13
+ self["state"] = 'married'
14
+ _doc["married_state"] = 'married'
15
+
16
+ unless person["married"] && person.last_role(Married, "married").spouse == _doc["_id"]
17
+ person.add_role(Married, "married") do |m|
18
+ m.date = self["date"]
19
+ m.marry _doc
20
+ end
21
+ person.is(Spouse)
22
+ person.save
23
+ end
24
+ end
25
+
26
+ def divorce(date)
27
+ self["state"] = 'divorced'
28
+ self['end'] = date
29
+ _doc.save
30
+ spouse = Spouse.get(self["spouse"])
31
+ if spouse.married_state == "married"
32
+ spouse.divorce(date)
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ class Spouse
39
+ def married(&code)
40
+ _doc.last_role Married, "married", &code
41
+ end
42
+
43
+ def divorce(date)
44
+ self["married_state"] = 'divorced'
45
+ married { |m| m.divorce(date) }
46
+ end
47
+ end
48
+
49
+ class Person
50
+ def marry(date, person)
51
+ _doc.add_role Married, "married", do |m|
52
+ m.date = "84 BC"
53
+ m.marry person
54
+ end
55
+ end
56
+ end
57
+
58
+ def test_improved_marriage()
59
+ julius = test_bear_julius()
60
+ julius.as(Person).marry "84 BC", :gender => "f", :name => 'Cornelia'
61
+
62
+ julius.save
63
+ julius.reload
64
+
65
+ assert_equal 'Cornelia', julius.has(Spouse).name
66
+ assert_equal 'married', julius.has(Spouse).married_state
67
+
68
+ conny = load julius.last_role(Married,"married").spouse
69
+
70
+ assert_equal 'Gaius Iulius Caesar', conny.has(Spouse).name
71
+ assert_equal 'married', conny.has(Spouse).married_state
72
+
73
+ assert_equal julius['_id'], conny.last_role(Married,"married").spouse
74
+ assert_equal "84 BC", conny.last_role(Married,"married").date
75
+
76
+ return julius
77
+ end
78
+
79
+ def test_improved_marriage_divorce()
80
+ julius = test_bear_julius()
81
+ julius.as(Person).marry "84 BC", :gender => "f", :name => 'Cornelia'
82
+ julius.as(Spouse).divorce "85 BC"
83
+
84
+ assert_equal nil, julius.has(Spouse).name
85
+ assert_equal 'Cornelia', julius.has(Spouse).ex
86
+ assert_equal 'divorced', julius["married_state"]
87
+ assert_equal "85 BC", julius.last_role(Married,"married").end
88
+
89
+ conny = load julius.last_role(Married,"married").spouse
90
+
91
+ assert_equal nil, conny.has(Spouse).name
92
+ assert_equal 'Gaius Iulius Caesar', conny.has(Spouse).ex
93
+ assert_equal 'divorced', conny["married_state"]
94
+
95
+ assert_equal julius['_id'], conny.last_role(Married,"married").spouse
96
+ assert_equal "85 BC", conny.last_role(Married,"married").end
97
+ return julius
98
+ end
99
+
100
+ def test_improved_marriage_shortcuts()
101
+ julius = test_bear_julius()
102
+ (julius>Person).marry "84 BC", :gender => "f", :name => 'Cornelia'
103
+
104
+ julius.save
105
+ julius.reload
106
+
107
+ assert_equal 'Cornelia', (julius>Spouse).name
108
+ assert_equal 'married', (julius>Spouse).married_state
109
+
110
+ conny = load (julius>Married-"married").spouse
111
+
112
+ assert_equal 'Gaius Iulius Caesar', (conny>Spouse).name
113
+ assert_equal 'married', (conny>Spouse).married_state
114
+
115
+ assert_equal julius['_id'], (conny>Married-"married").spouse
116
+ assert_equal "84 BC", (conny>Married-"married").date
117
+
118
+ return julius
119
+ end
120
+