perpetuity 0.4.8 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/README.md +13 -15
- data/lib/perpetuity/dereferencer.rb +10 -3
- data/lib/perpetuity/mapper.rb +46 -6
- data/lib/perpetuity/mongodb/query.rb +4 -0
- data/lib/perpetuity/mongodb/query_attribute.rb +12 -0
- data/lib/perpetuity/mongodb/query_expression.rb +26 -5
- data/lib/perpetuity/mongodb.rb +23 -4
- data/lib/perpetuity/retrieval.rb +1 -1
- data/lib/perpetuity/version.rb +1 -1
- data/lib/perpetuity.rb +1 -1
- data/spec/integration/enumerable_spec.rb +48 -0
- data/spec/integration/mongodb_spec.rb +16 -3
- data/spec/integration/retrieval_spec.rb +42 -3
- data/spec/integration/update_spec.rb +31 -0
- data/spec/perpetuity/dereferencer_spec.rb +31 -10
- data/spec/perpetuity/mapper_spec.rb +28 -8
- data/spec/perpetuity/mongodb/query_attribute_spec.rb +12 -0
- data/spec/perpetuity/mongodb/query_spec.rb +42 -0
- data/spec/support/test_classes/article.rb +5 -0
- data/spec/support/test_classes.rb +3 -3
- metadata +7 -19
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 70280878bf3a3182433a66c2133a364fbbc97e25
|
4
|
+
data.tar.gz: f953bdae10c21cf728ab816f6eb250a5c0553abd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 16bc5960f1be6d9d120c463c7f0310e732db0c30e8a40cef4ad8d2d9d06db0008318561f595653f5a5debfba98e4b40cc17453aa38698ec2c42b51800e5bb50e
|
7
|
+
data.tar.gz: e4ba4d28aefc0445bc5fbd3ddef864f7a29645389d448878c94d91eb17713f8c19a815efac7c3b9eae259514bdc1822d1be10e1d99ad7f574af97b35b34378f8
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
## Version 0.5.0
|
2
|
+
|
3
|
+
- Allow querying based on referenced class/id or embedded-object data
|
4
|
+
- Remove duplicate references when loading associations on referenced objects
|
5
|
+
- Optimize loading associations with zero/one/many objects. It is similar to `Array#detect` vs `Array#select`. `detect` is faster if you only need one.
|
6
|
+
- Fixed a bug in defining methods on generated mapper classes which would mistakenly define them on the class instead of the mapper objects
|
7
|
+
- Add `none?`/`one?`/`all?`/`any?` methods to mappers.
|
8
|
+
- Add block functionality to `Mapper#count`, similar to `Enumerable#count`
|
9
|
+
- Alias `Mapper#find_all` to `Mapper#select`
|
10
|
+
- Alias `Mapper#detect` to `Mapper#find`
|
11
|
+
- Add `Mapper#reject` method to negate queries
|
12
|
+
- Allow `Mapper#find` to take a block like `Mapper#select`
|
13
|
+
- Add atomic incrementation
|
14
|
+
|
1
15
|
## Version 0.4.8
|
2
16
|
|
3
17
|
- Provide configuration one-liner ability for simple configs
|
data/README.md
CHANGED
@@ -4,8 +4,6 @@ Perpetuity is a simple Ruby object persistence layer that attempts to follow Mar
|
|
4
4
|
|
5
5
|
Your objects will hopefully eventually be able to be persisted into whichever database you like. Right now, only MongoDB is supported. Other persistence solutions will come later.
|
6
6
|
|
7
|
-
This gem was inspired by [a blog post by Steve Klabnik](http://blog.steveklabnik.com/posts/2011-12-30-active-record-considered-harmful).
|
8
|
-
|
9
7
|
## How it works
|
10
8
|
|
11
9
|
In the Data Mapper pattern, the objects you work with don't understand how to persist themselves. They interact with other objects just as in any other object-oriented application, leaving all persistence logic to mapper objects. This decouples them from the database and allows you to write your code without it in mind.
|
@@ -20,20 +18,16 @@ gem 'perpetuity'
|
|
20
18
|
|
21
19
|
## Configuration
|
22
20
|
|
23
|
-
The only currently supported persistence method is MongoDB. Other schemaless solutions can probably be implemented easily.
|
21
|
+
The only currently supported persistence method is MongoDB. Other schemaless solutions can probably be implemented easily. The simplest configuration is with the following line:
|
24
22
|
|
25
23
|
```ruby
|
26
|
-
mongodb
|
27
|
-
|
28
|
-
host: 'mongodb.example.com', # Default: 'localhost'
|
29
|
-
port: 27017, # Default: 27017
|
30
|
-
username: 'mongo', # If no username/password given, do not authenticate
|
31
|
-
password: 'password'
|
32
|
-
)
|
24
|
+
Perpetuity.data_source :mongodb, 'my_database'
|
25
|
+
```
|
33
26
|
|
34
|
-
|
35
|
-
|
36
|
-
|
27
|
+
If your database is on another server or you need authentication, you can specify those as options:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
Perpetuity.data_source :mongodb, 'my_database', host: 'mongo.example.com', port: 27017, username: 'mongo', password: 'password'
|
37
31
|
```
|
38
32
|
|
39
33
|
## Setting up object mappers
|
@@ -112,7 +106,7 @@ Notice that we have to use a single `&` and surround each criterion with parenth
|
|
112
106
|
|
113
107
|
## Associations with Other Objects
|
114
108
|
|
115
|
-
The database can natively serialize some objects. For example, MongoDB can serialize `String`, `Numeric`, `Array`, `Hash`, `Time`, `nil`, `true`, `false`, and a few others. For other
|
109
|
+
The database can natively serialize some objects. For example, MongoDB can serialize `String`, `Numeric`, `Array`, `Hash`, `Time`, `nil`, `true`, `false`, and a few others. For other objects, you must determine whether you want those attributes embedded within the same document in the database or attached as a reference. For example, a `Post` could have `Comment`s, which would likely be embedded within the post object. But these comments could have an `author` attribute that references the `Person` that wrote the comment. Embedding the author in this case is not a good idea since it would be a duplicate of the `Person` that wrote it, which would then be out of sync if the original object is modified.
|
116
110
|
|
117
111
|
If an object references another type of object, the association is declared just as any other attribute. No special treatment is required. For embedded relationships, make sure you use the `embedded: true` option in the attribute.
|
118
112
|
|
@@ -158,6 +152,8 @@ article_mapper.load_association! articles, :author # All author objects for
|
|
158
152
|
article_mapper.load_association! articles, :tags # M:N
|
159
153
|
```
|
160
154
|
|
155
|
+
Each of these `load_association!` calls will only execute the number of queries necessary to retrieve all of the objects. For example, if the `author` attribute for the selected articles contains both `User` and `Admin` objects, it will execute two queries (one each for `User` and `Admin`). If the tags for all of the selected articles are all `Tag` objects, only one query will be executed even in the M:N case.
|
156
|
+
|
161
157
|
## Customizing persistence
|
162
158
|
|
163
159
|
Setting the ID of a record to a custom value rather than using the DB default.
|
@@ -212,6 +208,8 @@ end
|
|
212
208
|
Perpetuity[Article].reindex!
|
213
209
|
```
|
214
210
|
|
211
|
+
You could put this in a rake task to be executed when you deploy your app.
|
212
|
+
|
215
213
|
## Contributing
|
216
214
|
|
217
|
-
|
215
|
+
There are plenty of opportunities to improve what's here and possibly some design decisions that need some more refinement. You can help. If you have ideas to build on this, send some love in the form of pull requests, issues or [tweets](http://twitter.com/jamie_gaskins) and I'll do what I can for them.
|
@@ -35,9 +35,16 @@ module Perpetuity
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def objects klass, ids
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
ids = ids.uniq
|
39
|
+
if ids.one?
|
40
|
+
mapper_registry[klass].find(ids.first)
|
41
|
+
elsif ids.none?
|
42
|
+
[]
|
43
|
+
else
|
44
|
+
mapper_registry[klass].select { |object|
|
45
|
+
object.id.in ids.uniq
|
46
|
+
}.to_a
|
47
|
+
end
|
41
48
|
end
|
42
49
|
|
43
50
|
def referenceable? ref
|
data/lib/perpetuity/mapper.rb
CHANGED
@@ -72,8 +72,24 @@ module Perpetuity
|
|
72
72
|
configuration.data_source
|
73
73
|
end
|
74
74
|
|
75
|
-
def count
|
76
|
-
data_source.count mapped_class
|
75
|
+
def count &block
|
76
|
+
data_source.count mapped_class, &block
|
77
|
+
end
|
78
|
+
|
79
|
+
def any? &block
|
80
|
+
count(&block) > 0
|
81
|
+
end
|
82
|
+
|
83
|
+
def all? &block
|
84
|
+
count(&block) == count
|
85
|
+
end
|
86
|
+
|
87
|
+
def one? &block
|
88
|
+
count(&block) == 1
|
89
|
+
end
|
90
|
+
|
91
|
+
def none? &block
|
92
|
+
!any?(&block)
|
77
93
|
end
|
78
94
|
|
79
95
|
def first
|
@@ -85,12 +101,24 @@ module Perpetuity
|
|
85
101
|
end
|
86
102
|
|
87
103
|
def select &block
|
88
|
-
|
89
|
-
retrieve query
|
104
|
+
retrieve data_source.query(&block).to_db
|
90
105
|
end
|
91
106
|
|
92
|
-
|
93
|
-
|
107
|
+
alias :find_all :select
|
108
|
+
|
109
|
+
def find *args, &block
|
110
|
+
if block_given?
|
111
|
+
select(&block).first
|
112
|
+
else
|
113
|
+
id = args.first
|
114
|
+
select { |object| object.id == id }.first
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
alias :detect :find
|
119
|
+
|
120
|
+
def reject &block
|
121
|
+
retrieve data_source.negate_query(&block).to_db
|
94
122
|
end
|
95
123
|
|
96
124
|
def delete object
|
@@ -134,6 +162,18 @@ module Perpetuity
|
|
134
162
|
update object, serialize(object), false
|
135
163
|
end
|
136
164
|
|
165
|
+
def increment object, attribute, count=1
|
166
|
+
data_source.increment mapped_class, object.id, attribute, count
|
167
|
+
rescue Moped::Errors::OperationFailure
|
168
|
+
raise ArgumentError.new('Attempted to increment a non-numeric value')
|
169
|
+
end
|
170
|
+
|
171
|
+
def decrement object, attribute, count=1
|
172
|
+
data_source.increment mapped_class, object.id, attribute, -count
|
173
|
+
rescue Moped::Errors::OperationFailure
|
174
|
+
raise ArgumentError.new('Attempted to decrement a non-numeric value')
|
175
|
+
end
|
176
|
+
|
137
177
|
def self.validate &block
|
138
178
|
validations.instance_exec(&block)
|
139
179
|
end
|
@@ -45,6 +45,18 @@ module Perpetuity
|
|
45
45
|
def to_sym
|
46
46
|
name
|
47
47
|
end
|
48
|
+
|
49
|
+
def method_missing name
|
50
|
+
if name.to_s == 'id'
|
51
|
+
name = :"#{self.name}.__metadata__.#{name}"
|
52
|
+
elsif name.to_s == 'klass'
|
53
|
+
name = :"#{self.name}.__metadata__.class"
|
54
|
+
else
|
55
|
+
name = :"#{self.name}.#{name}"
|
56
|
+
end
|
57
|
+
|
58
|
+
self.class.new(name)
|
59
|
+
end
|
48
60
|
end
|
49
61
|
end
|
50
62
|
end
|
@@ -4,26 +4,37 @@ require 'perpetuity/mongodb/query_intersection'
|
|
4
4
|
module Perpetuity
|
5
5
|
class MongoDB
|
6
6
|
class QueryExpression
|
7
|
-
attr_accessor :comparator
|
7
|
+
attr_accessor :comparator, :negated
|
8
8
|
|
9
9
|
def initialize attribute, comparator, value
|
10
10
|
@attribute = attribute
|
11
11
|
@comparator = comparator
|
12
12
|
@value = value
|
13
|
+
@negated = false
|
13
14
|
|
14
15
|
@attribute = @attribute.to_sym if @attribute.respond_to? :to_sym
|
15
16
|
end
|
16
17
|
|
17
18
|
def to_db
|
18
|
-
|
19
|
+
public_send @comparator
|
19
20
|
end
|
20
21
|
|
21
22
|
def equals
|
22
|
-
|
23
|
+
if @negated
|
24
|
+
{ @attribute => { '$ne' => @value } }
|
25
|
+
else
|
26
|
+
{ @attribute => @value }
|
27
|
+
end
|
23
28
|
end
|
24
29
|
|
25
30
|
def function func
|
26
|
-
|
31
|
+
f = { func => @value }
|
32
|
+
|
33
|
+
if @negated
|
34
|
+
{ @attribute => { '$not' => f } }
|
35
|
+
else
|
36
|
+
{ @attribute => f }
|
37
|
+
end
|
27
38
|
end
|
28
39
|
|
29
40
|
def less_than
|
@@ -51,7 +62,11 @@ module Perpetuity
|
|
51
62
|
end
|
52
63
|
|
53
64
|
def matches
|
54
|
-
|
65
|
+
if @negated
|
66
|
+
{ @attribute => { '$not' => @value } }
|
67
|
+
else
|
68
|
+
{ @attribute => @value }
|
69
|
+
end
|
55
70
|
end
|
56
71
|
|
57
72
|
def | other
|
@@ -61,6 +76,12 @@ module Perpetuity
|
|
61
76
|
def & other
|
62
77
|
QueryIntersection.new(self, other)
|
63
78
|
end
|
79
|
+
|
80
|
+
def negate
|
81
|
+
expr = dup
|
82
|
+
expr.negated = true
|
83
|
+
expr
|
84
|
+
end
|
64
85
|
end
|
65
86
|
end
|
66
87
|
end
|
data/lib/perpetuity/mongodb.rb
CHANGED
@@ -59,9 +59,9 @@ module Perpetuity
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
-
def count klass, criteria={},
|
63
|
-
|
64
|
-
collection(klass).find(
|
62
|
+
def count klass, criteria={}, &block
|
63
|
+
q = block_given? ? query(&block).to_db : criteria
|
64
|
+
collection(klass).find(q).count
|
65
65
|
end
|
66
66
|
|
67
67
|
def delete_all klass
|
@@ -92,8 +92,23 @@ module Perpetuity
|
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
95
|
+
def increment klass, id, attribute, count=1
|
96
|
+
find(klass, id).update '$inc' => { attribute => count }
|
97
|
+
end
|
98
|
+
|
99
|
+
def find klass, id
|
100
|
+
collection(klass).find(to_bson_id(_id: id))
|
101
|
+
end
|
102
|
+
|
95
103
|
def to_bson_id criteria
|
96
104
|
criteria = criteria.dup
|
105
|
+
|
106
|
+
# Check for both string and symbol ID in criteria
|
107
|
+
if criteria.has_key?('id')
|
108
|
+
criteria['_id'] = Moped::BSON::ObjectId(criteria['id']) rescue criteria['id']
|
109
|
+
criteria.delete 'id'
|
110
|
+
end
|
111
|
+
|
97
112
|
if criteria.has_key?(:id)
|
98
113
|
criteria[:_id] = Moped::BSON::ObjectId(criteria[:id]) rescue criteria[:id]
|
99
114
|
criteria.delete :id
|
@@ -122,7 +137,7 @@ module Perpetuity
|
|
122
137
|
end
|
123
138
|
|
124
139
|
def update klass, id, new_data
|
125
|
-
|
140
|
+
find(klass, id).update(new_data)
|
126
141
|
end
|
127
142
|
|
128
143
|
def can_serialize? value
|
@@ -137,6 +152,10 @@ module Perpetuity
|
|
137
152
|
Query.new(&block)
|
138
153
|
end
|
139
154
|
|
155
|
+
def negate_query &block
|
156
|
+
Query.new(&block).negate
|
157
|
+
end
|
158
|
+
|
140
159
|
def index klass, attribute, options={}
|
141
160
|
@indexes[klass] ||= Set.new
|
142
161
|
|
data/lib/perpetuity/retrieval.rb
CHANGED
data/lib/perpetuity/version.rb
CHANGED
data/lib/perpetuity.rb
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/test_classes'
|
3
|
+
|
4
|
+
describe 'enumerable syntax' do
|
5
|
+
let(:mapper) { Perpetuity[Article] }
|
6
|
+
|
7
|
+
let(:current_time) { Time.now }
|
8
|
+
let(:foo) { Article.new("Foo #{Time.now.to_f}", '', nil, current_time - 60) }
|
9
|
+
let(:bar) { Article.new("Bar #{Time.now.to_f}", '', nil, current_time + 60) }
|
10
|
+
|
11
|
+
before do
|
12
|
+
mapper.insert foo
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'finds a single object based on criteria' do
|
16
|
+
mapper.find { |a| a.title == foo.title }.should be == foo
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'excludes objects based on criteria' do
|
20
|
+
before do
|
21
|
+
mapper.insert bar
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'excludes on equality' do
|
25
|
+
articles = mapper.reject { |a| a.title == bar.title }.to_a
|
26
|
+
articles.should include foo
|
27
|
+
articles.should_not include bar
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'excludes on inequality' do
|
31
|
+
articles = mapper.reject { |a| a.published_at <= current_time }.to_a
|
32
|
+
articles.should include bar
|
33
|
+
articles.should_not include foo
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'excludes on not-equal' do
|
37
|
+
articles = mapper.reject { |a| a.title != foo.title }.to_a
|
38
|
+
articles.should include foo
|
39
|
+
articles.should_not include bar
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'excludes on regex match' do
|
43
|
+
articles = mapper.reject { |a| a.title =~ /Foo/ }.to_a
|
44
|
+
articles.should include bar
|
45
|
+
articles.should_not include foo
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -62,11 +62,11 @@ module Perpetuity
|
|
62
62
|
mongo.count(klass).should == 3
|
63
63
|
end
|
64
64
|
|
65
|
-
it 'counts documents matching
|
65
|
+
it 'counts the documents matching a query' do
|
66
66
|
mongo.delete_all klass
|
67
|
+
1.times { mongo.insert klass, { name: 'bar' } }
|
67
68
|
3.times { mongo.insert klass, { name: 'foo' } }
|
68
|
-
|
69
|
-
mongo.count(klass, name: 'foo').should == 3
|
69
|
+
mongo.count(klass) { |o| o.name == 'foo' }.should == 3
|
70
70
|
end
|
71
71
|
|
72
72
|
it 'gets the first document in a collection' do
|
@@ -139,6 +139,19 @@ module Perpetuity
|
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
142
|
+
describe 'atomic operations' do
|
143
|
+
after(:all) { mongo.delete_all klass }
|
144
|
+
|
145
|
+
it 'increments the value of an attribute' do
|
146
|
+
id = mongo.insert klass, count: 1
|
147
|
+
mongo.increment klass, id, :count
|
148
|
+
mongo.increment klass, id, :count, 10
|
149
|
+
mongo.retrieve(klass, id: id).first['count'].should == 12
|
150
|
+
mongo.increment klass, id, :count, -1
|
151
|
+
mongo.retrieve(klass, id: id).first['count'].should == 11
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
142
155
|
describe 'operation errors' do
|
143
156
|
let(:data) { { foo: 'bar' } }
|
144
157
|
let(:index) { mongo.index Object, :foo, unique: true }
|
@@ -7,7 +7,7 @@ describe "retrieval" do
|
|
7
7
|
|
8
8
|
it "gets all the objects of a class" do
|
9
9
|
expect { mapper.insert Article.new }.
|
10
|
-
to change { mapper.all.count }.by 1
|
10
|
+
to change { mapper.all.to_a.count }.by 1
|
11
11
|
end
|
12
12
|
|
13
13
|
it "has an ID when retrieved" do
|
@@ -51,6 +51,13 @@ describe "retrieval" do
|
|
51
51
|
mapper.all.limit(4).to_a.should have(4).items
|
52
52
|
end
|
53
53
|
|
54
|
+
it 'counts result set' do
|
55
|
+
title = "Foo #{Time.now.to_f}"
|
56
|
+
mapper = Perpetuity[Article]
|
57
|
+
5.times { mapper.insert Article.new(title) }
|
58
|
+
mapper.count { |article| article.title == title }.should == 5
|
59
|
+
end
|
60
|
+
|
54
61
|
describe "Array-like syntax" do
|
55
62
|
let(:draft) { Article.new 'Draft', 'draft content', nil, Time.now + 30 }
|
56
63
|
let(:published) { Article.new 'Published', 'content', nil, Time.now - 30, 3 }
|
@@ -118,7 +125,7 @@ describe "retrieval" do
|
|
118
125
|
describe 'counting results' do
|
119
126
|
let(:title) { SecureRandom.hex }
|
120
127
|
let(:articles) do
|
121
|
-
|
128
|
+
2.times.map { Article.new(title) } + 2.times.map { Article.new }
|
122
129
|
end
|
123
130
|
|
124
131
|
before do
|
@@ -127,7 +134,32 @@ describe "retrieval" do
|
|
127
134
|
|
128
135
|
it 'counts the results' do
|
129
136
|
query = mapper.select { |article| article.title == title }
|
130
|
-
query.count.should ==
|
137
|
+
query.count.should == 2
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'checks whether any results match' do
|
141
|
+
mapper.any? { |article| article.title == title }.should be_true
|
142
|
+
mapper.any? { |article| article.title == SecureRandom.hex }.should be_false
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'checks whether all results match' do
|
146
|
+
mapper.delete_all
|
147
|
+
2.times { |i| mapper.insert Article.new(title, nil, nil, nil, i) }
|
148
|
+
mapper.all? { |article| article.title == title }.should be_true
|
149
|
+
mapper.all? { |article| article.views == 0 }.should be_false
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'checks whether only one result matches' do
|
153
|
+
unique_title = SecureRandom.hex
|
154
|
+
mapper.insert Article.new(unique_title)
|
155
|
+
mapper.one? { |article| article.title == unique_title }.should be_true
|
156
|
+
mapper.one? { |article| article.title == title }.should be_false
|
157
|
+
mapper.one? { |article| article.title == 'Title' }.should be_false
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'checks whether no results match' do
|
161
|
+
mapper.none? { |article| article.title == SecureRandom.hex }.should be_true
|
162
|
+
mapper.none? { |article| article.title == title }.should be_false
|
131
163
|
end
|
132
164
|
end
|
133
165
|
|
@@ -144,4 +176,11 @@ describe "retrieval" do
|
|
144
176
|
retrieved_article.author.should be_a CRM::Person
|
145
177
|
end
|
146
178
|
end
|
179
|
+
|
180
|
+
it 'selects objects with nested data' do
|
181
|
+
user = User.new(first_name: 'foo', last_name: 'bar')
|
182
|
+
mapper = Perpetuity[User]
|
183
|
+
mapper.insert user
|
184
|
+
mapper.select { |user| user.name.first_name == 'foo' }.map(&:id).should include user.id
|
185
|
+
end
|
147
186
|
end
|
@@ -56,6 +56,37 @@ describe 'updating' do
|
|
56
56
|
retrieved_authors.map(&:klass).should == [User, User]
|
57
57
|
retrieved_authors.map(&:id).should == [dave.id, andy.id]
|
58
58
|
end
|
59
|
+
|
60
|
+
describe 'atomic increments/decrements' do
|
61
|
+
let(:view_count) { 0 }
|
62
|
+
let(:article) { Article.new('title', 'body', nil, nil, view_count) }
|
63
|
+
|
64
|
+
it 'increments attributes of objects in the database' do
|
65
|
+
mapper.increment article, :views
|
66
|
+
mapper.increment article, :views, 10
|
67
|
+
mapper.find(article.id).views.should == view_count + 11
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'decrements attributes of objects in the database' do
|
71
|
+
mapper.decrement article, :views
|
72
|
+
mapper.decrement article, :views, 10
|
73
|
+
mapper.find(article.id).views.should == view_count - 11
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'with an object with the specified attribute missing' do
|
77
|
+
let(:article) { Article.new('title', 'body', nil, nil, nil) }
|
78
|
+
|
79
|
+
it 'raises an exception when incrementing' do
|
80
|
+
expect { mapper.increment article, :views }.to raise_error(
|
81
|
+
ArgumentError, 'Attempted to increment a non-numeric value')
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'raises an exception when decrementing' do
|
85
|
+
expect { mapper.decrement article, :views }.to raise_error(
|
86
|
+
ArgumentError, 'Attempted to decrement a non-numeric value')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
59
90
|
end
|
60
91
|
|
61
92
|
|
@@ -5,19 +5,40 @@ module Perpetuity
|
|
5
5
|
describe Dereferencer do
|
6
6
|
let(:registry) { double('Mapper Registry') }
|
7
7
|
let(:mapper) { double('ObjectMapper') }
|
8
|
-
let(:
|
9
|
-
let(:
|
10
|
-
let(:
|
8
|
+
let(:first) { double('Object', id: 1, class: Object) }
|
9
|
+
let(:second) { double('Object', id: 2, class: Object) }
|
10
|
+
let(:first_ref) { Reference.new(Object, 1) }
|
11
|
+
let(:second_ref) { Reference.new(Object, 2) }
|
12
|
+
let(:objects) { [first, second] }
|
13
|
+
let(:derefer) { Dereferencer.new(registry) }
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
context 'with one reference' do
|
16
|
+
before do
|
17
|
+
registry.should_receive(:[]).with(Object) { mapper }
|
18
|
+
mapper.should_receive(:find).with(1) { first }
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'loads objects based on the specified objects and attribute' do
|
22
|
+
derefer.load first_ref
|
23
|
+
derefer[first_ref].should == first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with no references' do
|
28
|
+
it 'returns an empty array' do
|
29
|
+
derefer.load(nil).should == []
|
30
|
+
end
|
15
31
|
end
|
16
32
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
33
|
+
context 'with multiple references' do
|
34
|
+
before do
|
35
|
+
registry.should_receive(:[]).with(Object) { mapper }
|
36
|
+
mapper.should_receive(:select) { objects }
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'returns the array of dereferenced objects' do
|
40
|
+
derefer.load([first_ref, second_ref]).should == objects
|
41
|
+
end
|
21
42
|
end
|
22
43
|
end
|
23
44
|
end
|
@@ -59,14 +59,34 @@ module Perpetuity
|
|
59
59
|
mapper.count.should be == 4
|
60
60
|
end
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
62
|
+
describe 'finding a single object' do
|
63
|
+
let(:options) { {:attribute=>nil, :direction=>nil, :limit=>nil, :page=>nil} }
|
64
|
+
let(:returned_object) { double('Retrieved Object') }
|
65
|
+
|
66
|
+
it 'finds an object by ID' do
|
67
|
+
criteria = { id: 1 }
|
68
|
+
data_source.should_receive(:retrieve)
|
69
|
+
.with(Object, criteria, options) { [returned_object] }
|
70
|
+
|
71
|
+
mapper.find(1).should be == returned_object
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'finds multiple objects with a block' do
|
75
|
+
criteria = { name: 'foo' }
|
76
|
+
data_source.should_receive(:retrieve)
|
77
|
+
.with(Object, criteria, options) { [returned_object] }.twice
|
78
|
+
|
79
|
+
mapper.select { |e| e.name == 'foo' }.to_a.should == [returned_object]
|
80
|
+
mapper.find_all { |e| e.name == 'foo' }.to_a.should == [returned_object]
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'finds an object with a block' do
|
84
|
+
criteria = { name: 'foo' }
|
85
|
+
data_source.should_receive(:retrieve)
|
86
|
+
.with(Object, criteria, options) { [returned_object] }.twice
|
87
|
+
mapper.find { |o| o.name == 'foo' }.should == returned_object
|
88
|
+
mapper.detect { |o| o.name == 'foo' }.should == returned_object
|
89
|
+
end
|
70
90
|
end
|
71
91
|
|
72
92
|
it 'saves an object' do
|
@@ -7,6 +7,18 @@ module Perpetuity
|
|
7
7
|
|
8
8
|
its(:name) { should == :attribute_name }
|
9
9
|
|
10
|
+
it 'allows checking subattributes' do
|
11
|
+
attribute.title.name.should == :'attribute_name.title'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'wraps .id subattribute in metadata' do
|
15
|
+
attribute.id.name.should == :'attribute_name.__metadata__.id'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'wraps .klass subattribute in metadata' do
|
19
|
+
attribute.klass.name.should == :'attribute_name.__metadata__.class'
|
20
|
+
end
|
21
|
+
|
10
22
|
it 'checks for equality' do
|
11
23
|
(attribute == 1).should be_a MongoDB::QueryExpression
|
12
24
|
end
|
@@ -33,5 +33,47 @@ module Perpetuity
|
|
33
33
|
it 'generates Mongo regexp expressions' do
|
34
34
|
query.new{ |user| user.name =~ /Jamie/ }.to_db.should == {name: /Jamie/}
|
35
35
|
end
|
36
|
+
|
37
|
+
describe 'negated queries' do
|
38
|
+
it 'negates an equality query' do
|
39
|
+
q = query.new { |user| user.name == 'Jamie' }
|
40
|
+
q.negate.to_db.should == { name: { '$ne' => 'Jamie' } }
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'negates a not-equal query' do
|
44
|
+
q = query.new { |account| account.balance != 10 }
|
45
|
+
q.negate.to_db.should == { balance: { '$not' => { '$ne' => 10 } } }
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'negates a less-than query' do
|
49
|
+
q = query.new { |account| account.balance < 10 }
|
50
|
+
q.negate.to_db.should == { balance: { '$not' => { '$lt' => 10 } } }
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'negates a less-than-or-equal query' do
|
54
|
+
q = query.new { |account| account.balance <= 10 }
|
55
|
+
q.negate.to_db.should == { balance: { '$not' => { '$lte' => 10 } } }
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'negates a greater-than query' do
|
59
|
+
q = query.new { |account| account.balance > 10 }
|
60
|
+
q.negate.to_db.should == { balance: { '$not' => { '$gt' => 10 } } }
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'negates a greater-than-or-equal query' do
|
64
|
+
q = query.new { |account| account.balance >= 10 }
|
65
|
+
q.negate.to_db.should == { balance: { '$not' => { '$gte' => 10 } } }
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'negates a regex query' do
|
69
|
+
q = query.new { |account| account.name =~ /Jamie/ }
|
70
|
+
q.negate.to_db.should == { name: { '$not' => /Jamie/ } }
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'negates a inclusion query' do
|
74
|
+
q = query.new { |article| article.tags.in ['tag1', 'tag2'] }
|
75
|
+
q.negate.to_db.should == { tags: { '$not' => { '$in' => ['tag1', 'tag2'] } } }
|
76
|
+
end
|
77
|
+
end
|
36
78
|
end
|
37
79
|
end
|
@@ -2,12 +2,12 @@
|
|
2
2
|
require "support/test_classes/#{file}"
|
3
3
|
end
|
4
4
|
|
5
|
-
|
5
|
+
class UserMapper < Perpetuity::Mapper
|
6
|
+
map User
|
6
7
|
attribute :name
|
7
8
|
end
|
8
9
|
|
9
|
-
|
10
|
-
map Article
|
10
|
+
Perpetuity.generate_mapper_for Article do
|
11
11
|
attribute :title
|
12
12
|
attribute :body
|
13
13
|
attribute :author
|
metadata
CHANGED
@@ -1,20 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perpetuity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.5.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Jamie Gaskins
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2013-05-
|
11
|
+
date: 2013-05-24 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: rake
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
17
|
- - '>='
|
20
18
|
- !ruby/object:Gem::Version
|
@@ -22,7 +20,6 @@ dependencies:
|
|
22
20
|
type: :development
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
24
|
- - '>='
|
28
25
|
- !ruby/object:Gem::Version
|
@@ -30,7 +27,6 @@ dependencies:
|
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: rspec
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
31
|
- - ~>
|
36
32
|
- !ruby/object:Gem::Version
|
@@ -38,7 +34,6 @@ dependencies:
|
|
38
34
|
type: :development
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
38
|
- - ~>
|
44
39
|
- !ruby/object:Gem::Version
|
@@ -46,7 +41,6 @@ dependencies:
|
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: moped
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
45
|
- - '>='
|
52
46
|
- !ruby/object:Gem::Version
|
@@ -54,7 +48,6 @@ dependencies:
|
|
54
48
|
type: :runtime
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
52
|
- - '>='
|
60
53
|
- !ruby/object:Gem::Version
|
@@ -103,6 +96,7 @@ files:
|
|
103
96
|
- perpetuity.gemspec
|
104
97
|
- spec/integration/associations_spec.rb
|
105
98
|
- spec/integration/deletion_spec.rb
|
99
|
+
- spec/integration/enumerable_spec.rb
|
106
100
|
- spec/integration/indexing_spec.rb
|
107
101
|
- spec/integration/mongodb_spec.rb
|
108
102
|
- spec/integration/pagination_spec.rb
|
@@ -146,37 +140,31 @@ files:
|
|
146
140
|
- spec/support/test_classes/user.rb
|
147
141
|
homepage: https://github.com/jgaskins/perpetuity.git
|
148
142
|
licenses: []
|
143
|
+
metadata: {}
|
149
144
|
post_install_message:
|
150
145
|
rdoc_options: []
|
151
146
|
require_paths:
|
152
147
|
- lib
|
153
148
|
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
-
none: false
|
155
149
|
requirements:
|
156
150
|
- - '>='
|
157
151
|
- !ruby/object:Gem::Version
|
158
152
|
version: '0'
|
159
|
-
segments:
|
160
|
-
- 0
|
161
|
-
hash: -66928365718597043
|
162
153
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
|
-
none: false
|
164
154
|
requirements:
|
165
155
|
- - '>='
|
166
156
|
- !ruby/object:Gem::Version
|
167
157
|
version: '0'
|
168
|
-
segments:
|
169
|
-
- 0
|
170
|
-
hash: -66928365718597043
|
171
158
|
requirements: []
|
172
159
|
rubyforge_project:
|
173
|
-
rubygems_version:
|
160
|
+
rubygems_version: 2.0.3
|
174
161
|
signing_key:
|
175
|
-
specification_version:
|
162
|
+
specification_version: 4
|
176
163
|
summary: Persistence library allowing serialization of Ruby objects
|
177
164
|
test_files:
|
178
165
|
- spec/integration/associations_spec.rb
|
179
166
|
- spec/integration/deletion_spec.rb
|
167
|
+
- spec/integration/enumerable_spec.rb
|
180
168
|
- spec/integration/indexing_spec.rb
|
181
169
|
- spec/integration/mongodb_spec.rb
|
182
170
|
- spec/integration/pagination_spec.rb
|