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 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