ricordami 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.
Files changed (62) hide show
  1. data/CHANGELOG.md +13 -0
  2. data/ISSUES.md +0 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +454 -0
  5. data/TODO.md +21 -0
  6. data/examples/calls.rb +64 -0
  7. data/examples/singers.rb +42 -0
  8. data/lib/ricordami/attribute.rb +52 -0
  9. data/lib/ricordami/can_be_queried.rb +133 -0
  10. data/lib/ricordami/can_be_validated.rb +27 -0
  11. data/lib/ricordami/can_have_relationships.rb +152 -0
  12. data/lib/ricordami/configuration.rb +39 -0
  13. data/lib/ricordami/connection.rb +22 -0
  14. data/lib/ricordami/exceptions.rb +14 -0
  15. data/lib/ricordami/has_attributes.rb +159 -0
  16. data/lib/ricordami/has_indices.rb +105 -0
  17. data/lib/ricordami/is_lockable.rb +52 -0
  18. data/lib/ricordami/is_persisted.rb +123 -0
  19. data/lib/ricordami/is_retrievable.rb +35 -0
  20. data/lib/ricordami/key_namer.rb +63 -0
  21. data/lib/ricordami/model.rb +29 -0
  22. data/lib/ricordami/query.rb +68 -0
  23. data/lib/ricordami/relationship.rb +40 -0
  24. data/lib/ricordami/unique_index.rb +59 -0
  25. data/lib/ricordami/unique_validator.rb +21 -0
  26. data/lib/ricordami/value_index.rb +26 -0
  27. data/lib/ricordami/version.rb +3 -0
  28. data/lib/ricordami.rb +26 -0
  29. data/spec/acceptance/manage_relationships_spec.rb +42 -0
  30. data/spec/acceptance/model_with_validation_spec.rb +78 -0
  31. data/spec/acceptance/query_model_spec.rb +93 -0
  32. data/spec/acceptance_helper.rb +2 -0
  33. data/spec/ricordami/attribute_spec.rb +113 -0
  34. data/spec/ricordami/can_be_queried_spec.rb +254 -0
  35. data/spec/ricordami/can_be_validated_spec.rb +115 -0
  36. data/spec/ricordami/can_have_relationships_spec.rb +255 -0
  37. data/spec/ricordami/configuration_spec.rb +45 -0
  38. data/spec/ricordami/connection_spec.rb +25 -0
  39. data/spec/ricordami/exceptions_spec.rb +43 -0
  40. data/spec/ricordami/has_attributes_spec.rb +266 -0
  41. data/spec/ricordami/has_indices_spec.rb +73 -0
  42. data/spec/ricordami/is_lockable_spec.rb +45 -0
  43. data/spec/ricordami/is_persisted_spec.rb +186 -0
  44. data/spec/ricordami/is_retrievable_spec.rb +55 -0
  45. data/spec/ricordami/key_namer_spec.rb +56 -0
  46. data/spec/ricordami/model_spec.rb +65 -0
  47. data/spec/ricordami/query_spec.rb +156 -0
  48. data/spec/ricordami/relationship_spec.rb +123 -0
  49. data/spec/ricordami/unique_index_spec.rb +87 -0
  50. data/spec/ricordami/unique_validator_spec.rb +41 -0
  51. data/spec/ricordami/value_index_spec.rb +40 -0
  52. data/spec/spec_helper.rb +29 -0
  53. data/spec/support/constants.rb +43 -0
  54. data/spec/support/db_manager.rb +18 -0
  55. data/test/bin/data_loader.rb +107 -0
  56. data/test/data/domains.txt +462 -0
  57. data/test/data/first_names.txt +1220 -0
  58. data/test/data/last_names.txt +1028 -0
  59. data/test/data/people_100_000.csv.bz2 +0 -0
  60. data/test/data/people_10_000.csv.bz2 +0 -0
  61. data/test/data/people_1_000_000.csv.bz2 +0 -0
  62. metadata +258 -0
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog #
2
+
3
+ ## 0.0.1 (March 5th, 2011)
4
+
5
+ Initial release.
6
+
7
+ Features:
8
+
9
+ - keeps track of dirty attributes
10
+ - persist models to 1 Redis instance
11
+ - can include model 1 to 1 and 1 to many relationships
12
+ - can validate with unique index enforced in Redis
13
+ - can query using queries with and/or/not attribute equality conditions
data/ISSUES.md ADDED
File without changes
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2004-2010 David Heinemeier Hansson
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.
21
+
data/README.md ADDED
@@ -0,0 +1,454 @@
1
+ # Ricordami: store and query Ruby objects using Redis #
2
+
3
+ Ricordami ("Remember me" in Italian) is an attempt at providing a simple
4
+ interface to build Ruby objects that can be validated, persisted and
5
+ queried in a Redis data structure server.
6
+
7
+ <div style="color: red">NOTE: This gem is in active development and is
8
+ not ready for use yet.</div>
9
+
10
+
11
+ ## What Does It Look Like? ##
12
+
13
+ require "ricordami"
14
+
15
+ Ricordami::Model.configure do |config|
16
+ config.redis_host = "127.0.0.1"
17
+ config.redis_port = 6379
18
+ config.redis_db = 0
19
+ end
20
+
21
+ class Singer
22
+ include Ricordami::Model
23
+
24
+ model_can :be_validated, :have_relationships
25
+
26
+ attribute :name
27
+
28
+ validates_presence_of :name
29
+ validates_uniqueness_of :name
30
+
31
+ references_many :songs
32
+ end
33
+
34
+ class Song
35
+ include Ricordami::Model
36
+
37
+ model_can :be_queried, :have_relationships
38
+
39
+ attribute :title, :indexed => :unique, :get_by => true
40
+ attribute :year
41
+
42
+ referenced_in :singer
43
+ end
44
+
45
+ serge = Singer.create :name => "Gainsbourg"
46
+ jetaime = serge.songs.create :title => "Je T'Aime Moi Non Plus", :year => "1967"
47
+ jetaime.year = "1968"
48
+ jetaime.changes # => {:year => ["1967", "1968"]}
49
+ jetaime.save
50
+ ["La Javanaise", "Melody Nelson", "Love On The Beat"].each do |name|
51
+ serge.songs.create :title => name, :year => "1962"
52
+ end
53
+ Song.get_by_title("Melody Nelson").update_attributes(:year => "1971")
54
+ Song.get_by_title("Love On The Beat").update_attributes(:year => "1984")
55
+
56
+ Song.count # => 3
57
+ Song.where(:year => "1971").map(&:title) # => "Melody Nelson"
58
+
59
+
60
+ ## How To Install? ##
61
+
62
+
63
+ Ricordami is tested against the following versions of Ruby:
64
+
65
+ - MRI 1.9.2
66
+ - Ruby Enterprise 1.8.7
67
+ - Rubinius 1.2.2
68
+
69
+ and Redis 2.2.x.
70
+
71
+ Install using bundler:
72
+
73
+ In your **Gemfile** file:
74
+
75
+ gem "ricordami"
76
+
77
+ And just run:
78
+
79
+ $ bundle
80
+
81
+ Directly with Rubygems:
82
+
83
+ $ gem install ricordami
84
+
85
+ ## Features ##
86
+
87
+ Here is a quick description for each main feature.
88
+
89
+ ### Configuration ###
90
+
91
+ Ricordami can be configured in two ways. With values stored in the
92
+ source code:
93
+
94
+ Ricordami::Model.configure do |config|
95
+ config.redis_host = "redis.lab"
96
+ config.redis_port = 6379
97
+ config.thread_safe = true
98
+ end
99
+
100
+ Or using a hash:
101
+
102
+ Ricordami.configure do |config|
103
+ config.from_hash(YAML.load("config.yml"))
104
+ end
105
+
106
+ ### Declare A Model ###
107
+
108
+ You just need to require **"ricordami"** and include the
109
+ **Ricordami::Model** module into the model class. You can also include
110
+ additional features using the class method **#model_can**.
111
+
112
+ class Asset
113
+ include Ricordami::Model
114
+ model_can :be_validated,
115
+ :be_queried,
116
+ :have_relationships
117
+ end
118
+
119
+ ### Declare Attributes ###
120
+
121
+ The model state is stored in attributes. Those attributes can be indexed
122
+ in order to query the models later on, or enforce the unicity of certain
123
+ attributes. Each model gets a default attribute **id** that is a unique
124
+ sequence set automatically when the model is saved into Redis. It is
125
+ possible to override this attribute by redeclaring it with different
126
+ options.
127
+
128
+ An attribute is declared using the class method **#attribute** and takes
129
+ the following options:
130
+
131
+ - *:default* - that's the value the attribute will take when it is not
132
+ specified - it can be a value or a Proc (or any object responding to
133
+ **#call**) that will return the value
134
+ - *:initial* - similar to *:default* but rather than used when the
135
+ model is instanciated, it is used when it is persisted to Redis
136
+ - *:read_only* - this attribute can be set only once, after that you
137
+ are certified it will not change
138
+ - *:indexed** - this attribute will be indexed as unique to enforce
139
+ unicity (*:indexed => :unique*) or as value (*:indexed => :value*)
140
+ to allow querying the model (using where/and/any/not)
141
+ - *:type* - attribute type is a string by default (*:string*) but can
142
+ also be an integer (*:integer*) or a float (*:float*)
143
+
144
+ Example:
145
+
146
+ class Person
147
+ include Ricordami::Model
148
+ attribute :name, :default => "First name, Last name"
149
+ attribute :sex, :indexed => :value
150
+ attribute :age, :type => :integer
151
+ end
152
+
153
+ zhanna = Person.create(:name => "Zhanna", :sex => "Female", :age => 29
154
+ zhanna.id # => 42
155
+ Person[42].name # => "Zhanna"
156
+ Person.get_by_id(42).name # => "Zhanna"
157
+
158
+ Methods:
159
+
160
+ - save: persists the model to Redis (attributes and indices added
161
+ in one atomic operation)
162
+ - update_attributes: update the value of the attributes passed and
163
+ saves the model to Redis
164
+ - delete: deletes the model from Redis (attributes and indices are
165
+ removed in one atomic operation)
166
+
167
+ ### Declare Indices ###
168
+
169
+ It is also possible to declare an index using the class method
170
+ **#index** to add index specific options, or conditionnaly index an
171
+ attribute dynamically. The only option currently supported is *:get_by*
172
+ which is used for unique indices in order to request generating
173
+ a **get_by_xxx** class method used to fetch a model instance by its
174
+ unique value.
175
+
176
+ Example:
177
+
178
+ class Person
179
+ include Ricordami::Model
180
+ attribute :name
181
+ index :name => :unique, :get_by => true
182
+ end
183
+
184
+ zhanna = Person.get_by_name("Zhanna")
185
+
186
+ ### Validation Rules ###
187
+
188
+ Ricordami relies on the validation capabilities offered by Active Model,
189
+ so you can refer to Rails documentation pages for
190
+ [ActiveModel::Validations](http://api.rubyonrails.org/classes/ActiveModel/Validations.html) and
191
+ [ActiveModel::Validations::HelperMethods](http://api.rubyonrails.org/classes/ActiveModel/Validations/HelperMethods.html).
192
+
193
+ Note: when using the **#validates_uniqueness_of** macro, Ricordami
194
+ automatically adds a value index to the column it it is not done
195
+ already.
196
+
197
+ Example:
198
+
199
+ class Singer
200
+ include Ricordami::Model
201
+ model_can :be_validated
202
+
203
+ attribute :username
204
+ attribute :email
205
+ attribute :first_name
206
+ attribute :last_name
207
+ attribute :deceased, :default => "false", :indexed => :value
208
+
209
+ validates_presence_of :username, :email, :deceased
210
+ validates_uniqueness_of :username
211
+ validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
212
+ :allow_blank => true, :message => "is not a valid email"
213
+ validates_inclusion_of :deceased, :in => ["true", "false"]
214
+ end
215
+
216
+ ### Relationships ###
217
+
218
+ Ricordami handles two kind of relationships: one to one and one to many.
219
+ You declare a referrer model to have many of a referenced model using
220
+ the class method **#references_many**. It gives the referrer instances
221
+ access to an instance method of the plural name of the reference. This
222
+ method can be used to fetch the list of reference objects, build or
223
+ create a new one, or query the list (see next section for querying).
224
+
225
+ You declare the referenced method using the class method
226
+ **#referenced_in**, which creates one method of the name of the referrer
227
+ to fetch it. It also creates two other methods **#build_xxx** and
228
+ **#create_xxx** where xxx is the referrer name. Finally it declares a
229
+ new attribute *xxx_id* where xxx is the name or the alias of the
230
+ referrer.
231
+
232
+ Finally you can setup a one to one relationship using
233
+ **#references_one** and **#referenced_in**. **#references_one** gives
234
+ the referrer access to the same type of methods than **#referenced_in**.
235
+
236
+ Better go with an example to make it all clear:
237
+
238
+ class Singer
239
+ include Ricordami::Model
240
+ model_can :have_relationships
241
+ attribute :name
242
+
243
+ references_many :songs
244
+ end
245
+
246
+ class Song
247
+ include Ricordami::Model
248
+ model_can :have_relationships
249
+ attribute :title
250
+
251
+ referenced_in :singer
252
+ end
253
+
254
+ bashung = Singer.create(:name => "Alain Bashung")
255
+ bashung.songs # => []
256
+ osez = bashung.songs.build(:title => "Osez Josephine")
257
+ osez.save
258
+ gaby = bashung.songs.create(:title => "Vertiges de l'Amour")
259
+ bashung.songs.map(&:title) # => ["Osez Josephine", "Vertiges de l'Amour"]
260
+ gaby.singer_id == bashung.id # => true
261
+
262
+ padam = Song.create(:title => "Padam")
263
+ benjamin = padam.create_singer(:name => "Benjamin Biolay")
264
+ benjamin.songs.map(&:title) # => "Padam"
265
+
266
+ The class methods **#references_many**, **#references_one** and
267
+ **#referenced_by** can take the following options:
268
+
269
+ - *:as* - used to give a different name to the other party in the
270
+ relationship
271
+ - *:alias* - used to give a differnt name of itself to the other party
272
+ in the relationship - there must be a mapping: if A references_many
273
+ B as Ben and B is referenced_in A as Al, references_many must have
274
+ an alias Al and referenced_in must have an alias Ben.
275
+ - *:dependent* - only used for :references_one and :references_many
276
+ relationships - it is possible to set to :nullify so all dependents
277
+ get their referrer id set to nil when the referrer is deleted, or to
278
+ :delete to have them all deleted instead when the referrer is
279
+ deleted
280
+
281
+ ### Basic Queries ###
282
+
283
+ It is possible to create basic queries and sort the result list of
284
+ models. Please note that the queries currently available are quite
285
+ limited but might be enhanced in the future. Currently any kind of
286
+ querying more advanced than what is described here would have to be
287
+ implemented using directly the Redis gem and Redis native commands.
288
+
289
+ The querying feature adds the following class methods that can be
290
+ chained together:
291
+
292
+ - *#when*/*#and*: pass a hash of equalities, the result will be the
293
+ list of items that matches ALL the parameter equalities at once
294
+ - *#any*: pass a hash of equalities, the result will be the list of
295
+ items that matches ANY of the parameter equalities
296
+ - *#not*: pass a hash of equalities, the result will be the list of
297
+ items that matches NONE of the parameter equalities
298
+ - *#sort*: sorts the result based on the attribute passed, using the
299
+ default ascending alphanumeric order (:inc_alpha) - the other
300
+ possible orders are: :desc_num, :desc_alpha, :asc_num and :asc
301
+ :desc_alpha
302
+ - *#first*, *#last*, *#rand* and *#all* can be called on any sort
303
+ query result to fetch the desired result
304
+
305
+ The methods *#and* (and alias *#when*), *#any* and *#not* create
306
+ intermediate Redis sets
307
+
308
+ Example: we have a tenant model that represent user accounts on a
309
+ telephony service application. A tenant has many phone calls that are
310
+ made on the platform. Each phone call that goes through the platform is
311
+ made from a phone number called the ANI (calling number), to another
312
+ phone number called the DNIS (number called). Each call can be using the
313
+ Plain Old Telephone Service (pots) or Voice Over IP (voip), and lasts
314
+ for a number of seconds. And finally each call goes through the network
315
+ of an operator among AT&amp;T, Qwest and Level3.
316
+
317
+ class Tenant
318
+ include Ricordami::Model
319
+ model_can :be_queried, :be_validated, :have_relationships
320
+
321
+ attribute :name, :read_only => true
322
+ index :unique => :name, :get_by => true
323
+
324
+ references_many :calls, :alias => :owner, :dependent => :delete
325
+
326
+ validates_presence_of :name
327
+ validates_uniqueness_of :name
328
+ end
329
+
330
+ class Call
331
+ include Ricordami::Model
332
+ model_can :be_queried, :be_validated, :have_relationships
333
+
334
+ attribute :ani, :indexed => :value
335
+ attribute :dnis, :indexed => :value
336
+ attribute :call_type, :indexed => :value
337
+ attribute :network, :indexed => :value
338
+ attribute :seconds, :type => :integer
339
+
340
+ referenced_in :tenant, :as owner
341
+
342
+ validates_presence_of :call_type, :seconds, :owner_id
343
+ validates_inclusion_of :call_type, :in => ["pots", "voip"]
344
+ validates_inclusion_of :network, :in => ["att", "qwest", "level3"]
345
+ end
346
+
347
+ # what is the total number of seconds of the phone calls made from
348
+ # the phone number 650 123 4567?
349
+ Call.where(:ani => "6501234567").inject(0) { |sum, call| sum + call.seconds }
350
+
351
+ # what are the VoIP calls that didn't go through Level3 network?
352
+ Call.where(:call_type => "voip").not(:network => "level3").all
353
+
354
+ # what are the calls for tenant "mycompany" that went through
355
+ # AT&amp;T's network or originated from ANI 408 123 4567? but were
356
+ # not VoIP calls?
357
+ mycompany = Tenant.get_by_name("mycompany")
358
+ mycompany.calls.any(:ani => "4081234567", :network => "att").not(:call_type => "voip").all
359
+
360
+
361
+ ## How To Run Specs ##
362
+
363
+ $ bundle exec rspec spec
364
+ $ rake rspec
365
+ $ bundle exec autotest
366
+
367
+
368
+ ### Multiple Ruby Versions ###
369
+
370
+ Infinity test is like autotest for testing with several versions of Ruby
371
+ rather than just one. It requires using rvm to install and manage
372
+ multiple Ruby versions.
373
+
374
+
375
+ First you need to install the ruby versions (only install those that are
376
+ missing of course). For each version we create a new gemset which
377
+ basically acts as a gem sandbox that won't affect the other work you do
378
+ on the same machine.
379
+
380
+ Ruby Enterprise:
381
+
382
+ $ rvm install ree-1.8.7-2011.03 # install if necessary
383
+ $ rvm use ree-1.8.7-2011.03
384
+ $ rvm gemset create ricordami
385
+ $ gem install bundler --no-ri --no-rdoc
386
+ $ rvm gemset use ricordami
387
+ $ bundle
388
+
389
+ Rubinius:
390
+
391
+ $ rvm install rbx-1.2.2 # install if necessary
392
+ $ rvm use rbx-1.2.2
393
+ $ rvm gemset create ricordami
394
+ $ rvm gemset use ricordami
395
+ $ gem install bundler --no-ri --no-rdoc
396
+ $ bundle
397
+
398
+ MRI 1.9.2:
399
+
400
+ $ rvm install 1.9.2-p180 # install if necessary
401
+ $ rvm use 1.9.2-p180
402
+ $ rvm gemset create ricordami
403
+ $ rvm gemset use ricordami
404
+ $ gem install bundler --no-ri --no-rdoc
405
+ $ bundle
406
+
407
+ Run the infinity test:
408
+
409
+ $ bundle exec infinity_test
410
+
411
+
412
+ ## Why Ricordami? ##
413
+
414
+ Ricordami's design goal is to find the best trade off between speed and
415
+ features. Its syntax goal is to be close enough to ORMs such as Active
416
+ Record or Mongoid, so the learning curve stays pretty small.
417
+
418
+ Ricordami is NOT an attempt at competing with full featured ORMs such as
419
+ Active Record or Data Mapper for relational databases, or Mongoid or
420
+ Mongo Mapper for MongoDB.
421
+
422
+ I started Ricordami because I needed to scale and distribute an
423
+ event based application accross many servers. I decided to use
424
+ the REST-like API micro framework Grape to structure the API, and chose
425
+ Redis to externalize and hold the application state. I needed a library
426
+ to structure the data layer and didn't find any library that would work
427
+ for me. If I would have searched a bit more I would have found Ohm
428
+ (http://ohm.keyvalue.org/) and the story would have stopped here.
429
+
430
+
431
+ ## Thanks ##
432
+
433
+ First of all thanks to Salvatore Sanfilippo ([@antirez](http://twitter.com/antirez))
434
+ for Redis. Redis is sucn an amazing application, it makes you want to
435
+ write things for it just for the fun of playing with it.
436
+
437
+ Also I might not have started Ricordami without the amazing work done
438
+ and shared by the Rails team, especially DHH, Yehuda Katz and Carl Huda.
439
+ ActiveSupport and ActiveModel are just amazingly flexible and so easy to
440
+ build on. Also I might never have heard of great resources like
441
+ [Grape](https://github.com/intridea/grape) and
442
+ [Infinity Test](https://github.com/tomas-stefano/infinity_test) without
443
+ the podcasts [Ruby5](http://ruby5.envylabs.com/),
444
+ [ChangeLog](http://thechangelog.com/) and [The Ruby Show](http://rubyshow.com/).
445
+
446
+
447
+ ## License ##
448
+
449
+ Released under the MIT License. See the MIT-LICENSE file for further
450
+ details.
451
+
452
+ ## Copyright ##
453
+
454
+ Copyright (c) 2011 Mathieu Lajugie
data/TODO.md ADDED
@@ -0,0 +1,21 @@
1
+ # TODO list #
2
+
3
+ ## Documentation ##
4
+
5
+ - add inline documentation to the code and add task to generate RDoc
6
+ - add full blown example with grape and new gem to limit concurrent
7
+ requests
8
+
9
+ ## Development ##
10
+
11
+ - add support for booleans and datetimes
12
+ - add support for native types (list, hash, set, sorted set)
13
+
14
+ ## Maybe ##
15
+
16
+ - add external observers using pubsub?
17
+ - add generic pubsub to broadcast events/requests to subscribers
18
+ - add blocking lists to target events/requests to target pools
19
+
20
+ ## Bugs ##
21
+
data/examples/calls.rb ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+ #$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
3
+
4
+ require "rubygems"
5
+ require "ricordami"
6
+
7
+ Ricordami.configure do |config|
8
+ config.redis_host = "127.0.0.1"
9
+ config.redis_port = 6379
10
+ config.redis_db = 15
11
+ end
12
+ Ricordami.driver.flushdb
13
+
14
+ class Tenant
15
+ include Ricordami::Model
16
+ model_can :be_queried, :be_validated, :have_relationships
17
+
18
+ attribute :name, :read_only => true
19
+ index :unique => :name, :get_by => true
20
+
21
+ references_many :calls, :alias => :owner, :dependent => :delete
22
+
23
+ validates_presence_of :name
24
+ validates_uniqueness_of :name
25
+ end
26
+
27
+ class Call
28
+ include Ricordami::Model
29
+ model_can :be_queried, :be_validated, :have_relationships
30
+
31
+ attribute :ani, :indexed => :value
32
+ attribute :dnis, :indexed => :value
33
+ attribute :call_type, :indexed => :value
34
+ attribute :network, :indexed => :value
35
+ attribute :seconds, :type => :integer
36
+
37
+ referenced_in :tenant, :as => :owner
38
+
39
+ validates_presence_of :call_type, :seconds, :owner_id
40
+ validates_inclusion_of :call_type, :in => ["pots", "voip"]
41
+ validates_inclusion_of :network, :in => ["att", "qwest", "level3"]
42
+ end
43
+
44
+ t = Tenant.create(:name => "mycompany")
45
+ t.calls.create(:ani => "6501234567", :dnis => "911", :call_type => "pots", :network => "qwest", :seconds => 42)
46
+ # TODO: create more calls
47
+
48
+ puts <<EOC
49
+ ?? What is the total number of seconds of the phone calls made from the phone number 650 123 4567?
50
+ EOC
51
+ seconds = Call.where(:ani => "6501234567").inject(0) { |sum, call| sum + call.seconds }
52
+ puts " => seconds = #{seconds}"
53
+
54
+ puts "?? What are the VoIP calls that didn't go through Level3 network?"
55
+ calls = Call.where(:call_type => "voip").not(:network => "level3")
56
+ puts " => #{calls.inspect}"
57
+
58
+ puts <<EOC
59
+ ?? What are the calls for tenant "mycompany" that went through AT&amp;T's network or originated from ANI 408 123 4567? but were not VoIP calls?
60
+ EOC
61
+ mycompany = Tenant.get_by_name("mycompany")
62
+ calls = mycompany.calls.any(:ani => "4081234567", :network => "att").not(:call_type => "voip")
63
+ puts " => #{calls.count} calls"
64
+ puts " => first page of 10: #{calls.paginate(:page => 1, :per_page => 10).inspect}"
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+ #$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
3
+
4
+ require "rubygems"
5
+ require "ricordami"
6
+
7
+ Ricordami.configure do |config|
8
+ config.redis_host = "127.0.0.1"
9
+ config.redis_port = 6379
10
+ config.redis_db = 0
11
+ end
12
+ Ricordami.driver.flushdb
13
+
14
+ class Singer
15
+ include Ricordami::Model
16
+ model_can :have_relationships
17
+ attribute :name
18
+
19
+ references_many :songs
20
+ end
21
+
22
+ class Song
23
+ include Ricordami::Model
24
+ model_can :have_relationships
25
+ attribute :title
26
+
27
+ referenced_in :singer
28
+ end
29
+
30
+ bashung = Singer.create(:name => "Alain Bashung")
31
+ bashung.songs # => []
32
+ osez = bashung.songs.build(:title => "Osez Josephine")
33
+ osez.save
34
+ gaby = bashung.songs.create(:title => "Vertiges de l'Amour")
35
+ p bashung.songs.map(&:title) # => ["Osez Josephine", "Vertiges de l'Amour"]
36
+ p gaby.singer_id == bashung.id # => true
37
+
38
+ padam = Song.create(:title => "Padam")
39
+ p :padam, padam
40
+ benjamin = padam.build_singer(:name => "Benjamin Biolay")
41
+ p :benjamin, benjamin
42
+ p benjamin.songs.map(&:title) # => "Padam"
@@ -0,0 +1,52 @@
1
+ require "ricordami/key_namer"
2
+
3
+ module Ricordami
4
+ class Attribute
5
+ attr_reader :name
6
+
7
+ def initialize(name, options = {})
8
+ options.assert_valid_keys(:default, :read_only, :initial, :indexed, :type)
9
+ if options[:indexed] && ![:value, :unique].include?(options[:indexed])
10
+ raise InvalidIndexDefinition.new(options[:indexed].to_s)
11
+ end
12
+ options[:type] ||= :string
13
+ @options = options
14
+ @name = name.to_sym
15
+ end
16
+
17
+ [:default, :initial].each do |name|
18
+ define_method(:"#{name}_value") do
19
+ return @options[name].call if @options[name].respond_to?(:call)
20
+ @options[name]
21
+ end
22
+
23
+ define_method(:"#{name}_value?") do
24
+ @options.has_key?(name)
25
+ end
26
+ end
27
+
28
+ def read_only?
29
+ !!@options[:read_only]
30
+ end
31
+
32
+ def indexed
33
+ @options[:indexed]
34
+ end
35
+
36
+ def indexed?
37
+ !!@options[:indexed]
38
+ end
39
+
40
+ def type
41
+ @options[:type]
42
+ end
43
+
44
+ def converter
45
+ case @options[:type]
46
+ when :string then :to_s
47
+ when :integer then :to_i
48
+ when :float then :to_f
49
+ end
50
+ end
51
+ end
52
+ end