perpetuity 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|