ricordami 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +13 -0
- data/ISSUES.md +0 -0
- data/MIT-LICENSE +21 -0
- data/README.md +454 -0
- data/TODO.md +21 -0
- data/examples/calls.rb +64 -0
- data/examples/singers.rb +42 -0
- data/lib/ricordami/attribute.rb +52 -0
- data/lib/ricordami/can_be_queried.rb +133 -0
- data/lib/ricordami/can_be_validated.rb +27 -0
- data/lib/ricordami/can_have_relationships.rb +152 -0
- data/lib/ricordami/configuration.rb +39 -0
- data/lib/ricordami/connection.rb +22 -0
- data/lib/ricordami/exceptions.rb +14 -0
- data/lib/ricordami/has_attributes.rb +159 -0
- data/lib/ricordami/has_indices.rb +105 -0
- data/lib/ricordami/is_lockable.rb +52 -0
- data/lib/ricordami/is_persisted.rb +123 -0
- data/lib/ricordami/is_retrievable.rb +35 -0
- data/lib/ricordami/key_namer.rb +63 -0
- data/lib/ricordami/model.rb +29 -0
- data/lib/ricordami/query.rb +68 -0
- data/lib/ricordami/relationship.rb +40 -0
- data/lib/ricordami/unique_index.rb +59 -0
- data/lib/ricordami/unique_validator.rb +21 -0
- data/lib/ricordami/value_index.rb +26 -0
- data/lib/ricordami/version.rb +3 -0
- data/lib/ricordami.rb +26 -0
- data/spec/acceptance/manage_relationships_spec.rb +42 -0
- data/spec/acceptance/model_with_validation_spec.rb +78 -0
- data/spec/acceptance/query_model_spec.rb +93 -0
- data/spec/acceptance_helper.rb +2 -0
- data/spec/ricordami/attribute_spec.rb +113 -0
- data/spec/ricordami/can_be_queried_spec.rb +254 -0
- data/spec/ricordami/can_be_validated_spec.rb +115 -0
- data/spec/ricordami/can_have_relationships_spec.rb +255 -0
- data/spec/ricordami/configuration_spec.rb +45 -0
- data/spec/ricordami/connection_spec.rb +25 -0
- data/spec/ricordami/exceptions_spec.rb +43 -0
- data/spec/ricordami/has_attributes_spec.rb +266 -0
- data/spec/ricordami/has_indices_spec.rb +73 -0
- data/spec/ricordami/is_lockable_spec.rb +45 -0
- data/spec/ricordami/is_persisted_spec.rb +186 -0
- data/spec/ricordami/is_retrievable_spec.rb +55 -0
- data/spec/ricordami/key_namer_spec.rb +56 -0
- data/spec/ricordami/model_spec.rb +65 -0
- data/spec/ricordami/query_spec.rb +156 -0
- data/spec/ricordami/relationship_spec.rb +123 -0
- data/spec/ricordami/unique_index_spec.rb +87 -0
- data/spec/ricordami/unique_validator_spec.rb +41 -0
- data/spec/ricordami/value_index_spec.rb +40 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/constants.rb +43 -0
- data/spec/support/db_manager.rb +18 -0
- data/test/bin/data_loader.rb +107 -0
- data/test/data/domains.txt +462 -0
- data/test/data/first_names.txt +1220 -0
- data/test/data/last_names.txt +1028 -0
- data/test/data/people_100_000.csv.bz2 +0 -0
- data/test/data/people_10_000.csv.bz2 +0 -0
- data/test/data/people_1_000_000.csv.bz2 +0 -0
- 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&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&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&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}"
|
data/examples/singers.rb
ADDED
@@ -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
|