perpetuity 0.4.7 → 0.4.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +9 -0
- data/lib/perpetuity/dereferencer.rb +47 -0
- data/lib/perpetuity/identity_map.rb +10 -18
- data/lib/perpetuity/mapper.rb +11 -12
- data/lib/perpetuity/mongodb/serializer.rb +151 -0
- data/lib/perpetuity/mongodb.rb +33 -27
- data/lib/perpetuity/persisted_object.rb +1 -1
- data/lib/perpetuity/retrieval.rb +9 -8
- data/lib/perpetuity/version.rb +1 -1
- data/lib/perpetuity.rb +7 -1
- data/spec/integration/associations_spec.rb +33 -1
- data/spec/integration/mongodb_spec.rb +7 -0
- data/spec/integration/pagination_spec.rb +1 -1
- data/spec/integration/retrieval_spec.rb +41 -23
- data/spec/integration/update_spec.rb +18 -0
- data/spec/perpetuity/dereferencer_spec.rb +23 -0
- data/spec/perpetuity/identity_map_spec.rb +22 -0
- data/spec/perpetuity/mapper_spec.rb +0 -19
- data/spec/perpetuity/mongodb/serializer_spec.rb +117 -0
- data/spec/perpetuity/retrieval_spec.rb +4 -10
- data/spec/spec_helper.rb +1 -4
- data/spec/support/test_classes/generic_object.rb +3 -0
- data/spec/support/test_classes/user.rb +4 -0
- data/spec/support/test_classes.rb +6 -1
- metadata +20 -13
- data/lib/perpetuity/serializer.rb +0 -147
- data/spec/perpetuity/serializer_spec.rb +0 -86
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## Version 0.4.8
|
2
|
+
|
3
|
+
- Provide configuration one-liner ability for simple configs
|
4
|
+
- Memoize results for Retrieval objects
|
5
|
+
- `Retrieval#count` sends a count query rather than retrieving the result set and returning the count of the array
|
6
|
+
- Allow updating of objects that contain `Perpetuity::Reference`s to other persisted objects without first loading the association
|
7
|
+
- Fix bug preventing objects with an array of referenced objects from being updated
|
8
|
+
- Move serialization into the MongoDB adapter, allowing future adapters to implement their own serializations
|
9
|
+
|
1
10
|
## Version 0.4.7
|
2
11
|
|
3
12
|
- Use instance variables rather than attr_readers in IdentityMap (avoids calling methods during `load_association!`).
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'perpetuity/identity_map'
|
2
|
+
require 'perpetuity/reference'
|
3
|
+
|
4
|
+
module Perpetuity
|
5
|
+
class Dereferencer
|
6
|
+
attr_reader :map, :mapper_registry
|
7
|
+
|
8
|
+
def initialize mapper_registry
|
9
|
+
@map = IdentityMap.new
|
10
|
+
@mapper_registry = mapper_registry
|
11
|
+
end
|
12
|
+
|
13
|
+
def load references
|
14
|
+
references = Array(references).flatten.select {|ref| referenceable?(ref) }
|
15
|
+
|
16
|
+
cache grouped(references).map { |klass, refs|
|
17
|
+
objects klass, refs.map(&:id)
|
18
|
+
}.flatten
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache objects
|
22
|
+
objects.each { |object| map << object }
|
23
|
+
end
|
24
|
+
|
25
|
+
def [] reference
|
26
|
+
if referenceable?(reference)
|
27
|
+
map[reference.klass, reference.id]
|
28
|
+
else
|
29
|
+
reference
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def grouped references
|
34
|
+
references.group_by(&:klass)
|
35
|
+
end
|
36
|
+
|
37
|
+
def objects klass, ids
|
38
|
+
mapper_registry[klass].select { |object|
|
39
|
+
object.id.in ids.uniq
|
40
|
+
}.to_a
|
41
|
+
end
|
42
|
+
|
43
|
+
def referenceable? ref
|
44
|
+
[:klass, :id].all? { |msg| ref.respond_to?(msg) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -1,25 +1,17 @@
|
|
1
1
|
module Perpetuity
|
2
2
|
class IdentityMap
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
Hash[
|
12
|
-
mapper_registry[klass].select { |object|
|
13
|
-
object.id.in ref.map(&:id).uniq
|
14
|
-
}.map { |obj| [obj.id, obj] }
|
15
|
-
]
|
16
|
-
]
|
17
|
-
}
|
18
|
-
]
|
3
|
+
attr_reader :map
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@map = Hash.new { |hash, key| hash[key] = {} }
|
7
|
+
end
|
8
|
+
|
9
|
+
def [] klass, id
|
10
|
+
map[klass][id]
|
19
11
|
end
|
20
12
|
|
21
|
-
def
|
22
|
-
|
13
|
+
def << object
|
14
|
+
map[object.class][object.id] = object
|
23
15
|
end
|
24
16
|
end
|
25
17
|
end
|
data/lib/perpetuity/mapper.rb
CHANGED
@@ -2,8 +2,7 @@ require 'perpetuity/attribute_set'
|
|
2
2
|
require 'perpetuity/attribute'
|
3
3
|
require 'perpetuity/validations'
|
4
4
|
require 'perpetuity/data_injectable'
|
5
|
-
require 'perpetuity/
|
6
|
-
require 'perpetuity/identity_map'
|
5
|
+
require 'perpetuity/dereferencer'
|
7
6
|
require 'perpetuity/retrieval'
|
8
7
|
|
9
8
|
module Perpetuity
|
@@ -95,23 +94,23 @@ module Perpetuity
|
|
95
94
|
end
|
96
95
|
|
97
96
|
def delete object
|
98
|
-
|
97
|
+
id = object.is_a?(PersistedObject) ? object.id : object
|
98
|
+
data_source.delete id, mapped_class
|
99
99
|
end
|
100
100
|
|
101
101
|
def load_association! object, attribute
|
102
102
|
objects = Array(object)
|
103
|
-
|
103
|
+
dereferencer = Dereferencer.new(mapper_registry)
|
104
|
+
dereferencer.load objects.map { |obj| obj.instance_variable_get("@#{attribute}") }
|
104
105
|
|
105
|
-
objects.each do |
|
106
|
-
reference =
|
106
|
+
objects.each do |obj|
|
107
|
+
reference = obj.instance_variable_get("@#{attribute}")
|
107
108
|
if reference.is_a? Array
|
108
109
|
refs = reference
|
109
|
-
real_objects = refs.map { |ref|
|
110
|
-
inject_attribute
|
110
|
+
real_objects = refs.map { |ref| dereferencer[ref] }
|
111
|
+
inject_attribute obj, attribute, real_objects
|
111
112
|
else
|
112
|
-
|
113
|
-
id = reference.id
|
114
|
-
inject_attribute object, attribute, identity_map[klass][id]
|
113
|
+
inject_attribute obj, attribute, dereferencer[reference]
|
115
114
|
end
|
116
115
|
end
|
117
116
|
end
|
@@ -148,7 +147,7 @@ module Perpetuity
|
|
148
147
|
end
|
149
148
|
|
150
149
|
def serialize object
|
151
|
-
|
150
|
+
data_source.serialize(object, self)
|
152
151
|
end
|
153
152
|
|
154
153
|
def self.mapped_class
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'perpetuity/data_injectable'
|
2
|
+
|
3
|
+
module Perpetuity
|
4
|
+
class MongoDB
|
5
|
+
class Serializer
|
6
|
+
include DataInjectable
|
7
|
+
|
8
|
+
attr_reader :mapper, :mapper_registry
|
9
|
+
|
10
|
+
def initialize(mapper)
|
11
|
+
@mapper = mapper
|
12
|
+
@class = mapper.mapped_class
|
13
|
+
@mapper_registry = mapper.mapper_registry
|
14
|
+
end
|
15
|
+
|
16
|
+
def attribute_for object, attribute_name
|
17
|
+
object.instance_variable_get("@#{attribute_name}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def serialize object
|
21
|
+
attrs = mapper.class.attribute_set.map do |attrib|
|
22
|
+
value = attribute_for object, attrib.name
|
23
|
+
|
24
|
+
serialized_value = if value.is_a? Reference
|
25
|
+
serialize_reference value
|
26
|
+
elsif value.is_a? Array
|
27
|
+
serialize_array(value, attrib.embedded?)
|
28
|
+
elsif mapper.data_source.can_serialize? value
|
29
|
+
value
|
30
|
+
elsif mapper_registry.has_mapper?(value.class)
|
31
|
+
serialize_with_foreign_mapper(value, attrib.embedded?)
|
32
|
+
else
|
33
|
+
Marshal.dump(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
[attrib.name.to_s, serialized_value]
|
37
|
+
end
|
38
|
+
|
39
|
+
Hash[attrs]
|
40
|
+
end
|
41
|
+
|
42
|
+
def unserialize data
|
43
|
+
if data.is_a? Array
|
44
|
+
unserialize_object_array data
|
45
|
+
else
|
46
|
+
object = unserialize_object(data)
|
47
|
+
give_id_to object
|
48
|
+
object
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def unserialize_object data, klass=@class
|
53
|
+
if data.is_a? Hash
|
54
|
+
object = klass.allocate
|
55
|
+
data.each do |attr, value|
|
56
|
+
inject_attribute object, attr, unserialize_attribute(value)
|
57
|
+
end
|
58
|
+
object
|
59
|
+
else
|
60
|
+
unserialize_attribute data
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def unserialize_object_array objects
|
65
|
+
objects.map do |data|
|
66
|
+
object = unserialize_object data
|
67
|
+
give_id_to object
|
68
|
+
object
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def unserialize_attribute data
|
73
|
+
if data.is_a?(String) && data.start_with?("\u0004") # if it's marshaled
|
74
|
+
Marshal.load(data)
|
75
|
+
elsif data.is_a? Array
|
76
|
+
data.map { |i| unserialize_attribute i }
|
77
|
+
elsif data.is_a? Hash
|
78
|
+
metadata = data.delete('__metadata__')
|
79
|
+
if metadata
|
80
|
+
klass = metadata['class'].split('::').inject(Kernel) do |scope, const_name|
|
81
|
+
scope.const_get(const_name)
|
82
|
+
end
|
83
|
+
id = metadata['id']
|
84
|
+
|
85
|
+
if id
|
86
|
+
object = Reference.new(klass, id)
|
87
|
+
else
|
88
|
+
object = unserialize_object(data, klass)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
data
|
92
|
+
end
|
93
|
+
else
|
94
|
+
data
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def serialize_with_foreign_mapper value, embedded = false
|
99
|
+
if embedded
|
100
|
+
value_mapper = mapper_registry[value.class]
|
101
|
+
value_serializer = Serializer.new(value_mapper)
|
102
|
+
attr = value_serializer.serialize(value)
|
103
|
+
attr.merge '__metadata__' => { 'class' => value.class }
|
104
|
+
else
|
105
|
+
serialize_reference(value)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def serialize_array enum, embedded
|
110
|
+
enum.map do |value|
|
111
|
+
if value.is_a? Reference
|
112
|
+
serialize_reference value
|
113
|
+
elsif value.is_a? Array
|
114
|
+
serialize_array(value)
|
115
|
+
elsif mapper.data_source.can_serialize? value
|
116
|
+
value
|
117
|
+
elsif mapper_registry.has_mapper?(value.class)
|
118
|
+
if embedded
|
119
|
+
{
|
120
|
+
'__metadata__' => {
|
121
|
+
'class' => value.class.to_s
|
122
|
+
}
|
123
|
+
}.merge mapper_registry[value.class].serialize(value)
|
124
|
+
else
|
125
|
+
serialize_reference value
|
126
|
+
end
|
127
|
+
else
|
128
|
+
Marshal.dump(value)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def serialize_reference value
|
134
|
+
if value.is_a? Reference
|
135
|
+
reference = value
|
136
|
+
else
|
137
|
+
unless value.is_a? PersistedObject
|
138
|
+
mapper_registry[value.class].insert value
|
139
|
+
end
|
140
|
+
reference = Reference.new(value.class.to_s, value.id)
|
141
|
+
end
|
142
|
+
{
|
143
|
+
'__metadata__' => {
|
144
|
+
'class' => reference.klass.to_s,
|
145
|
+
'id' => reference.id
|
146
|
+
}
|
147
|
+
}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/lib/perpetuity/mongodb.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'moped'
|
2
2
|
require 'perpetuity/mongodb/query'
|
3
3
|
require 'perpetuity/mongodb/index'
|
4
|
+
require 'perpetuity/mongodb/serializer'
|
4
5
|
require 'set'
|
5
6
|
require 'perpetuity/exceptions/duplicate_key_error'
|
6
7
|
|
@@ -45,12 +46,7 @@ module Perpetuity
|
|
45
46
|
end
|
46
47
|
|
47
48
|
def insert klass, attributes
|
48
|
-
|
49
|
-
attributes[:_id] = attributes[:id]
|
50
|
-
attributes.delete :id
|
51
|
-
else
|
52
|
-
attributes[:_id] = Moped::BSON::ObjectId.new
|
53
|
-
end
|
49
|
+
attributes[:_id] = attributes.delete(:id) || Moped::BSON::ObjectId.new
|
54
50
|
|
55
51
|
collection(klass).insert attributes
|
56
52
|
attributes[:_id]
|
@@ -63,8 +59,9 @@ module Perpetuity
|
|
63
59
|
end
|
64
60
|
end
|
65
61
|
|
66
|
-
def count klass
|
67
|
-
|
62
|
+
def count klass, criteria={}, options={}
|
63
|
+
criteria = to_bson_id(criteria)
|
64
|
+
collection(klass).find(criteria).count
|
68
65
|
end
|
69
66
|
|
70
67
|
def delete_all klass
|
@@ -80,27 +77,31 @@ module Perpetuity
|
|
80
77
|
|
81
78
|
def retrieve klass, criteria, options = {}
|
82
79
|
# MongoDB uses '_id' as its ID field.
|
83
|
-
|
84
|
-
if criteria[:id].is_a? String
|
85
|
-
criteria = { _id: (Moped::BSON::ObjectId(criteria[:id].to_s) rescue criteria[:id]) }
|
86
|
-
else
|
87
|
-
criteria[:_id] = criteria.delete(:id)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
query = collection(klass.to_s).find(criteria)
|
80
|
+
criteria = to_bson_id(criteria)
|
92
81
|
|
93
82
|
skipped = options[:page] ? (options[:page] - 1) * options[:limit] : 0
|
94
|
-
query = query.skip skipped
|
95
|
-
query = query.limit(options[:limit])
|
96
|
-
query = sort(query, options)
|
97
83
|
|
98
|
-
query
|
84
|
+
query = collection(klass.to_s)
|
85
|
+
.find(criteria)
|
86
|
+
.skip(skipped)
|
87
|
+
.limit(options[:limit])
|
88
|
+
|
89
|
+
sort(query, options).map do |document|
|
99
90
|
document[:id] = document.delete("_id")
|
100
91
|
document
|
101
92
|
end
|
102
93
|
end
|
103
94
|
|
95
|
+
def to_bson_id criteria
|
96
|
+
criteria = criteria.dup
|
97
|
+
if criteria.has_key?(:id)
|
98
|
+
criteria[:_id] = Moped::BSON::ObjectId(criteria[:id]) rescue criteria[:id]
|
99
|
+
criteria.delete :id
|
100
|
+
end
|
101
|
+
|
102
|
+
criteria
|
103
|
+
end
|
104
|
+
|
104
105
|
def sort query, options
|
105
106
|
return query unless options[:attribute] &&
|
106
107
|
options[:direction]
|
@@ -116,9 +117,7 @@ module Perpetuity
|
|
116
117
|
retrieve klass, {}, {}
|
117
118
|
end
|
118
119
|
|
119
|
-
def delete
|
120
|
-
id = object_or_id.is_a?(PersistedObject) ? object_or_id.id : object_or_id
|
121
|
-
klass ||= object.class
|
120
|
+
def delete id, klass
|
122
121
|
collection(klass.to_s).find("_id" => id).remove
|
123
122
|
end
|
124
123
|
|
@@ -151,8 +150,7 @@ module Perpetuity
|
|
151
150
|
end
|
152
151
|
|
153
152
|
def active_indexes klass
|
154
|
-
|
155
|
-
indexes.map do |index|
|
153
|
+
collection(klass).indexes.map do |index|
|
156
154
|
key = index['key'].keys.first
|
157
155
|
direction = index['key'][key]
|
158
156
|
unique = index['unique']
|
@@ -173,13 +171,21 @@ module Perpetuity
|
|
173
171
|
coll = collection(index.collection)
|
174
172
|
db_indexes = coll.indexes.select do |db_index|
|
175
173
|
db_index['name'] =~ /\A#{index.attribute}/
|
176
|
-
end.map { |
|
174
|
+
end.map { |idx| idx['key'] }
|
177
175
|
|
178
176
|
if db_indexes.any?
|
179
177
|
collection(index.collection).indexes.drop db_indexes.first
|
180
178
|
end
|
181
179
|
end
|
182
180
|
|
181
|
+
def serialize object, mapper
|
182
|
+
Serializer.new(mapper).serialize object
|
183
|
+
end
|
184
|
+
|
185
|
+
def unserialize data, mapper
|
186
|
+
Serializer.new(mapper).unserialize data
|
187
|
+
end
|
188
|
+
|
183
189
|
private
|
184
190
|
def serializable_types
|
185
191
|
@serializable_types ||= [NilClass, TrueClass, FalseClass, Fixnum, Float, String, Array, Hash, Time]
|
data/lib/perpetuity/retrieval.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'perpetuity/reference'
|
2
|
-
require 'perpetuity/serializer'
|
3
2
|
|
4
3
|
module Perpetuity
|
5
4
|
class Retrieval
|
@@ -46,18 +45,20 @@ module Perpetuity
|
|
46
45
|
end
|
47
46
|
|
48
47
|
def to_a
|
49
|
-
options
|
48
|
+
@results ||= @data_source.unserialize(@data_source.retrieve(@class, @criteria, options), @mapper)
|
49
|
+
end
|
50
|
+
|
51
|
+
def count
|
52
|
+
@data_source.count(@class, @criteria, options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def options
|
56
|
+
{
|
50
57
|
attribute: sort_attribute,
|
51
58
|
direction: sort_direction,
|
52
59
|
limit: result_limit || quantity_per_page,
|
53
60
|
page: result_page
|
54
61
|
}
|
55
|
-
results = @data_source.retrieve(@class, @criteria, options)
|
56
|
-
unserialize results
|
57
|
-
end
|
58
|
-
|
59
|
-
def unserialize(data)
|
60
|
-
Serializer.new(@mapper).unserialize(data)
|
61
62
|
end
|
62
63
|
|
63
64
|
def [] index
|
data/lib/perpetuity/version.rb
CHANGED
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
|
20
|
+
mapper.instance_exec(&block) if block_given?
|
21
21
|
end
|
22
22
|
|
23
23
|
def self.[] klass
|
@@ -27,4 +27,10 @@ module Perpetuity
|
|
27
27
|
def self.mapper_registry
|
28
28
|
@mapper_registry ||= MapperRegistry.new
|
29
29
|
end
|
30
|
+
|
31
|
+
def self.data_source adapter, db_name, options={}
|
32
|
+
adapters = { mongodb: MongoDB }
|
33
|
+
|
34
|
+
configure { data_source adapters[adapter].new(options.merge(db: db_name)) }
|
35
|
+
end
|
30
36
|
end
|
@@ -59,7 +59,39 @@ describe 'associations with other objects' do
|
|
59
59
|
|
60
60
|
books = book_mapper.select { |book| book.id.in book_ids }.to_a
|
61
61
|
book_mapper.load_association! books, :authors
|
62
|
-
books.map(&:authors).flatten.map(&:name).should include
|
62
|
+
books.map(&:authors).flatten.map(&:name).should include(*%w(Dave Andy Matt Aslak))
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'does not try dereferencing non-reference objects' do
|
66
|
+
article = Article.new
|
67
|
+
foo = User.new('foo')
|
68
|
+
bar = 'bar'
|
69
|
+
article.author = [foo, bar]
|
70
|
+
mapper = Perpetuity[Article]
|
71
|
+
|
72
|
+
mapper.insert article
|
73
|
+
retrieved = mapper.find(article.id)
|
74
|
+
mapper.load_association! retrieved, :author
|
75
|
+
retrieved.author.should == [foo, bar]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'embedded relationships' do
|
80
|
+
let(:mapper) { Perpetuity[GenericObject] }
|
81
|
+
let(:object) { GenericObject.new }
|
82
|
+
|
83
|
+
context 'with unserializable embedded attributes' do
|
84
|
+
let(:unserializable_object) { 1.to_c }
|
85
|
+
let(:serialized_attrs) do
|
86
|
+
[ Marshal.dump(unserializable_object) ]
|
87
|
+
end
|
88
|
+
|
89
|
+
before { object.embedded_attribute = [unserializable_object] }
|
90
|
+
|
91
|
+
it 'serializes attributes' do
|
92
|
+
mapper.insert object
|
93
|
+
mapper.find(object.id).embedded_attribute.should be == [unserializable_object]
|
94
|
+
end
|
63
95
|
end
|
64
96
|
end
|
65
97
|
end
|
@@ -62,6 +62,13 @@ module Perpetuity
|
|
62
62
|
mongo.count(klass).should == 3
|
63
63
|
end
|
64
64
|
|
65
|
+
it 'counts documents matching criteria' do
|
66
|
+
mongo.delete_all klass
|
67
|
+
3.times { mongo.insert klass, { name: 'foo' } }
|
68
|
+
3.times { mongo.insert klass, { name: 'bar' } }
|
69
|
+
mongo.count(klass, name: 'foo').should == 3
|
70
|
+
end
|
71
|
+
|
65
72
|
it 'gets the first document in a collection' do
|
66
73
|
value = {value: 1}
|
67
74
|
mongo.insert klass, value
|
@@ -20,7 +20,7 @@ describe 'pagination' do
|
|
20
20
|
it 'specifies per-page quantity' do
|
21
21
|
Perpetuity[Article].delete_all
|
22
22
|
5.times { |i| Perpetuity[Article].insert Article.new i }
|
23
|
-
data = Perpetuity[Article].all.page(3).per_page(2)
|
23
|
+
data = Perpetuity[Article].all.page(3).per_page(2).to_a
|
24
24
|
data.should have(1).item
|
25
25
|
end
|
26
26
|
end
|