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 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
- def initialize objects, attribute, mapper_registry
4
- @map = Hash[
5
- objects.map { |object| object.instance_variable_get("@#{attribute}") }
6
- .flatten
7
- .group_by(&:klass)
8
- .map { |klass, ref|
9
- [
10
- klass,
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 [] klass
22
- @map[klass]
13
+ def << object
14
+ map[object.class][object.id] = object
23
15
  end
24
16
  end
25
17
  end
@@ -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/serializer'
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
- data_source.delete object, mapped_class
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
- identity_map = IdentityMap.new(objects, attribute, mapper_registry)
103
+ dereferencer = Dereferencer.new(mapper_registry)
104
+ dereferencer.load objects.map { |obj| obj.instance_variable_get("@#{attribute}") }
104
105
 
105
- objects.each do |object|
106
- reference = object.instance_variable_get("@#{attribute}")
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| identity_map[ref.klass][ref.id] }
110
- inject_attribute object, attribute, real_objects
110
+ real_objects = refs.map { |ref| dereferencer[ref] }
111
+ inject_attribute obj, attribute, real_objects
111
112
  else
112
- klass = reference.klass
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
- Serializer.new(self).serialize(object)
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
@@ -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
- if attributes.has_key? :id
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
- collection(klass).find.count
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
- if criteria.has_key?(:id)
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.map do |document|
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 object_or_id, klass=nil
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
- indexes = collection(klass).indexes.to_a
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 { |index| index['key'] }
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]
@@ -1,7 +1,7 @@
1
1
  module Perpetuity
2
2
  module PersistedObject
3
3
  def id
4
- @id
4
+ @id if defined? @id
5
5
  end
6
6
  end
7
7
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Perpetuity
2
- VERSION = "0.4.7"
2
+ VERSION = "0.4.8"
3
3
  end
data/lib/perpetuity.rb CHANGED
@@ -17,7 +17,7 @@ module Perpetuity
17
17
  def self.generate_mapper_for klass, &block
18
18
  mapper = Class.new(Mapper)
19
19
  mapper.map klass, mapper_registry
20
- mapper.instance_exec &block if block_given?
20
+ mapper.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 *%w(Dave Andy Matt Aslak)
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