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.
- 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
|