perpetuity 0.7.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/perpetuity/attribute.rb +1 -1
- data/lib/perpetuity/mapper.rb +29 -11
- data/lib/perpetuity/mapper_registry.rb +3 -2
- data/lib/perpetuity/mongodb/nil_query.rb +11 -0
- data/lib/perpetuity/mongodb/query.rb +11 -1
- data/lib/perpetuity/mongodb/query_attribute.rb +4 -0
- data/lib/perpetuity/mongodb/query_expression.rb +8 -1
- data/lib/perpetuity/mongodb/serializer.rb +31 -19
- data/lib/perpetuity/mongodb.rb +22 -9
- data/lib/perpetuity/retrieval.rb +16 -5
- data/lib/perpetuity/version.rb +1 -1
- data/spec/integration/indexing_spec.rb +16 -0
- data/spec/integration/mongodb_spec.rb +14 -4
- data/spec/integration/persistence_spec.rb +7 -0
- data/spec/integration/retrieval_spec.rb +94 -66
- data/spec/integration/update_spec.rb +2 -2
- data/spec/perpetuity/dereferencer_spec.rb +2 -0
- data/spec/perpetuity/mapper_spec.rb +13 -12
- data/spec/perpetuity/mongodb/query_attribute_spec.rb +4 -0
- data/spec/perpetuity/mongodb/serializer_spec.rb +68 -20
- data/spec/perpetuity/rails_model_spec.rb +8 -8
- data/spec/perpetuity/retrieval_spec.rb +9 -2
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99bbdaa723ccda7b072e0b224d45508ee85173f2
|
4
|
+
data.tar.gz: 1b1fef3e2811ee1516d36c97b82de71d524279dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3768caff24b90ecf53787b530ac59f95fa920275a43a15fc906c2770e3d78e3a3ad07360b4ae4fb55b6976060ad5a8e8ad1f4d35bf842447bfff4c114f6e7cf8
|
7
|
+
data.tar.gz: 441477056696558e7be0fc26e789f0f0d996e48b5df471529a315d610b1891ab0cf61ef174f9c60ad5e7bac86d5664da5871fa849b7b610d23a9895b1143fd7e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## Version 0.7.1
|
2
|
+
|
3
|
+
- Only unmarshal attributes that we marshaled to begin with. This disallows the use of false marshaled objects. — with [Kevin Sjöberg](https://github.com/KevinSjoberg)
|
4
|
+
- Allow insertion of multiple objects in `Mapper#insert`
|
5
|
+
- Alias `Retrieval#limit` as `Retrieval#take` for `Enumerable` compatibility
|
6
|
+
- Leave result cache when branching to new retrievals if previous retrieval had triggered a query
|
7
|
+
- Silence warnings (some still exist in Moped, unfortunately)
|
8
|
+
- Add finding based on attribute truthiness. For example: `mapper.find { |obj| obj.name }` finds objects whose `name` is neither `nil` nor `false`.
|
9
|
+
- When you remove an index call from the mapper DSL, `Mapper#reindex!` now removes that index from the DB
|
10
|
+
- Previously activated indexes in the DB are converted to `Perpetuity::Attribute`s rather than stored in the format specific to the DB driver
|
11
|
+
|
1
12
|
## Version 0.7.0
|
2
13
|
|
3
14
|
- Add `Perpetuity::RailsModel`, an ActiveModel-compliant mixin
|
data/lib/perpetuity/attribute.rb
CHANGED
data/lib/perpetuity/mapper.rb
CHANGED
@@ -47,6 +47,12 @@ module Perpetuity
|
|
47
47
|
|
48
48
|
def reindex!
|
49
49
|
indexes.each { |index| data_source.activate_index! index }
|
50
|
+
(data_source.active_indexes(mapped_class) - indexes).reject do |index|
|
51
|
+
# TODO: Make this not MongoDB-specific
|
52
|
+
index.attribute.name.to_s == '_id'
|
53
|
+
end.each do |index|
|
54
|
+
data_source.remove_index index
|
55
|
+
end
|
50
56
|
end
|
51
57
|
|
52
58
|
def attributes
|
@@ -59,14 +65,26 @@ module Perpetuity
|
|
59
65
|
|
60
66
|
def insert object
|
61
67
|
raise "#{object} is invalid and cannot be persisted." unless self.class.validations.valid?(object)
|
62
|
-
|
63
|
-
|
64
|
-
|
68
|
+
objects = Array(object)
|
69
|
+
serialized_objects = objects.map do |obj|
|
70
|
+
attributes = serialize(obj)
|
71
|
+
if o_id = obj.instance_exec(&self.class.id)
|
72
|
+
attributes[:id] = o_id
|
73
|
+
end
|
74
|
+
|
75
|
+
attributes
|
65
76
|
end
|
66
77
|
|
67
|
-
|
68
|
-
|
69
|
-
|
78
|
+
new_ids = data_source.insert(mapped_class, serialized_objects)
|
79
|
+
objects.each_with_index do |obj, index|
|
80
|
+
give_id_to obj, new_ids[index]
|
81
|
+
end
|
82
|
+
|
83
|
+
if object.is_a? Array
|
84
|
+
new_ids
|
85
|
+
else
|
86
|
+
new_ids.first
|
87
|
+
end
|
70
88
|
end
|
71
89
|
|
72
90
|
def self.data_source(configuration=Perpetuity.configuration)
|
@@ -102,7 +120,7 @@ module Perpetuity
|
|
102
120
|
end
|
103
121
|
|
104
122
|
def select &block
|
105
|
-
retrieve data_source.query(&block)
|
123
|
+
retrieve data_source.query(&block)
|
106
124
|
end
|
107
125
|
|
108
126
|
alias :find_all :select
|
@@ -127,7 +145,7 @@ module Perpetuity
|
|
127
145
|
alias :detect :find
|
128
146
|
|
129
147
|
def reject &block
|
130
|
-
retrieve data_source.negate_query(&block)
|
148
|
+
retrieve data_source.negate_query(&block)
|
131
149
|
end
|
132
150
|
|
133
151
|
def delete object
|
@@ -192,7 +210,7 @@ module Perpetuity
|
|
192
210
|
end
|
193
211
|
|
194
212
|
def id_for object
|
195
|
-
object.instance_variable_get(:@id)
|
213
|
+
object.instance_variable_get(:@id) if persisted?(object)
|
196
214
|
end
|
197
215
|
|
198
216
|
def self.validate &block
|
@@ -221,8 +239,8 @@ module Perpetuity
|
|
221
239
|
|
222
240
|
private
|
223
241
|
|
224
|
-
def retrieve
|
225
|
-
Perpetuity::Retrieval.new self,
|
242
|
+
def retrieve query=data_source.query
|
243
|
+
Perpetuity::Retrieval.new self, query
|
226
244
|
end
|
227
245
|
end
|
228
246
|
end
|
@@ -16,9 +16,10 @@ module Perpetuity
|
|
16
16
|
unless @mappers.has_key? klass
|
17
17
|
raise KeyError, "No mapper for #{klass}"
|
18
18
|
end
|
19
|
+
@mappers[klass]
|
19
20
|
end
|
20
21
|
|
21
|
-
|
22
|
+
mapper_class.new(self)
|
22
23
|
end
|
23
24
|
|
24
25
|
def []= klass, mapper
|
@@ -26,7 +27,7 @@ module Perpetuity
|
|
26
27
|
end
|
27
28
|
|
28
29
|
def each &block
|
29
|
-
@mappers.each
|
30
|
+
@mappers.each(&block)
|
30
31
|
end
|
31
32
|
|
32
33
|
def load_mappers
|
@@ -1,10 +1,16 @@
|
|
1
1
|
require 'perpetuity/mongodb/query_attribute'
|
2
|
+
require 'perpetuity/mongodb/nil_query'
|
2
3
|
|
3
4
|
module Perpetuity
|
4
5
|
class MongoDB
|
5
6
|
class Query
|
7
|
+
attr_reader :query
|
6
8
|
def initialize &block
|
7
|
-
|
9
|
+
if block_given?
|
10
|
+
@query = block.call(self)
|
11
|
+
else
|
12
|
+
@query = NilQuery.new
|
13
|
+
end
|
8
14
|
end
|
9
15
|
|
10
16
|
def to_db
|
@@ -18,6 +24,10 @@ module Perpetuity
|
|
18
24
|
def method_missing missing_method
|
19
25
|
QueryAttribute.new missing_method
|
20
26
|
end
|
27
|
+
|
28
|
+
def == other
|
29
|
+
query == other.query
|
30
|
+
end
|
21
31
|
end
|
22
32
|
end
|
23
33
|
end
|
@@ -4,7 +4,7 @@ require 'perpetuity/mongodb/query_intersection'
|
|
4
4
|
module Perpetuity
|
5
5
|
class MongoDB
|
6
6
|
class QueryExpression
|
7
|
-
attr_accessor :comparator, :negated
|
7
|
+
attr_accessor :attribute, :comparator, :negated, :value
|
8
8
|
|
9
9
|
def initialize attribute, comparator, value
|
10
10
|
@attribute = attribute
|
@@ -82,6 +82,13 @@ module Perpetuity
|
|
82
82
|
expr.negated = true
|
83
83
|
expr
|
84
84
|
end
|
85
|
+
|
86
|
+
def == other
|
87
|
+
attribute == other.attribute &&
|
88
|
+
comparator == other.comparator &&
|
89
|
+
value == other.value &&
|
90
|
+
negated == other.negated
|
91
|
+
end
|
85
92
|
end
|
86
93
|
end
|
87
94
|
end
|
@@ -36,7 +36,7 @@ module Perpetuity
|
|
36
36
|
elsif mapper_registry.has_mapper?(value.class)
|
37
37
|
serialize_with_foreign_mapper(value, attrib.embedded?)
|
38
38
|
else
|
39
|
-
|
39
|
+
marshal(value)
|
40
40
|
end
|
41
41
|
|
42
42
|
[attrib.name.to_s, serialized_value]
|
@@ -76,25 +76,26 @@ module Perpetuity
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def unserialize_attribute data
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
end
|
89
|
-
id = metadata['id']
|
79
|
+
return data.map { |i| unserialize_attribute i } if data.is_a? Array
|
80
|
+
return data unless data.is_a? Hash
|
81
|
+
metadata = data.fetch("__metadata__", {})
|
82
|
+
marshaled = data.fetch("__marshaled__", false)
|
83
|
+
|
84
|
+
if marshaled
|
85
|
+
value = data.fetch("value")
|
86
|
+
return unmarshal(value)
|
87
|
+
end
|
90
88
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
89
|
+
if metadata.any?
|
90
|
+
klass = metadata['class'].split('::').inject(Kernel) do |scope, const_name|
|
91
|
+
scope.const_get(const_name)
|
92
|
+
end
|
93
|
+
id = metadata['id']
|
94
|
+
|
95
|
+
if id
|
96
|
+
Reference.new(klass, id)
|
96
97
|
else
|
97
|
-
data
|
98
|
+
unserialize_object(data, klass)
|
98
99
|
end
|
99
100
|
else
|
100
101
|
data
|
@@ -131,7 +132,7 @@ module Perpetuity
|
|
131
132
|
serialize_reference value
|
132
133
|
end
|
133
134
|
else
|
134
|
-
|
135
|
+
marshal value
|
135
136
|
end
|
136
137
|
end
|
137
138
|
end
|
@@ -152,6 +153,17 @@ module Perpetuity
|
|
152
153
|
}
|
153
154
|
}
|
154
155
|
end
|
156
|
+
|
157
|
+
def marshal value
|
158
|
+
{
|
159
|
+
'__marshaled__' => true,
|
160
|
+
'value' => Marshal.dump(value)
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
def unmarshal value
|
165
|
+
Marshal.load(value)
|
166
|
+
end
|
155
167
|
end
|
156
168
|
end
|
157
169
|
end
|
data/lib/perpetuity/mongodb.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'moped'
|
2
2
|
require 'perpetuity/mongodb/query'
|
3
|
+
require 'perpetuity/mongodb/nil_query'
|
3
4
|
require 'perpetuity/mongodb/index'
|
4
5
|
require 'perpetuity/mongodb/serializer'
|
5
6
|
require 'set'
|
6
7
|
require 'perpetuity/exceptions/duplicate_key_error'
|
8
|
+
require 'perpetuity/attribute'
|
7
9
|
|
8
10
|
module Perpetuity
|
9
11
|
class MongoDB
|
@@ -45,11 +47,18 @@ module Perpetuity
|
|
45
47
|
database[klass.to_s]
|
46
48
|
end
|
47
49
|
|
48
|
-
def insert klass,
|
49
|
-
|
50
|
+
def insert klass, objects
|
51
|
+
if objects.is_a? Array
|
52
|
+
objects.each do |object|
|
53
|
+
object[:_id] = object.delete(:id) || Moped::BSON::ObjectId.new
|
54
|
+
end
|
55
|
+
|
56
|
+
collection(klass).insert objects
|
57
|
+
objects.map { |object| object[:_id] }
|
58
|
+
else
|
59
|
+
insert(klass, [objects]).first
|
60
|
+
end
|
50
61
|
|
51
|
-
collection(klass).insert attributes
|
52
|
-
attributes[:_id]
|
53
62
|
rescue Moped::Errors::OperationFailure => e
|
54
63
|
if e.message =~ /duplicate key/
|
55
64
|
e.message =~ /\$(\w+)_\d.*dup key: { : (.*) }/
|
@@ -59,8 +68,8 @@ module Perpetuity
|
|
59
68
|
end
|
60
69
|
end
|
61
70
|
|
62
|
-
def count klass, criteria=
|
63
|
-
q = block_given? ? query(&block).to_db : criteria
|
71
|
+
def count klass, criteria=nil_query, &block
|
72
|
+
q = block_given? ? query(&block).to_db : criteria.to_db
|
64
73
|
collection(klass).find(q).count
|
65
74
|
end
|
66
75
|
|
@@ -77,7 +86,7 @@ module Perpetuity
|
|
77
86
|
|
78
87
|
def retrieve klass, criteria, options = {}
|
79
88
|
# MongoDB uses '_id' as its ID field.
|
80
|
-
criteria = to_bson_id(criteria)
|
89
|
+
criteria = to_bson_id(criteria.to_db)
|
81
90
|
|
82
91
|
skipped = options.fetch(:skip) { 0 }
|
83
92
|
|
@@ -129,7 +138,7 @@ module Perpetuity
|
|
129
138
|
end
|
130
139
|
|
131
140
|
def all klass
|
132
|
-
retrieve klass,
|
141
|
+
retrieve klass, nil_query, {}
|
133
142
|
end
|
134
143
|
|
135
144
|
def delete id, klass
|
@@ -152,6 +161,10 @@ module Perpetuity
|
|
152
161
|
Query.new(&block)
|
153
162
|
end
|
154
163
|
|
164
|
+
def nil_query
|
165
|
+
NilQuery.new
|
166
|
+
end
|
167
|
+
|
155
168
|
def negate_query &block
|
156
169
|
Query.new(&block).negate
|
157
170
|
end
|
@@ -173,7 +186,7 @@ module Perpetuity
|
|
173
186
|
key = index['key'].keys.first
|
174
187
|
direction = index['key'][key]
|
175
188
|
unique = index['unique']
|
176
|
-
Index.new(klass, key, order: Index::KEY_ORDERS[direction], unique: unique)
|
189
|
+
Index.new(klass, Attribute.new(key), order: Index::KEY_ORDERS[direction], unique: unique)
|
177
190
|
end.to_set
|
178
191
|
end
|
179
192
|
|
data/lib/perpetuity/retrieval.rb
CHANGED
@@ -3,12 +3,12 @@ require 'perpetuity/reference'
|
|
3
3
|
module Perpetuity
|
4
4
|
class Retrieval
|
5
5
|
include Enumerable
|
6
|
-
attr_accessor :sort_attribute, :sort_direction, :result_limit, :result_page, :result_offset
|
6
|
+
attr_accessor :sort_attribute, :sort_direction, :result_limit, :result_page, :result_offset, :result_cache
|
7
7
|
|
8
|
-
def initialize mapper,
|
8
|
+
def initialize mapper, query
|
9
9
|
@mapper = mapper
|
10
10
|
@class = mapper.mapped_class
|
11
|
-
@
|
11
|
+
@query = query
|
12
12
|
@data_source = mapper.data_source
|
13
13
|
end
|
14
14
|
|
@@ -16,6 +16,7 @@ module Perpetuity
|
|
16
16
|
retrieval = clone
|
17
17
|
retrieval.sort_attribute = attribute
|
18
18
|
retrieval.sort_direction = :ascending
|
19
|
+
retrieval.clear_cache
|
19
20
|
|
20
21
|
retrieval
|
21
22
|
end
|
@@ -23,6 +24,7 @@ module Perpetuity
|
|
23
24
|
def reverse
|
24
25
|
retrieval = clone
|
25
26
|
retrieval.sort_direction = retrieval.sort_direction == :descending ? :ascending : :descending
|
27
|
+
retrieval.clear_cache
|
26
28
|
|
27
29
|
retrieval
|
28
30
|
end
|
@@ -32,6 +34,7 @@ module Perpetuity
|
|
32
34
|
retrieval.result_limit ||= 20
|
33
35
|
retrieval.result_page = page
|
34
36
|
retrieval.result_offset = (page - 1) * retrieval.result_limit
|
37
|
+
retrieval.clear_cache
|
35
38
|
retrieval
|
36
39
|
end
|
37
40
|
|
@@ -39,6 +42,7 @@ module Perpetuity
|
|
39
42
|
retrieval = clone
|
40
43
|
retrieval.result_limit = per
|
41
44
|
retrieval.result_offset = (retrieval.result_page - 1) * per
|
45
|
+
retrieval.clear_cache
|
42
46
|
retrieval
|
43
47
|
end
|
44
48
|
|
@@ -47,11 +51,11 @@ module Perpetuity
|
|
47
51
|
end
|
48
52
|
|
49
53
|
def to_a
|
50
|
-
@
|
54
|
+
@result_cache ||= @data_source.unserialize(@data_source.retrieve(@class, @query, options), @mapper)
|
51
55
|
end
|
52
56
|
|
53
57
|
def count
|
54
|
-
@data_source.count(@class, @
|
58
|
+
@data_source.count(@class, @query)
|
55
59
|
end
|
56
60
|
|
57
61
|
def first
|
@@ -84,15 +88,22 @@ module Perpetuity
|
|
84
88
|
def limit lim
|
85
89
|
retrieval = clone
|
86
90
|
retrieval.result_limit = lim
|
91
|
+
retrieval.clear_cache
|
87
92
|
|
88
93
|
retrieval
|
89
94
|
end
|
95
|
+
alias_method :take, :limit
|
90
96
|
|
91
97
|
def drop count
|
92
98
|
retrieval = clone
|
93
99
|
retrieval.result_offset = count
|
100
|
+
retrieval.clear_cache
|
94
101
|
|
95
102
|
retrieval
|
96
103
|
end
|
104
|
+
|
105
|
+
def clear_cache
|
106
|
+
@result_cache = nil
|
107
|
+
end
|
97
108
|
end
|
98
109
|
end
|
data/lib/perpetuity/version.rb
CHANGED
@@ -8,6 +8,13 @@ describe 'indexing' do
|
|
8
8
|
index :name, unique: true
|
9
9
|
end
|
10
10
|
end
|
11
|
+
let(:mapper_class_without_index) do
|
12
|
+
klass = mapper_class.dup
|
13
|
+
klass.new.indexes.reject! do |index|
|
14
|
+
index.attribute.name == :name
|
15
|
+
end
|
16
|
+
klass
|
17
|
+
end
|
11
18
|
let(:mapper) { mapper_class.new }
|
12
19
|
let(:name_index) do
|
13
20
|
mapper.indexes.find do |index|
|
@@ -39,5 +46,14 @@ describe 'indexing' do
|
|
39
46
|
it 'specifies uniqueness of the index' do
|
40
47
|
name_index.should be_unique
|
41
48
|
end
|
49
|
+
|
50
|
+
it 'removes other indexes' do
|
51
|
+
mapper.reindex!
|
52
|
+
mapper_without_index = mapper_class_without_index.new
|
53
|
+
mapper_without_index.reindex!
|
54
|
+
mapper.data_source.active_indexes(Object).any? do |index|
|
55
|
+
index.attribute.name.to_s == 'name'
|
56
|
+
end.should be_false
|
57
|
+
end
|
42
58
|
end
|
43
59
|
|
@@ -48,6 +48,15 @@ module Perpetuity
|
|
48
48
|
its(:password) { should == password }
|
49
49
|
end
|
50
50
|
|
51
|
+
it 'inserts documents into a collection' do
|
52
|
+
expect { mongo.insert klass, name: 'foo' }.to change { mongo.count klass }.by 1
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'inserts multiple documents into a collection' do
|
56
|
+
expect { mongo.insert klass, [{name: 'foo'}, {name: 'bar'}] }
|
57
|
+
.to change { mongo.count klass }.by 2
|
58
|
+
end
|
59
|
+
|
51
60
|
it 'removes all documents from a collection' do
|
52
61
|
mongo.insert klass, {}
|
53
62
|
mongo.delete_all klass
|
@@ -77,7 +86,7 @@ module Perpetuity
|
|
77
86
|
|
78
87
|
it 'gets all of the documents in a collection' do
|
79
88
|
values = [{value: 1}, {value: 2}]
|
80
|
-
mongo.should_receive(:retrieve).with(Object,
|
89
|
+
mongo.should_receive(:retrieve).with(Object, mongo.nil_query, {})
|
81
90
|
.and_return(values)
|
82
91
|
mongo.all(Object).should == values
|
83
92
|
end
|
@@ -86,7 +95,7 @@ module Perpetuity
|
|
86
95
|
time = Time.now.utc
|
87
96
|
id = mongo.insert Object, {inserted: time}
|
88
97
|
|
89
|
-
object = mongo.retrieve(Object, id
|
98
|
+
object = mongo.retrieve(Object, mongo.query{|o| o.id == id.to_s }).first
|
90
99
|
retrieved_time = object["inserted"]
|
91
100
|
retrieved_time.to_f.should be_within(0.001).of time.to_f
|
92
101
|
end
|
@@ -148,9 +157,10 @@ module Perpetuity
|
|
148
157
|
id = mongo.insert klass, count: 1
|
149
158
|
mongo.increment klass, id, :count
|
150
159
|
mongo.increment klass, id, :count, 10
|
151
|
-
mongo.
|
160
|
+
query = mongo.query { |o| o.id == id }
|
161
|
+
mongo.retrieve(klass, query).first['count'].should be == 12
|
152
162
|
mongo.increment klass, id, :count, -1
|
153
|
-
mongo.retrieve(klass,
|
163
|
+
mongo.retrieve(klass, query).first['count'].should be == 11
|
154
164
|
end
|
155
165
|
end
|
156
166
|
|
@@ -11,6 +11,13 @@ describe 'Persistence' do
|
|
11
11
|
mapper.find(mapper.id_for(article)).title.should eq 'I have a title'
|
12
12
|
end
|
13
13
|
|
14
|
+
it 'persists multiple objects' do
|
15
|
+
mapper.delete_all
|
16
|
+
articles = 2.times.map { Article.new(SecureRandom.hex) }
|
17
|
+
expect { mapper.insert articles }.to change { mapper.count }.by 2
|
18
|
+
mapper.all.sort(:title).to_a.should == articles.sort_by(&:title)
|
19
|
+
end
|
20
|
+
|
14
21
|
it 'returns the id of the persisted object' do
|
15
22
|
article = Article.new
|
16
23
|
mapper.insert(article).should eq mapper.id_for(article)
|
@@ -53,71 +53,90 @@ describe "retrieval" do
|
|
53
53
|
end
|
54
54
|
|
55
55
|
describe "Array-like syntax" do
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
56
|
+
describe 'using comparison operators' do
|
57
|
+
let(:draft) { Article.new 'Draft', 'draft content', nil, Time.now + 30 }
|
58
|
+
let(:published) { Article.new 'Published', 'content', nil, Time.now - 30, 3 }
|
59
|
+
|
60
|
+
let(:published_id) { mapper.id_for published }
|
61
|
+
let(:draft_id) { mapper.id_for draft }
|
62
|
+
|
63
|
+
before do
|
64
|
+
mapper.insert draft
|
65
|
+
mapper.insert published
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'selects objects using equality' do
|
69
|
+
selected = mapper.select { |article| article.title == 'Published' }
|
70
|
+
ids = selected.map { |article| mapper.id_for article }
|
71
|
+
ids.should include published_id
|
72
|
+
ids.should_not include draft_id
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'selects objects using greater-than' do
|
76
|
+
selected = mapper.select { |article| article.published_at < Time.now }
|
77
|
+
ids = selected.map { |article| mapper.id_for article }
|
78
|
+
ids.should include published_id
|
79
|
+
ids.should_not include draft_id
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'selects objects using greater-than-or-equal' do
|
83
|
+
selected = mapper.select { |article| article.views >= 3 }
|
84
|
+
ids = selected.map { |article| mapper.id_for article }
|
85
|
+
ids.should include published_id
|
86
|
+
ids.should_not include draft_id
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'selects objects using less-than' do
|
90
|
+
selected = mapper.select { |article| article.views < 3 }
|
91
|
+
ids = selected.map { |article| mapper.id_for article }
|
92
|
+
ids.should include draft_id
|
93
|
+
ids.should_not include published_id
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'selects objects using less-than-or-equal' do
|
97
|
+
selected = mapper.select { |article| article.views <= 0 }
|
98
|
+
ids = selected.map { |article| mapper.id_for article }
|
99
|
+
ids.should include draft_id
|
100
|
+
ids.should_not include published_id
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'selects objects using inequality' do
|
104
|
+
selected = mapper.select { |article| article.title != 'Draft' }
|
105
|
+
ids = selected.map { |article| mapper.id_for article }
|
106
|
+
ids.should_not include draft_id
|
107
|
+
ids.should include published_id
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'selects objects using regular expressions' do
|
111
|
+
selected = mapper.select { |article| article.title =~ /Pub/ }
|
112
|
+
ids = selected.map { |article| mapper.id_for article }
|
113
|
+
ids.should include published_id
|
114
|
+
ids.should_not include draft_id
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'selects objects using inclusion' do
|
118
|
+
selected = mapper.select { |article| article.title.in %w( Published ) }
|
119
|
+
ids = selected.map { |article| mapper.id_for article }
|
120
|
+
ids.should include published_id
|
121
|
+
ids.should_not include draft_id
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'selects objects that are truthy' do
|
126
|
+
article_with_truthy_title = Article.new('I have a title')
|
127
|
+
article_with_false_title = Article.new(false)
|
128
|
+
article_with_nil_title = Article.new(nil)
|
129
|
+
|
130
|
+
false_id = mapper.insert article_with_false_title
|
131
|
+
truthy_id = mapper.insert article_with_truthy_title
|
132
|
+
nil_id = mapper.insert article_with_nil_title
|
133
|
+
|
134
|
+
selected = mapper.select { |article| article.title }
|
135
|
+
ids = selected.map { |article| mapper.id_for(article) }
|
136
|
+
|
137
|
+
ids.should include truthy_id
|
138
|
+
ids.should_not include false_id
|
139
|
+
ids.should_not include nil_id
|
121
140
|
end
|
122
141
|
end
|
123
142
|
|
@@ -180,7 +199,7 @@ describe "retrieval" do
|
|
180
199
|
user = User.new(first_name: 'foo', last_name: 'bar')
|
181
200
|
mapper = Perpetuity[User]
|
182
201
|
mapper.insert user
|
183
|
-
users = mapper.select { |
|
202
|
+
users = mapper.select { |u| u.name.first_name == 'foo' }
|
184
203
|
ids = users.map { |retrieved_user| mapper.id_for(retrieved_user) }
|
185
204
|
ids.should include mapper.id_for(user)
|
186
205
|
end
|
@@ -203,4 +222,13 @@ describe "retrieval" do
|
|
203
222
|
articles.should include mapper.sample
|
204
223
|
end
|
205
224
|
end
|
225
|
+
|
226
|
+
it 'does not unmarshal objects that were saved as strings' do
|
227
|
+
fake_title = Marshal.dump(Object.new)
|
228
|
+
id = mapper.insert Article.new(fake_title)
|
229
|
+
|
230
|
+
retrieved = mapper.find(id)
|
231
|
+
retrieved.title.should be_a String
|
232
|
+
retrieved.title.should == fake_title
|
233
|
+
end
|
206
234
|
end
|
@@ -53,8 +53,8 @@ describe 'updating' do
|
|
53
53
|
mapper.save retrieved_book
|
54
54
|
|
55
55
|
retrieved_authors = Perpetuity[Book].find(mapper.id_for retrieved_book).authors
|
56
|
-
retrieved_authors.map(&:klass).should == [User, User]
|
57
|
-
retrieved_authors.map(&:id).should == [mapper.id_for(dave), mapper.id_for(andy)]
|
56
|
+
retrieved_authors.map(&:klass).should be == [User, User]
|
57
|
+
retrieved_authors.map(&:id).should be == [mapper.id_for(dave), mapper.id_for(andy)]
|
58
58
|
end
|
59
59
|
|
60
60
|
describe 'atomic increments/decrements' do
|
@@ -30,6 +30,8 @@ module Perpetuity
|
|
30
30
|
|
31
31
|
context 'with multiple references' do
|
32
32
|
it 'returns the array of dereferenced objects' do
|
33
|
+
first.instance_variable_set :@id, 1
|
34
|
+
second.instance_variable_set :@id, 2
|
33
35
|
mapper.should_receive(:select) { objects }
|
34
36
|
derefer.load([first_ref, second_ref]).should == objects
|
35
37
|
end
|
@@ -48,8 +48,8 @@ module Perpetuity
|
|
48
48
|
data_source.should_receive(:can_serialize?).with('foo') { true }
|
49
49
|
data_source.should_receive(:insert)
|
50
50
|
.with(Object,
|
51
|
-
{ 'my_attribute' => 'foo' })
|
52
|
-
.and_return('bar')
|
51
|
+
[{ 'my_attribute' => 'foo' }])
|
52
|
+
.and_return(['bar'])
|
53
53
|
|
54
54
|
mapper.insert(obj).should be == 'bar'
|
55
55
|
end
|
@@ -61,10 +61,11 @@ module Perpetuity
|
|
61
61
|
|
62
62
|
describe 'finding a single object' do
|
63
63
|
let(:options) { {:attribute=>nil, :direction=>nil, :limit=>1, :skip=>nil} }
|
64
|
-
let(:returned_object) { double('Retrieved Object', class: Object) }
|
64
|
+
let(:returned_object) { double('Retrieved Object', class: Object, delete: nil) }
|
65
65
|
|
66
66
|
it 'finds an object by ID' do
|
67
|
-
|
67
|
+
returned_object.instance_variable_set :@id, 1
|
68
|
+
criteria = data_source.query { |o| o.id == 1 }
|
68
69
|
data_source.should_receive(:retrieve)
|
69
70
|
.with(Object, criteria, options) { [returned_object] }
|
70
71
|
|
@@ -72,26 +73,26 @@ module Perpetuity
|
|
72
73
|
end
|
73
74
|
|
74
75
|
it 'finds multiple objects with a block' do
|
75
|
-
criteria = { name
|
76
|
+
criteria = data_source.query { |o| o.name == 'foo' }
|
76
77
|
options = self.options.merge(limit: nil)
|
77
78
|
data_source.should_receive(:retrieve)
|
78
79
|
.with(Object, criteria, options) { [returned_object] }.twice
|
79
80
|
|
80
|
-
mapper.select { |e| e.name == 'foo' }.to_a.should == [returned_object]
|
81
|
-
mapper.find_all { |e| e.name == 'foo' }.to_a.should == [returned_object]
|
81
|
+
mapper.select { |e| e.name == 'foo' }.to_a.should be == [returned_object]
|
82
|
+
mapper.find_all { |e| e.name == 'foo' }.to_a.should be == [returned_object]
|
82
83
|
end
|
83
84
|
|
84
85
|
it 'finds an object with a block' do
|
85
|
-
criteria = { name
|
86
|
+
criteria = data_source.query { |o| o.name == 'foo' }
|
86
87
|
data_source.should_receive(:retrieve)
|
87
88
|
.with(Object, criteria, options) { [returned_object] }.twice
|
88
|
-
mapper.find { |o| o.name == 'foo' }.should == returned_object
|
89
|
-
mapper.detect { |o| o.name == 'foo' }.should == returned_object
|
89
|
+
mapper.find { |o| o.name == 'foo' }.should be == returned_object
|
90
|
+
mapper.detect { |o| o.name == 'foo' }.should be == returned_object
|
90
91
|
end
|
91
92
|
|
92
93
|
it 'caches results' do
|
93
94
|
mapper.give_id_to returned_object, 1
|
94
|
-
criteria = { id
|
95
|
+
criteria = data_source.query { |o| o.id == 1 }
|
95
96
|
data_source.should_receive(:retrieve)
|
96
97
|
.with(Object, criteria, options) { [returned_object] }
|
97
98
|
.once
|
@@ -101,7 +102,7 @@ module Perpetuity
|
|
101
102
|
end
|
102
103
|
|
103
104
|
it 'does not cache nil results' do
|
104
|
-
criteria = { id
|
105
|
+
criteria = data_source.query { |o| o.id == 1 }
|
105
106
|
data_source.should_receive(:retrieve)
|
106
107
|
.with(Object, criteria, options) { [] }
|
107
108
|
.twice
|
@@ -50,5 +50,9 @@ module Perpetuity
|
|
50
50
|
it 'checks for inclusion' do
|
51
51
|
(attribute.in [1, 2, 3]).should be_a MongoDB::QueryExpression
|
52
52
|
end
|
53
|
+
|
54
|
+
it 'checks for its own truthiness' do
|
55
|
+
attribute.to_db.should == ((attribute != false) & (attribute != nil)).to_db
|
56
|
+
end
|
53
57
|
end
|
54
58
|
end
|
@@ -63,11 +63,7 @@ module Perpetuity
|
|
63
63
|
|
64
64
|
context 'with objects that have hashes as attributes' do
|
65
65
|
let(:name_data) { {first_name: 'Jamie', last_name: 'Gaskins'} }
|
66
|
-
let(:serialized_data)
|
67
|
-
{
|
68
|
-
'name' => name_data
|
69
|
-
}
|
70
|
-
end
|
66
|
+
let(:serialized_data) { { 'name' => name_data } }
|
71
67
|
let(:user) { User.new(name_data) }
|
72
68
|
let(:user_serializer) { Serializer.new(user_mapper) }
|
73
69
|
|
@@ -86,21 +82,6 @@ module Perpetuity
|
|
86
82
|
end
|
87
83
|
end
|
88
84
|
|
89
|
-
describe 'unserializes attributes' do
|
90
|
-
let(:unserializable_object) { 1.to_c }
|
91
|
-
let(:serialized_attrs) { [ Marshal.dump(unserializable_object) ] }
|
92
|
-
let(:objects) { serializer.unserialize(serialized_attrs) }
|
93
|
-
subject { objects.first }
|
94
|
-
|
95
|
-
before do
|
96
|
-
user_mapper.stub(data_source: data_source)
|
97
|
-
book_mapper.stub(data_source: data_source)
|
98
|
-
end
|
99
|
-
|
100
|
-
it { should be_a Complex }
|
101
|
-
it { should eq unserializable_object}
|
102
|
-
end
|
103
|
-
|
104
85
|
describe 'with an array of references' do
|
105
86
|
let(:author) { Reference.new(User, 1) }
|
106
87
|
let(:title) { 'title' }
|
@@ -147,6 +128,73 @@ module Perpetuity
|
|
147
128
|
serializer.serialize(car).should == { 'model' => car_model }
|
148
129
|
end
|
149
130
|
end
|
131
|
+
|
132
|
+
context 'with marshaled data' do
|
133
|
+
let(:unserializable_value) { 1..10 }
|
134
|
+
|
135
|
+
it 'stores metadata with marshal information' do
|
136
|
+
book = Book.new(unserializable_value)
|
137
|
+
|
138
|
+
book_mapper.stub(data_source: data_source)
|
139
|
+
data_source.stub(:can_serialize?).with(book.title) { false }
|
140
|
+
|
141
|
+
serializer.serialize(book).should == {
|
142
|
+
'title' => {
|
143
|
+
'__marshaled__' => true,
|
144
|
+
'value' => Marshal.dump(unserializable_value)
|
145
|
+
},
|
146
|
+
'authors' => []
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'stores marshaled attributes within arrays' do
|
151
|
+
book = Book.new([unserializable_value])
|
152
|
+
book_mapper.stub(data_source: data_source)
|
153
|
+
data_source.stub(:can_serialize?).with(book.title.first) { false }
|
154
|
+
|
155
|
+
serializer.serialize(book).should == {
|
156
|
+
'title' => [{
|
157
|
+
'__marshaled__' => true,
|
158
|
+
'value' => Marshal.dump(unserializable_value)
|
159
|
+
}],
|
160
|
+
'authors' => []
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'unmarshals data that has been marshaled by the serializer' do
|
165
|
+
data = {
|
166
|
+
'title' => {
|
167
|
+
'__marshaled__' => true,
|
168
|
+
'value' => Marshal.dump(unserializable_value),
|
169
|
+
}
|
170
|
+
}
|
171
|
+
serializer.unserialize(data).title.should be_a unserializable_value.class
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'does not unmarshal data not marshaled by the serializer' do
|
175
|
+
data = { 'title' => Marshal.dump(unserializable_value) }
|
176
|
+
|
177
|
+
serializer.unserialize(data).title.should be_a String
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'unserializes a hash of primitives' do
|
182
|
+
time = Time.now
|
183
|
+
serialized_data = {
|
184
|
+
'number' => 1,
|
185
|
+
'string' => 'hello',
|
186
|
+
'boolean' => true,
|
187
|
+
'float' => 7.5,
|
188
|
+
'time' => time
|
189
|
+
}
|
190
|
+
|
191
|
+
object = serializer.unserialize(serialized_data)
|
192
|
+
object.instance_variable_get(:@number).should == 1
|
193
|
+
object.instance_variable_get(:@string).should == 'hello'
|
194
|
+
object.instance_variable_get(:@boolean).should == true
|
195
|
+
object.instance_variable_get(:@float).should == 7.5
|
196
|
+
object.instance_variable_get(:@time).should == time
|
197
|
+
end
|
150
198
|
end
|
151
199
|
end
|
152
200
|
end
|
@@ -18,34 +18,34 @@ module Perpetuity
|
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'returns the id as to_param' do
|
21
|
-
object.to_param.should == nil
|
21
|
+
object.to_param.should be == nil
|
22
22
|
object.id = 'foo'
|
23
|
-
object.to_param.should == 'foo'
|
23
|
+
object.to_param.should be == 'foo'
|
24
24
|
end
|
25
25
|
|
26
26
|
it 'returns the keys on the object' do
|
27
|
-
object.to_key.should == nil
|
27
|
+
object.to_key.should be == nil
|
28
28
|
object.id = 'bar'
|
29
|
-
object.to_key.should == ['bar']
|
29
|
+
object.to_key.should be == ['bar']
|
30
30
|
end
|
31
31
|
|
32
32
|
it 'returns the model name' do
|
33
|
-
klass.model_name.should == klass
|
33
|
+
klass.model_name.should be == klass
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'returns the param_key' do
|
37
37
|
stub_const 'Foo::Bar', klass
|
38
|
-
Foo::Bar.param_key.should == 'foo_bar'
|
38
|
+
Foo::Bar.param_key.should be == 'foo_bar'
|
39
39
|
end
|
40
40
|
|
41
41
|
it 'returns the route_key' do
|
42
42
|
stub_const 'Foo::Bar', klass
|
43
|
-
Foo::Bar.route_key.should == 'foo_bars'
|
43
|
+
Foo::Bar.route_key.should be == 'foo_bars'
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'returns the singular_route_key' do
|
47
47
|
stub_const 'Foo::Bar', klass
|
48
|
-
Foo::Bar.singular_route_key.should == 'foo_bar'
|
48
|
+
Foo::Bar.singular_route_key.should be == 'foo_bar'
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
@@ -5,7 +5,8 @@ module Perpetuity
|
|
5
5
|
let(:data_source) { double('data_source') }
|
6
6
|
let(:registry) { double('mapper_registry') }
|
7
7
|
let(:mapper) { double(mapped_class: Object, data_source: data_source, mapper_registry: registry) }
|
8
|
-
let(:
|
8
|
+
let(:query) { double('Query', to_db: {}) }
|
9
|
+
let(:retrieval) { Perpetuity::Retrieval.new mapper, query }
|
9
10
|
subject { retrieval }
|
10
11
|
|
11
12
|
it "sorts the results" do
|
@@ -53,12 +54,18 @@ module Perpetuity
|
|
53
54
|
return_object.stub(id: return_data[:id])
|
54
55
|
options = { attribute: nil, direction: nil, limit: nil, skip: nil }
|
55
56
|
|
56
|
-
data_source.should_receive(:retrieve).with(Object,
|
57
|
+
data_source.should_receive(:retrieve).with(Object, query, options).
|
57
58
|
and_return([return_data])
|
58
59
|
data_source.should_receive(:unserialize).with([return_data], mapper) { [return_object] }
|
59
60
|
results = retrieval.to_a
|
60
61
|
|
61
62
|
results.map(&:id).should == [0]
|
62
63
|
end
|
64
|
+
|
65
|
+
it 'clears results cache' do
|
66
|
+
retrieval.result_cache = [1,2,3]
|
67
|
+
retrieval.clear_cache
|
68
|
+
retrieval.result_cache.should be_nil
|
69
|
+
end
|
63
70
|
end
|
64
71
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perpetuity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jamie Gaskins
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-08-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -79,6 +79,7 @@ files:
|
|
79
79
|
- lib/perpetuity/mapper_registry.rb
|
80
80
|
- lib/perpetuity/mongodb.rb
|
81
81
|
- lib/perpetuity/mongodb/index.rb
|
82
|
+
- lib/perpetuity/mongodb/nil_query.rb
|
82
83
|
- lib/perpetuity/mongodb/query.rb
|
83
84
|
- lib/perpetuity/mongodb/query_attribute.rb
|
84
85
|
- lib/perpetuity/mongodb/query_expression.rb
|
@@ -159,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
159
160
|
version: '0'
|
160
161
|
requirements: []
|
161
162
|
rubyforge_project:
|
162
|
-
rubygems_version: 2.0.
|
163
|
+
rubygems_version: 2.0.5
|
163
164
|
signing_key:
|
164
165
|
specification_version: 4
|
165
166
|
summary: Persistence library allowing serialization of Ruby objects
|