perpetuity 0.4.7 → 0.4.8
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.
- 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
|