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 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 = Perpetuity::MongoDB.new(
27
- db: 'example_db', # Required
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
- Perpetuity.configure do
35
- data_source mongodb
36
- end
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 data types, 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.
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
- Right now, this code is pretty bare and there are 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 or issues or [tweets](http://twitter.com/jamie_gaskins) or e-mails and I'll do what I can for them.
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
- mapper_registry[klass].select { |object|
39
- object.id.in ids.uniq
40
- }.to_a
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
@@ -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
- query = data_source.query(&block).to_db
89
- retrieve query
104
+ retrieve data_source.query(&block).to_db
90
105
  end
91
106
 
92
- def find id
93
- select { |object| object.id == id }.first
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
@@ -11,6 +11,10 @@ module Perpetuity
11
11
  @query.to_db
12
12
  end
13
13
 
14
+ def negate
15
+ @query.negate
16
+ end
17
+
14
18
  def method_missing missing_method
15
19
  QueryAttribute.new missing_method
16
20
  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
- send @comparator
19
+ public_send @comparator
19
20
  end
20
21
 
21
22
  def equals
22
- { @attribute => @value }
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
- { @attribute => { func => @value } }
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
- { @attribute => @value }
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
@@ -59,9 +59,9 @@ module Perpetuity
59
59
  end
60
60
  end
61
61
 
62
- def count klass, criteria={}, options={}
63
- criteria = to_bson_id(criteria)
64
- collection(klass).find(criteria).count
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
- collection(klass).find({ _id: id }).update(new_data)
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
 
@@ -49,7 +49,7 @@ module Perpetuity
49
49
  end
50
50
 
51
51
  def count
52
- @data_source.count(@class, @criteria, options)
52
+ @data_source.count(@class, @criteria)
53
53
  end
54
54
 
55
55
  def options
@@ -1,3 +1,3 @@
1
1
  module Perpetuity
2
- VERSION = "0.4.8"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/perpetuity.rb CHANGED
@@ -17,7 +17,7 @@ module Perpetuity
17
17
  def self.generate_mapper_for klass, &block
18
18
  mapper = Class.new(Mapper)
19
19
  mapper.map klass, mapper_registry
20
- mapper.instance_exec(&block) if block_given?
20
+ mapper.class_eval(&block) if block_given?
21
21
  end
22
22
 
23
23
  def self.[] klass
@@ -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 criteria' do
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
- 3.times { mongo.insert klass, { name: 'bar' } }
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
- 5.times.map { Article.new(title) } + 5.times.map { Article.new }
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 == 5
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(:object) { double('Object', id: 1, class: Object) }
9
- let(:reference) { Reference.new(Object, 1) }
10
- let(:objects) { [object] }
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
- before do
13
- registry.should_receive(:[]).with(Object) { mapper }
14
- mapper.should_receive(:select) { objects }
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
- it 'loads objects based on the specified objects and attribute' do
18
- derefer = Dereferencer.new(registry)
19
- derefer.load reference
20
- derefer[reference].should == object
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
- it 'finds an object by ID' do
63
- returned_object = double('Retrieved Object')
64
- criteria = { id: 1 }
65
- options = {:attribute=>nil, :direction=>nil, :limit=>nil, :page=>nil}
66
- data_source.should_receive(:retrieve)
67
- .with(Object, criteria, options) { [returned_object] }
68
-
69
- mapper.find(1).should be == returned_object
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
@@ -8,4 +8,9 @@ class Article
8
8
  @published_at = published_at
9
9
  @views = views
10
10
  end
11
+
12
+ def == other
13
+ title == other.title &&
14
+ body == other.body
15
+ end
11
16
  end
@@ -2,12 +2,12 @@
2
2
  require "support/test_classes/#{file}"
3
3
  end
4
4
 
5
- Perpetuity.generate_mapper_for User do
5
+ class UserMapper < Perpetuity::Mapper
6
+ map User
6
7
  attribute :name
7
8
  end
8
9
 
9
- class ArticleMapper < Perpetuity::Mapper
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.4.8
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-07 00:00:00.000000000 Z
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: 1.8.25
160
+ rubygems_version: 2.0.3
174
161
  signing_key:
175
- specification_version: 3
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