ricordami 0.0.1

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