ohm 0.0.18 → 0.0.19
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +20 -25
- data/lib/ohm.rb +112 -26
- data/test/connection_test.rb +1 -1
- data/test/indices_test.rb +89 -4
- data/test/model_test.rb +8 -0
- data/test/validations_test.rb +3 -3
- metadata +2 -2
data/README.markdown
CHANGED
@@ -135,6 +135,21 @@ with an attribute name, which will determine the sorting order. Both
|
|
135
135
|
methods receive an options hash which is explained in the documentation
|
136
136
|
for {Ohm::Attributes::Collection#sort}.
|
137
137
|
|
138
|
+
Adding instances of `Person` to the attendees hash is done with the
|
139
|
+
`add` method:
|
140
|
+
|
141
|
+
@event.attendees.add(Person.create(name: "Albert"))
|
142
|
+
|
143
|
+
# And now...
|
144
|
+
@event.attendees.each do |person|
|
145
|
+
# ...do what you want with this person.
|
146
|
+
end
|
147
|
+
|
148
|
+
Just to clarify: when you add items to a set with `<<`, Ohm inserts
|
149
|
+
whatever you send without checking it. When you use `add`, it assumes
|
150
|
+
it's an instance of some `Ohm::Model` and stores its id.
|
151
|
+
|
152
|
+
|
138
153
|
Indexes
|
139
154
|
-------
|
140
155
|
|
@@ -144,14 +159,9 @@ Ohm maintains different sets of objects ids for quick lookups.
|
|
144
159
|
For example, in the example above, the index on the name attribute will
|
145
160
|
allow for searches like Event.find(:name, "some value").
|
146
161
|
|
147
|
-
You can also declare an index on multiple colums, like this:
|
148
|
-
|
149
|
-
index [:name, :company]
|
150
|
-
|
151
162
|
Note that the `find` method and the `assert_unique` validation need a
|
152
163
|
corresponding index to exist.
|
153
164
|
|
154
|
-
|
155
165
|
Validations
|
156
166
|
-----------
|
157
167
|
|
@@ -183,43 +193,28 @@ to false.
|
|
183
193
|
Checks that the given field is not nil or empty. The error code for this
|
184
194
|
assertion is :not_present.
|
185
195
|
|
186
|
-
|
187
|
-
assert(!send(att).to_s.empty?, error)
|
188
|
-
end
|
196
|
+
assert_present :name
|
189
197
|
|
190
198
|
### assert_format
|
191
199
|
|
192
200
|
Checks that the given field matches the provided format. The error code
|
193
201
|
for this assertion is :format.
|
194
202
|
|
195
|
-
|
196
|
-
if assert_present(att, error)
|
197
|
-
assert(send(att).to_s.match(format), error)
|
198
|
-
end
|
199
|
-
end
|
203
|
+
assert_format :username, /^\w+$/
|
200
204
|
|
201
205
|
### assert_numeric
|
202
206
|
|
203
207
|
Checks that the given field holds a number as a Fixnum or as a string
|
204
208
|
representation. The error code for this assertion is :not_numeric.
|
205
209
|
|
206
|
-
|
207
|
-
if assert_present(att, error)
|
208
|
-
assert_format(att, /^\d+$/, error)
|
209
|
-
end
|
210
|
-
end
|
210
|
+
assert_numeric :votes
|
211
211
|
|
212
212
|
### assert_unique
|
213
213
|
|
214
214
|
Validates that the attribute or array of attributes are unique.
|
215
|
-
For this, an index of the same kind must exist. The error code is
|
216
|
-
:not_unique.
|
217
|
-
|
218
|
-
def assert_unique(attrs)
|
219
|
-
index_key = index_key_for(Array(attrs), read_locals(Array(attrs)))
|
220
|
-
assert(db.scard(index_key).zero? || db.sismember(index_key, id), [Array(attrs), :not_unique])
|
221
|
-
end
|
215
|
+
For this, an index of the same kind must exist. The error code is :not_unique.
|
222
216
|
|
217
|
+
assert_unique :email
|
223
218
|
|
224
219
|
Errors
|
225
220
|
------
|
data/lib/ohm.rb
CHANGED
@@ -81,7 +81,8 @@ module Ohm
|
|
81
81
|
return [] if empty?
|
82
82
|
options[:start] ||= 0
|
83
83
|
options[:limit] = [options[:start], options[:limit]] if options[:limit]
|
84
|
-
|
84
|
+
result = db.sort(key, options)
|
85
|
+
options[:get] ? result : instantiate(result)
|
85
86
|
end
|
86
87
|
|
87
88
|
# Sort the model instances by the given attribute.
|
@@ -222,6 +223,57 @@ module Ohm
|
|
222
223
|
def size
|
223
224
|
db.scard(key)
|
224
225
|
end
|
226
|
+
|
227
|
+
# Returns an intersection with the sets generated from the passed hash.
|
228
|
+
#
|
229
|
+
# @see Ohm::Model.filter
|
230
|
+
# @yield [results] Results of the filtering. Beware that the set of results is deleted from Redis when the block ends.
|
231
|
+
# @example
|
232
|
+
# Event.search(day: "2009-09-11") do |search_results|
|
233
|
+
# events = search_results.all
|
234
|
+
# end
|
235
|
+
def filter(hash, &block)
|
236
|
+
apply(:sinterstore, keys(hash).push(key), &block)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns a union with the sets generated from the passed hash.
|
240
|
+
#
|
241
|
+
# @see Ohm::Model.search
|
242
|
+
# @yield [results] Results of the search. Beware that the set of results is deleted from Redis when the block ends.
|
243
|
+
# @example
|
244
|
+
# Event.search(day: "2009-09-11") do |search_results|
|
245
|
+
# search_results.filter(public: true) do |filter_results|
|
246
|
+
# events = filter_results.all
|
247
|
+
# end
|
248
|
+
# end
|
249
|
+
def search(hash, &block)
|
250
|
+
apply(:sunionstore, keys(hash), &block)
|
251
|
+
end
|
252
|
+
|
253
|
+
def delete!
|
254
|
+
db.del(key)
|
255
|
+
end
|
256
|
+
|
257
|
+
# Apply a redis operation on a collection of sets. Note that
|
258
|
+
# the resulting set is removed inmediatly after use.
|
259
|
+
def apply(operation, source, &block)
|
260
|
+
target = source.join(operation)
|
261
|
+
db.send(operation, target, *source)
|
262
|
+
set = self.class.new(db, target, model)
|
263
|
+
block.call(set)
|
264
|
+
set.delete!
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
# Transform a hash of attribute/values into an array of keys.
|
270
|
+
def keys(hash)
|
271
|
+
hash.inject([]) do |acc, t|
|
272
|
+
acc + Array(t[1]).map do |v|
|
273
|
+
model.key(t[0], model.encode(v))
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
225
277
|
end
|
226
278
|
end
|
227
279
|
|
@@ -237,8 +289,8 @@ module Ohm
|
|
237
289
|
# @overload assert_unique [:street, :city]
|
238
290
|
# Validates that the :street and :city pair is unique.
|
239
291
|
def assert_unique(attrs)
|
240
|
-
|
241
|
-
assert(
|
292
|
+
result = db.sinter(*Array(attrs).map { |att| index_key_for(att, send(att)) })
|
293
|
+
assert(result.empty? || result.include?(id.to_s), [attrs, :not_unique])
|
242
294
|
end
|
243
295
|
end
|
244
296
|
|
@@ -305,9 +357,6 @@ module Ohm
|
|
305
357
|
# If you want to find a model instance by some attribute value, then an index for that
|
306
358
|
# attribute must exist.
|
307
359
|
#
|
308
|
-
# Each index declaration creates an index. It can be either an index on one particular attribute,
|
309
|
-
# or an index accross many attributes.
|
310
|
-
#
|
311
360
|
# @example
|
312
361
|
# class User < Ohm::Model
|
313
362
|
# attribute :email
|
@@ -317,12 +366,9 @@ module Ohm
|
|
317
366
|
# # Now this is possible:
|
318
367
|
# User.find :email, "ohm@example.com"
|
319
368
|
#
|
320
|
-
# @
|
321
|
-
|
322
|
-
|
323
|
-
# Creates a composite index for street and city.
|
324
|
-
def self.index(attrs)
|
325
|
-
indices << Array(attrs)
|
369
|
+
# @param name [Symbol] Name of the attribute to be indexed.
|
370
|
+
def self.index(att)
|
371
|
+
indices << att
|
326
372
|
end
|
327
373
|
|
328
374
|
def self.attr_list_reader(name, model = nil)
|
@@ -369,18 +415,42 @@ module Ohm
|
|
369
415
|
model
|
370
416
|
end
|
371
417
|
|
372
|
-
|
373
|
-
|
418
|
+
# Find all the records matching the specified attribute-value pair.
|
419
|
+
#
|
420
|
+
# @example
|
421
|
+
# Event.find(:starts_on, Date.today)
|
422
|
+
def self.find(attrs, value)
|
423
|
+
Attributes::Set.new(db, key(attrs, encode(value)), self)
|
424
|
+
end
|
425
|
+
|
426
|
+
# Search across multiple indices and return the intersection of the sets.
|
427
|
+
#
|
428
|
+
# @example Finds all the user events for the supplied days
|
429
|
+
# event1 = Event.create day: "2009-09-09", author: "Albert"
|
430
|
+
# event2 = Event.create day: "2009-09-09", author: "Benoit"
|
431
|
+
# event3 = Event.create day: "2009-09-10", author: "Albert"
|
432
|
+
# Event.filter(author: "Albert", day: "2009-09-09") do |events|
|
433
|
+
# assert_equal [event1], events
|
434
|
+
# end
|
435
|
+
def self.filter(hash, &block)
|
436
|
+
self.all.filter(hash, &block)
|
374
437
|
end
|
375
438
|
|
376
|
-
|
377
|
-
|
439
|
+
# Search across multiple indices and return the union of the sets.
|
440
|
+
#
|
441
|
+
# @example Finds all the events for the supplied days
|
442
|
+
# event1 = Event.create day: "2009-09-09"
|
443
|
+
# event2 = Event.create day: "2009-09-10"
|
444
|
+
# event3 = Event.create day: "2009-09-11"
|
445
|
+
# Event.search(day: ["2009-09-09", "2009-09-10", "2009-09-011"]) do |events|
|
446
|
+
# assert_equal [event1, event2, event3], events
|
447
|
+
# end
|
448
|
+
def self.search(hash, &block)
|
449
|
+
self.all.search(hash, &block)
|
378
450
|
end
|
379
451
|
|
380
|
-
def self.
|
381
|
-
|
382
|
-
encode(value)
|
383
|
-
end
|
452
|
+
def self.encode(value)
|
453
|
+
Base64.encode64(value.to_s).gsub("\n", "")
|
384
454
|
end
|
385
455
|
|
386
456
|
def initialize(attrs = {})
|
@@ -533,14 +603,30 @@ module Ohm
|
|
533
603
|
end
|
534
604
|
|
535
605
|
def add_to_indices
|
536
|
-
indices.each do |
|
537
|
-
|
606
|
+
indices.each do |att|
|
607
|
+
next add_to_index(att) unless collection?(send(att))
|
608
|
+
send(att).each { |value| add_to_index(att, value) }
|
538
609
|
end
|
539
610
|
end
|
540
611
|
|
612
|
+
def collection?(value)
|
613
|
+
self.class.collection?(value)
|
614
|
+
end
|
615
|
+
|
616
|
+
def self.collection?(value)
|
617
|
+
value.kind_of?(Enumerable) &&
|
618
|
+
value.kind_of?(String) == false
|
619
|
+
end
|
620
|
+
|
621
|
+
def add_to_index(att, value = send(att))
|
622
|
+
index = index_key_for(att, value)
|
623
|
+
db.sadd(index, id)
|
624
|
+
db.sadd(key(:_indices), index)
|
625
|
+
end
|
626
|
+
|
541
627
|
def delete_from_indices
|
542
|
-
|
543
|
-
db.srem(
|
628
|
+
db.smembers(key(:_indices)).each do |index|
|
629
|
+
db.srem(index, id)
|
544
630
|
end
|
545
631
|
end
|
546
632
|
|
@@ -572,8 +658,8 @@ module Ohm
|
|
572
658
|
end
|
573
659
|
end
|
574
660
|
|
575
|
-
def index_key_for(
|
576
|
-
self.class.key
|
661
|
+
def index_key_for(att, value)
|
662
|
+
self.class.key(att, self.class.encode(value))
|
577
663
|
end
|
578
664
|
|
579
665
|
# Lock the object so no other instances can modify it.
|
data/test/connection_test.rb
CHANGED
data/test/indices_test.rb
CHANGED
@@ -1,21 +1,30 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), "test_helper")
|
2
2
|
|
3
3
|
class IndicesTest < Test::Unit::TestCase
|
4
|
+
setup do
|
5
|
+
Ohm.flush
|
6
|
+
end
|
7
|
+
|
4
8
|
class User < Ohm::Model
|
5
9
|
attribute :email
|
10
|
+
attribute :update
|
6
11
|
|
7
12
|
index :email
|
8
13
|
index :email_provider
|
14
|
+
index :working_days
|
15
|
+
index :update
|
9
16
|
|
10
17
|
def email_provider
|
11
18
|
email.split("@").last
|
12
19
|
end
|
20
|
+
|
21
|
+
def working_days
|
22
|
+
@working_days ||= []
|
23
|
+
end
|
13
24
|
end
|
14
25
|
|
15
26
|
context "A model with an indexed attribute" do
|
16
27
|
setup do
|
17
|
-
Ohm.flush
|
18
|
-
|
19
28
|
@user1 = User.create(:email => "foo")
|
20
29
|
@user2 = User.create(:email => "bar")
|
21
30
|
@user3 = User.create(:email => "baz qux")
|
@@ -51,8 +60,6 @@ class IndicesTest < Test::Unit::TestCase
|
|
51
60
|
|
52
61
|
context "Indexing arbitrary attributes" do
|
53
62
|
setup do
|
54
|
-
Ohm.flush
|
55
|
-
|
56
63
|
@user1 = User.create(:email => "foo@gmail.com")
|
57
64
|
@user2 = User.create(:email => "bar@gmail.com")
|
58
65
|
@user3 = User.create(:email => "bazqux@yahoo.com")
|
@@ -63,4 +70,82 @@ class IndicesTest < Test::Unit::TestCase
|
|
63
70
|
assert_equal [@user3], User.find(:email_provider, "yahoo.com")
|
64
71
|
end
|
65
72
|
end
|
73
|
+
|
74
|
+
context "Indexing enumerables" do
|
75
|
+
setup do
|
76
|
+
@user1 = User.create(:email => "foo@gmail.com")
|
77
|
+
@user2 = User.create(:email => "bar@gmail.com")
|
78
|
+
|
79
|
+
@user1.working_days << "Mon"
|
80
|
+
@user1.working_days << "Tue"
|
81
|
+
@user2.working_days << "Mon"
|
82
|
+
@user2.working_days << "Wed"
|
83
|
+
|
84
|
+
@user1.save
|
85
|
+
@user2.save
|
86
|
+
end
|
87
|
+
|
88
|
+
should "index each item" do
|
89
|
+
assert_equal [@user1, @user2], User.find(:working_days, "Mon").to_a.sort_by { |u| u.id }
|
90
|
+
end
|
91
|
+
|
92
|
+
# TODO If we deal with Ohm collections, the updates are atomic but the reindexing never happens.
|
93
|
+
# One solution may be to reindex after inserts or deletes in collection.
|
94
|
+
should "remove the indices when the object changes" do
|
95
|
+
@user2.working_days.delete "Mon"
|
96
|
+
@user2.save
|
97
|
+
assert_equal [@user1], User.find(:working_days, "Mon")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "Intersection and and union" do
|
102
|
+
class Event < Ohm::Model
|
103
|
+
attr_writer :days
|
104
|
+
|
105
|
+
attribute :timeline
|
106
|
+
index :timeline
|
107
|
+
index :days
|
108
|
+
|
109
|
+
def days
|
110
|
+
@days ||= []
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
setup do
|
115
|
+
@event1 = Event.create(timeline: 1)
|
116
|
+
@event2 = Event.create(timeline: 1)
|
117
|
+
@event3 = Event.create(timeline: 2)
|
118
|
+
@event1.days = [1, 2]
|
119
|
+
@event2.days = [2, 3]
|
120
|
+
@event3.days = [3, 4]
|
121
|
+
@event1.save
|
122
|
+
@event2.save
|
123
|
+
@event3.save
|
124
|
+
end
|
125
|
+
|
126
|
+
should "intersect multiple sets of results" do
|
127
|
+
Event.filter(timeline: 1, days: [1, 2]) do |set|
|
128
|
+
assert_equal [@event1], set
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
should "group multiple sets of results" do
|
133
|
+
Event.search(days: [1, 2]) do |set|
|
134
|
+
assert_equal [@event1, @event2], set
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
should "combine intersections and unions" do
|
139
|
+
Event.search(days: [1, 2, 3]) do |events|
|
140
|
+
events.filter(timeline: 1) do |result|
|
141
|
+
assert_equal [@event1, @event2], result
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
should "work with strings that generate a new line when encoded" do
|
147
|
+
user = User.create(email: "foo@bar", update: "CORRECTED - UPDATE 2-Suspected US missile strike kills 5 in Pakistan")
|
148
|
+
assert_equal [user], User.find(:update, "CORRECTED - UPDATE 2-Suspected US missile strike kills 5 in Pakistan")
|
149
|
+
end
|
150
|
+
end
|
66
151
|
end
|
data/test/model_test.rb
CHANGED
@@ -221,6 +221,14 @@ class TestRedis < Test::Unit::TestCase
|
|
221
221
|
Person.create :name => "A"
|
222
222
|
assert_equal "A", Person.all.first(:by => :name, :order => "ALPHA").name
|
223
223
|
end
|
224
|
+
|
225
|
+
should "return attribute values when the get parameter is specified" do
|
226
|
+
Ohm.flush
|
227
|
+
Person.create :name => "B"
|
228
|
+
Person.create :name => "A"
|
229
|
+
|
230
|
+
assert_equal "A", Person.all.sort_by(:name, get: "Person:*:name", order: "ALPHA").first
|
231
|
+
end
|
224
232
|
end
|
225
233
|
|
226
234
|
context "Loading attributes" do
|
data/test/validations_test.rb
CHANGED
@@ -7,7 +7,7 @@ class ValidationsTest < Test::Unit::TestCase
|
|
7
7
|
attribute :capacity
|
8
8
|
|
9
9
|
index :name
|
10
|
-
index
|
10
|
+
index :place
|
11
11
|
|
12
12
|
def validate
|
13
13
|
assert_format(:name, /^\w+$/)
|
@@ -99,12 +99,12 @@ class ValidationsTest < Test::Unit::TestCase
|
|
99
99
|
@event.create
|
100
100
|
|
101
101
|
assert_nil @event.id
|
102
|
-
assert_equal [[
|
102
|
+
assert_equal [[:name, :not_unique]], @event.errors
|
103
103
|
end
|
104
104
|
end
|
105
105
|
|
106
106
|
context "That must have a unique name scoped by place" do
|
107
|
-
should "fail when the value already exists" do
|
107
|
+
should "fail when the value already exists for a scoped attribute" do
|
108
108
|
def @event.validate
|
109
109
|
assert_unique [:name, :place]
|
110
110
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ohm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.19
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michel Martens
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2009-09-
|
13
|
+
date: 2009-09-15 00:00:00 -03:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|