perpetuity 0.4.2 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/CHANGELOG.md +17 -0
- data/lib/perpetuity.rb +9 -2
- data/lib/perpetuity/identity_map.rb +24 -0
- data/lib/perpetuity/mapper.rb +26 -23
- data/lib/perpetuity/mapper_registry.rb +7 -5
- data/lib/perpetuity/mongodb.rb +13 -9
- data/lib/perpetuity/serializer.rb +21 -13
- data/lib/perpetuity/validations/validation_set.rb +1 -0
- data/lib/perpetuity/version.rb +1 -1
- data/spec/perpetuity/mapper_registry_spec.rb +5 -4
- data/spec/perpetuity/mapper_spec.rb +73 -12
- data/spec/perpetuity/serializer_spec.rb +56 -0
- data/spec/perpetuity_spec.rb +33 -2
- data/spec/{test_classes.rb → support/test_classes.rb} +4 -55
- data/spec/support/test_classes/article.rb +11 -0
- data/spec/support/test_classes/book.rb +7 -0
- data/spec/support/test_classes/car.rb +3 -0
- data/spec/support/test_classes/comment.rb +7 -0
- data/spec/support/test_classes/message.rb +13 -0
- data/spec/support/test_classes/topic.rb +3 -0
- data/spec/support/test_classes/user.rb +6 -0
- metadata +25 -4
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## Version 0.4.3
|
2
|
+
|
3
|
+
- Made `Mapper#load_association!` more friendly. It now loads the associated objects for all objects passed in and works with arrays of referenced objects.
|
4
|
+
- `Mapper#load_association!` is also now more efficient — the N+1 queries have been optimized. The number of queries it fires off is now equal to the quantity of different classes of associated objects.
|
5
|
+
- For example, if a user can have either a `UserProfile` or an `AdminProfile` as its `profile` attribute, Perpetuity will use two queries in `#load_association!` if and only if both types of profiles are used.
|
6
|
+
- In that example, if you only query users with a `UserProfile`, only one DB query will be triggered.
|
7
|
+
|
8
|
+
New query example:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
user_mapper = Perpetuity[User]
|
12
|
+
users = user_mapper.all.to_a
|
13
|
+
user_mapper.load_association! users, :profile
|
14
|
+
```
|
15
|
+
|
16
|
+
Each of the users in the DB will have their profiles loaded with a single DB query per profile type and stored in their `profile` attributes.
|
17
|
+
|
1
18
|
## Version 0.4.2
|
2
19
|
|
3
20
|
- Improved speed and stability of `Mapper#first` and `Mapper#all`
|
data/lib/perpetuity.rb
CHANGED
@@ -3,6 +3,7 @@ require "perpetuity/retrieval"
|
|
3
3
|
require "perpetuity/mongodb"
|
4
4
|
require "perpetuity/config"
|
5
5
|
require "perpetuity/mapper"
|
6
|
+
require "perpetuity/mapper_registry"
|
6
7
|
|
7
8
|
module Perpetuity
|
8
9
|
def self.configure &block
|
@@ -14,10 +15,16 @@ module Perpetuity
|
|
14
15
|
end
|
15
16
|
|
16
17
|
def self.generate_mapper_for klass, &block
|
17
|
-
|
18
|
+
mapper = Class.new(Mapper)
|
19
|
+
mapper.map klass, mapper_registry
|
20
|
+
mapper.instance_exec &block if block_given?
|
18
21
|
end
|
19
22
|
|
20
23
|
def self.[] klass
|
21
|
-
|
24
|
+
mapper_registry[klass]
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.mapper_registry
|
28
|
+
@mapper_registry ||= MapperRegistry.new
|
22
29
|
end
|
23
30
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Perpetuity
|
2
|
+
class IdentityMap
|
3
|
+
def initialize objects, attribute, mapper_registry
|
4
|
+
@map = Hash[
|
5
|
+
objects.map(&attribute)
|
6
|
+
.flatten
|
7
|
+
.group_by(&:klass)
|
8
|
+
.map { |klass, ref|
|
9
|
+
[klass,
|
10
|
+
Hash[
|
11
|
+
mapper_registry[klass].select { |object|
|
12
|
+
object.id.in ref.map(&:id).uniq
|
13
|
+
}.map { |obj| [obj.id, obj] }
|
14
|
+
]
|
15
|
+
]
|
16
|
+
}
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
def [] klass
|
21
|
+
@map[klass]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/perpetuity/mapper.rb
CHANGED
@@ -2,21 +2,21 @@ require 'perpetuity/attribute_set'
|
|
2
2
|
require 'perpetuity/attribute'
|
3
3
|
require 'perpetuity/validations'
|
4
4
|
require 'perpetuity/data_injectable'
|
5
|
-
require 'perpetuity/mapper_registry'
|
6
5
|
require 'perpetuity/serializer'
|
6
|
+
require 'perpetuity/identity_map'
|
7
|
+
require 'perpetuity/retrieval'
|
7
8
|
|
8
9
|
module Perpetuity
|
9
10
|
class Mapper
|
10
11
|
include DataInjectable
|
12
|
+
attr_reader :mapper_registry
|
11
13
|
|
12
|
-
def
|
13
|
-
|
14
|
-
mapper.map klass
|
15
|
-
mapper.instance_exec &block if block_given?
|
14
|
+
def initialize registry=Perpetuity.mapper_registry
|
15
|
+
@mapper_registry = registry
|
16
16
|
end
|
17
17
|
|
18
|
-
def self.map klass
|
19
|
-
|
18
|
+
def self.map klass, registry=Perpetuity.mapper_registry
|
19
|
+
registry[klass] = self
|
20
20
|
@mapped_class = klass
|
21
21
|
end
|
22
22
|
|
@@ -70,11 +70,11 @@ module Perpetuity
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def serialize object
|
73
|
-
Serializer.new(self).serialize(object)
|
73
|
+
Serializer.new(self, mapper_registry).serialize(object)
|
74
74
|
end
|
75
75
|
|
76
|
-
def self.data_source
|
77
|
-
|
76
|
+
def self.data_source(configuration=Perpetuity.configuration)
|
77
|
+
configuration.data_source
|
78
78
|
end
|
79
79
|
|
80
80
|
def data_source
|
@@ -102,7 +102,7 @@ module Perpetuity
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def retrieve criteria={}
|
105
|
-
Perpetuity::Retrieval.new mapped_class, criteria
|
105
|
+
Perpetuity::Retrieval.new mapped_class, criteria, data_source
|
106
106
|
end
|
107
107
|
|
108
108
|
def select &block
|
@@ -119,11 +119,21 @@ module Perpetuity
|
|
119
119
|
end
|
120
120
|
|
121
121
|
def load_association! object, attribute
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
122
|
+
objects = Array(object)
|
123
|
+
identity_map = IdentityMap.new(objects, attribute, mapper_registry)
|
124
|
+
|
125
|
+
objects.each do |object|
|
126
|
+
reference = object.send(attribute)
|
127
|
+
if reference.is_a? Array
|
128
|
+
refs = reference
|
129
|
+
real_objects = refs.map { |ref| identity_map[ref.klass][ref.id] }
|
130
|
+
inject_attribute object, attribute, real_objects
|
131
|
+
else
|
132
|
+
klass = reference.klass
|
133
|
+
id = reference.id
|
134
|
+
inject_attribute object, attribute, identity_map[klass][id]
|
135
|
+
end
|
136
|
+
end
|
127
137
|
end
|
128
138
|
|
129
139
|
def self.id &block
|
@@ -146,19 +156,12 @@ module Perpetuity
|
|
146
156
|
end
|
147
157
|
|
148
158
|
def self.validate &block
|
149
|
-
@validations ||= ValidationSet.new
|
150
|
-
|
151
159
|
validations.instance_exec(&block)
|
152
160
|
end
|
153
161
|
|
154
162
|
def self.validations
|
155
163
|
@validations ||= ValidationSet.new
|
156
164
|
end
|
157
|
-
|
158
|
-
private
|
159
|
-
def self.base_class
|
160
|
-
Mapper
|
161
|
-
end
|
162
165
|
end
|
163
166
|
end
|
164
167
|
|
@@ -1,16 +1,18 @@
|
|
1
1
|
module Perpetuity
|
2
2
|
class MapperRegistry
|
3
|
-
|
3
|
+
def initialize
|
4
|
+
@mappers = Hash.new { |_, klass| raise KeyError, "No mapper for #{klass}" }
|
5
|
+
end
|
4
6
|
|
5
|
-
def
|
7
|
+
def has_mapper? klass
|
6
8
|
@mappers.has_key? klass
|
7
9
|
end
|
8
10
|
|
9
|
-
def
|
10
|
-
@mappers[klass].new
|
11
|
+
def [] klass
|
12
|
+
@mappers[klass].new(self)
|
11
13
|
end
|
12
14
|
|
13
|
-
def
|
15
|
+
def []= klass, mapper
|
14
16
|
@mappers[klass] = mapper
|
15
17
|
end
|
16
18
|
end
|
data/lib/perpetuity/mongodb.rb
CHANGED
@@ -60,31 +60,35 @@ module Perpetuity
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def retrieve klass, criteria, options = {}
|
63
|
-
objects = []
|
64
|
-
|
65
63
|
# MongoDB uses '_id' as its ID field.
|
66
64
|
if criteria.has_key?(:id)
|
67
65
|
if criteria[:id].is_a? String
|
68
|
-
criteria = { _id: BSON::ObjectId.from_string(criteria[:id].to_s) }
|
66
|
+
criteria = { _id: (BSON::ObjectId.from_string(criteria[:id].to_s) rescue criteria[:id]) }
|
69
67
|
else
|
70
68
|
criteria[:_id] = criteria.delete(:id)
|
71
69
|
end
|
72
70
|
end
|
73
71
|
|
74
|
-
sort_field = options[:attribute]
|
75
|
-
sort_direction = options[:direction]
|
76
|
-
sort_criteria = [[sort_field, sort_direction]]
|
77
72
|
other_options = { limit: options[:limit] }
|
78
73
|
if options[:page]
|
79
74
|
other_options = other_options.merge skip: (options[:page] - 1) * options[:limit]
|
80
75
|
end
|
76
|
+
cursor = database.collection(klass.to_s).find(criteria, other_options)
|
81
77
|
|
82
|
-
|
78
|
+
sort_cursor(cursor, options).map do |document|
|
83
79
|
document[:id] = document.delete("_id")
|
84
|
-
|
80
|
+
document
|
85
81
|
end
|
82
|
+
end
|
86
83
|
|
87
|
-
|
84
|
+
def sort_cursor cursor, options
|
85
|
+
return cursor unless options.has_key?(:attribute) &&
|
86
|
+
options.has_key?(:direction)
|
87
|
+
|
88
|
+
sort_field = options[:attribute]
|
89
|
+
sort_direction = options[:direction]
|
90
|
+
sort_criteria = [[sort_field, sort_direction]]
|
91
|
+
cursor.sort(sort_criteria)
|
88
92
|
end
|
89
93
|
|
90
94
|
def all klass
|
@@ -1,11 +1,10 @@
|
|
1
|
-
require 'perpetuity/mapper_registry'
|
2
|
-
|
3
1
|
module Perpetuity
|
4
2
|
class Serializer
|
5
|
-
attr_reader :mapper
|
3
|
+
attr_reader :mapper, :mapper_registry
|
6
4
|
|
7
|
-
def initialize(mapper)
|
5
|
+
def initialize(mapper, mapper_registry)
|
8
6
|
@mapper = mapper
|
7
|
+
@mapper_registry = mapper_registry
|
9
8
|
end
|
10
9
|
|
11
10
|
def attribute_for object, attribute_name
|
@@ -17,10 +16,10 @@ module Perpetuity
|
|
17
16
|
value = attribute_for object, attrib.name
|
18
17
|
|
19
18
|
serialized_value = if value.is_a? Array
|
20
|
-
serialize_array(value)
|
19
|
+
serialize_array(value, attrib.embedded?)
|
21
20
|
elsif mapper.data_source.can_serialize? value
|
22
21
|
value
|
23
|
-
elsif
|
22
|
+
elsif mapper_registry.has_mapper?(value.class)
|
24
23
|
serialize_with_foreign_mapper(value, attrib.embedded?)
|
25
24
|
else
|
26
25
|
if attrib.embedded?
|
@@ -36,7 +35,7 @@ module Perpetuity
|
|
36
35
|
|
37
36
|
def serialize_with_foreign_mapper value, embedded = false
|
38
37
|
if embedded
|
39
|
-
value_mapper =
|
38
|
+
value_mapper = mapper_registry[value.class]
|
40
39
|
value_serializer = Serializer.new(value_mapper)
|
41
40
|
attr = value_serializer.serialize(value)
|
42
41
|
attr.merge '__metadata__' => { 'class' => value.class }
|
@@ -50,18 +49,27 @@ module Perpetuity
|
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
53
|
-
def serialize_array enum
|
52
|
+
def serialize_array enum, embedded
|
54
53
|
enum.map do |value|
|
55
54
|
if value.is_a? Array
|
56
55
|
serialize_array(value)
|
57
56
|
elsif mapper.data_source.can_serialize? value
|
58
57
|
value
|
59
|
-
elsif
|
60
|
-
|
61
|
-
|
62
|
-
'
|
58
|
+
elsif mapper_registry.has_mapper?(value.class)
|
59
|
+
if embedded
|
60
|
+
{
|
61
|
+
'__metadata__' => {
|
62
|
+
'class' => value.class.to_s
|
63
|
+
}
|
64
|
+
}.merge mapper_registry[value.class].serialize(value)
|
65
|
+
else
|
66
|
+
{
|
67
|
+
'__metadata__' => {
|
68
|
+
'class' => value.class.to_s,
|
69
|
+
'id' => value.id
|
70
|
+
}
|
63
71
|
}
|
64
|
-
|
72
|
+
end
|
65
73
|
else
|
66
74
|
Marshal.dump(value)
|
67
75
|
end
|
data/lib/perpetuity/version.rb
CHANGED
@@ -2,14 +2,15 @@ require 'perpetuity/mapper_registry'
|
|
2
2
|
|
3
3
|
module Perpetuity
|
4
4
|
describe MapperRegistry do
|
5
|
-
|
6
|
-
let(:mapper) { Class.new }
|
5
|
+
let(:registry) { MapperRegistry.new }
|
6
|
+
let(:mapper) { Class.new { def initialize(map_reg); end } }
|
7
|
+
subject { registry }
|
7
8
|
|
8
|
-
before {
|
9
|
+
before { registry[Object] = mapper }
|
9
10
|
|
10
11
|
it { should have_mapper Object }
|
11
12
|
it 'maps classes to instances of their mappers' do
|
12
|
-
|
13
|
+
registry[Object].should be_a mapper
|
13
14
|
end
|
14
15
|
end
|
15
16
|
end
|
@@ -1,25 +1,32 @@
|
|
1
|
+
require 'perpetuity/mapper_registry'
|
1
2
|
require 'perpetuity/mapper'
|
2
3
|
|
3
4
|
module Perpetuity
|
4
5
|
describe Mapper do
|
6
|
+
let(:registry) { MapperRegistry.new}
|
5
7
|
let(:mapper_class) { Class.new(Mapper) }
|
6
|
-
let(:mapper) { mapper_class.new }
|
8
|
+
let(:mapper) { mapper_class.new(registry) }
|
7
9
|
subject { mapper }
|
8
10
|
|
9
11
|
it { should be_a Mapper }
|
10
12
|
|
11
13
|
it 'has correct attributes' do
|
12
|
-
|
14
|
+
mapper_class.attribute :name
|
15
|
+
mapper_class.attributes.should eq [:name]
|
13
16
|
end
|
14
17
|
|
15
18
|
it 'returns an empty attribute list when no attributes have been assigned' do
|
16
|
-
|
19
|
+
mapper_class.attributes.should be_empty
|
17
20
|
end
|
18
21
|
|
19
22
|
it 'can have embedded attributes' do
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
mapper_class.attribute :comments, embedded: true
|
24
|
+
mapper_class.attribute_set[:comments].should be_embedded
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'registers itself with the mapper registry' do
|
28
|
+
mapper_class.map Object, registry
|
29
|
+
registry[Object].should be_instance_of mapper_class
|
23
30
|
end
|
24
31
|
|
25
32
|
context 'with unserializable embedded attributes' do
|
@@ -32,7 +39,7 @@ module Perpetuity
|
|
32
39
|
object = Object.new
|
33
40
|
object.instance_variable_set '@sub_objects', [unserializable_object]
|
34
41
|
mapper_class.attribute :sub_objects, embedded: true
|
35
|
-
mapper_class.map Object
|
42
|
+
mapper_class.map Object, registry
|
36
43
|
data_source = double(:data_source)
|
37
44
|
mapper.stub(data_source: data_source)
|
38
45
|
data_source.should_receive(:can_serialize?).with(unserializable_object).and_return false
|
@@ -41,12 +48,66 @@ module Perpetuity
|
|
41
48
|
end
|
42
49
|
end
|
43
50
|
|
44
|
-
describe '
|
45
|
-
let
|
46
|
-
|
51
|
+
describe 'talking to the data source' do
|
52
|
+
let(:data_source) { double }
|
53
|
+
before do
|
54
|
+
mapper_class.stub(data_source: data_source)
|
55
|
+
mapper_class.map Object, registry
|
56
|
+
end
|
57
|
+
|
58
|
+
specify 'mappers use the data source that the mapper class uses' do
|
59
|
+
mapper.data_source.should be data_source
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'inserts objects into a data source' do
|
63
|
+
mapper_class.attribute :my_attribute
|
64
|
+
obj = Object.new
|
65
|
+
obj.instance_variable_set '@my_attribute', 'foo'
|
66
|
+
data_source.should_receive(:can_serialize?).with('foo') { true }
|
67
|
+
data_source.should_receive(:insert)
|
68
|
+
.with(Object,
|
69
|
+
{ 'my_attribute' => 'foo' })
|
70
|
+
.and_return('bar')
|
71
|
+
|
72
|
+
mapper.insert(obj).should be == 'bar'
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'counts objects of its mapped class in the data source' do
|
76
|
+
data_source.should_receive(:count).with(Object) { 4 }
|
77
|
+
mapper.count.should be == 4
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'finds an object by ID' do
|
81
|
+
returned_object = double
|
82
|
+
criteria = { id: 1 }
|
83
|
+
options = {:attribute=>nil, :direction=>nil, :limit=>nil, :page=>nil}
|
84
|
+
data_source.should_receive(:retrieve)
|
85
|
+
.with(Object, criteria, options) { [returned_object] }
|
86
|
+
|
87
|
+
mapper.find(1).should be == returned_object
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'saves an object' do
|
91
|
+
mapper_class.attribute :foo
|
92
|
+
object = Object.new
|
93
|
+
object.define_singleton_method(:id) { 1 }
|
94
|
+
object.instance_variable_set '@foo', 'bar'
|
95
|
+
data_source.should_receive(:can_serialize?).with('bar') { true }
|
96
|
+
data_source.should_receive(:update).with Object, 1, { 'foo' => 'bar' }
|
97
|
+
|
98
|
+
mapper.save object
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'deletes an object from the data source' do
|
102
|
+
object = Object.new
|
103
|
+
|
104
|
+
data_source.should_receive(:delete).with object, Object
|
105
|
+
mapper.delete object
|
106
|
+
end
|
47
107
|
|
48
|
-
it '
|
49
|
-
|
108
|
+
it 'deletes all objects it manages' do
|
109
|
+
data_source.should_receive(:delete_all).with(Object)
|
110
|
+
mapper.delete_all
|
50
111
|
end
|
51
112
|
end
|
52
113
|
end
|
@@ -1,6 +1,62 @@
|
|
1
1
|
require 'perpetuity/serializer'
|
2
|
+
require 'perpetuity/mapper'
|
3
|
+
require 'perpetuity/mapper_registry'
|
4
|
+
require 'support/test_classes/book'
|
5
|
+
require 'support/test_classes/user'
|
2
6
|
|
3
7
|
module Perpetuity
|
4
8
|
describe Serializer do
|
9
|
+
let(:dave) { User.new('Dave') }
|
10
|
+
let(:andy) { User.new('Andy') }
|
11
|
+
let(:authors) { [dave, andy] }
|
12
|
+
let(:book) { Book.new('The Pragmatic Programmer', authors) }
|
13
|
+
let(:mapper_registry) { MapperRegistry.new }
|
14
|
+
let!(:book_mapper) do
|
15
|
+
registry = mapper_registry
|
16
|
+
Class.new(Perpetuity::Mapper) do
|
17
|
+
map Book, registry
|
18
|
+
attribute :title
|
19
|
+
attribute :authors
|
20
|
+
end
|
21
|
+
end
|
22
|
+
let!(:user_mapper) do
|
23
|
+
registry = mapper_registry
|
24
|
+
Class.new(Perpetuity::Mapper) do
|
25
|
+
map User, registry
|
26
|
+
attribute :name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
let(:data_source) { double('Data Source') }
|
30
|
+
let(:serializer) { Serializer.new(mapper_registry[Book], mapper_registry) }
|
31
|
+
|
32
|
+
before do
|
33
|
+
dave.stub(id: 1)
|
34
|
+
andy.stub(id: 2)
|
35
|
+
user_mapper.stub(data_source: data_source)
|
36
|
+
book_mapper.stub(data_source: data_source)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'serializes an array of non-embedded attributes as references' do
|
40
|
+
data_source.should_receive(:can_serialize?).with(book.title).and_return true
|
41
|
+
data_source.should_receive(:can_serialize?).with(dave).and_return false
|
42
|
+
data_source.should_receive(:can_serialize?).with(andy).and_return false
|
43
|
+
serializer.serialize(book).should be == {
|
44
|
+
'title' => book.title,
|
45
|
+
'authors' => [
|
46
|
+
{
|
47
|
+
'__metadata__' => {
|
48
|
+
'class' => 'User',
|
49
|
+
'id' => dave.id
|
50
|
+
}
|
51
|
+
},
|
52
|
+
{
|
53
|
+
'__metadata__' => {
|
54
|
+
'class' => 'User',
|
55
|
+
'id' => andy.id
|
56
|
+
}
|
57
|
+
}
|
58
|
+
]
|
59
|
+
}
|
60
|
+
end
|
5
61
|
end
|
6
62
|
end
|
data/spec/perpetuity_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require 'perpetuity'
|
|
3
3
|
mongodb = Perpetuity::MongoDB.new db: 'perpetuity_gem_test'
|
4
4
|
Perpetuity.configure { data_source mongodb }
|
5
5
|
|
6
|
-
require 'test_classes'
|
6
|
+
require 'support/test_classes'
|
7
7
|
|
8
8
|
describe Perpetuity do
|
9
9
|
describe 'mapper generation' do
|
@@ -265,12 +265,43 @@ describe Perpetuity do
|
|
265
265
|
its(:id) { should be == user.id }
|
266
266
|
end
|
267
267
|
|
268
|
-
it 'can retrieve
|
268
|
+
it 'can retrieve a one-to-one association' do
|
269
269
|
retrieved_topic = topic_mapper.find(topic.id)
|
270
270
|
|
271
271
|
topic_mapper.load_association! retrieved_topic, :creator
|
272
272
|
retrieved_topic.creator.name.should eq 'Flump'
|
273
273
|
end
|
274
|
+
|
275
|
+
describe 'associations with many objects' do
|
276
|
+
let(:pragprogs) { [User.new('Dave'), User.new('Andy')] }
|
277
|
+
let(:cuke_authors) { [User.new('Matt'), User.new('Aslak')] }
|
278
|
+
let(:pragprog_book) { Book.new("PragProg #{Time.now.to_f}", pragprogs) }
|
279
|
+
let(:cuke_book) { Book.new("Cucumber Book #{Time.now.to_f}", cuke_authors) }
|
280
|
+
let(:book_mapper) { Perpetuity[Book] }
|
281
|
+
|
282
|
+
before do
|
283
|
+
pragprogs.each { |author| Perpetuity[User].insert author }
|
284
|
+
book_mapper.insert pragprog_book
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'can retrieve a one-to-many association' do
|
288
|
+
persisted_book = book_mapper.find(pragprog_book.id)
|
289
|
+
book_mapper.load_association! persisted_book, :authors
|
290
|
+
|
291
|
+
persisted_book.authors.first.name.should be == 'Dave'
|
292
|
+
persisted_book.authors.last.name.should be == 'Andy'
|
293
|
+
end
|
294
|
+
|
295
|
+
it 'can retrieve a many-to-many association' do
|
296
|
+
cuke_authors.each { |author| Perpetuity[User].insert author }
|
297
|
+
book_mapper.insert cuke_book
|
298
|
+
book_ids = [pragprog_book, cuke_book].map(&:id)
|
299
|
+
|
300
|
+
books = book_mapper.select { |book| book.id.in book_ids }.to_a
|
301
|
+
book_mapper.load_association! books, :authors
|
302
|
+
books.map(&:authors).flatten.map(&:name).should include *%w(Dave Andy Matt Aslak)
|
303
|
+
end
|
304
|
+
end
|
274
305
|
end
|
275
306
|
|
276
307
|
describe 'updating' do
|
@@ -1,32 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
def initialize name="Foo"
|
4
|
-
@name = name
|
5
|
-
end
|
1
|
+
%w( user article comment book message topic car ).each do |file|
|
2
|
+
require "support/test_classes/#{file}"
|
6
3
|
end
|
7
4
|
|
8
5
|
Perpetuity.generate_mapper_for User do
|
9
6
|
attribute :name
|
10
7
|
end
|
11
8
|
|
12
|
-
class Article
|
13
|
-
attr_accessor :title, :body, :author, :comments, :published_at, :views
|
14
|
-
def initialize title="Title", body="Body", author=nil, published_at=Time.now, views=0
|
15
|
-
@title = title
|
16
|
-
@body = body
|
17
|
-
@author = author
|
18
|
-
@comments = []
|
19
|
-
@published_at = published_at
|
20
|
-
@views = views
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
9
|
class ArticleMapper < Perpetuity::Mapper
|
25
10
|
map Article
|
26
11
|
attribute :title
|
27
12
|
attribute :body
|
28
13
|
attribute :author
|
29
|
-
attribute :comments
|
14
|
+
attribute :comments, embedded: true
|
30
15
|
attribute :published_at
|
31
16
|
attribute :views
|
32
17
|
|
@@ -41,62 +26,26 @@ class ArticleMapper < Perpetuity::Mapper
|
|
41
26
|
end
|
42
27
|
end
|
43
28
|
|
44
|
-
class Comment
|
45
|
-
attr_accessor :body, :author
|
46
|
-
def initialize body='Body', author=nil
|
47
|
-
@body = body
|
48
|
-
@author = author
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
29
|
Perpetuity.generate_mapper_for(Comment) do
|
53
30
|
attribute :body
|
54
31
|
attribute :author
|
55
32
|
end
|
56
33
|
|
57
|
-
class Book
|
58
|
-
attr_accessor :title
|
59
|
-
def initialize title="Foo Bar"
|
60
|
-
@title = title
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
34
|
Perpetuity.generate_mapper_for Book do
|
65
35
|
id { title.gsub(/\W+/, '-').downcase }
|
66
36
|
attribute :title
|
67
|
-
|
68
|
-
|
69
|
-
class Message
|
70
|
-
def initialize text="My Message!"
|
71
|
-
self.text = text
|
72
|
-
end
|
73
|
-
|
74
|
-
def text
|
75
|
-
@text.reverse
|
76
|
-
end
|
77
|
-
|
78
|
-
def text= new_text
|
79
|
-
@text = new_text.reverse
|
80
|
-
end
|
37
|
+
attribute :authors
|
81
38
|
end
|
82
39
|
|
83
40
|
Perpetuity.generate_mapper_for Message do
|
84
41
|
attribute :text
|
85
42
|
end
|
86
43
|
|
87
|
-
class Topic
|
88
|
-
attr_accessor :title, :creator
|
89
|
-
end
|
90
|
-
|
91
44
|
Perpetuity.generate_mapper_for(Topic) do
|
92
45
|
attribute :title
|
93
46
|
attribute :creator
|
94
47
|
end
|
95
48
|
|
96
|
-
class Car
|
97
|
-
attr_accessor :make, :model, :seats
|
98
|
-
end
|
99
|
-
|
100
49
|
Perpetuity.generate_mapper_for(Car) do
|
101
50
|
attribute :make
|
102
51
|
attribute :model
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Article
|
2
|
+
attr_accessor :title, :body, :author, :comments, :published_at, :views
|
3
|
+
def initialize title="Title", body="Body", author=nil, published_at=Time.now, views=0
|
4
|
+
@title = title
|
5
|
+
@body = body
|
6
|
+
@author = author
|
7
|
+
@comments = []
|
8
|
+
@published_at = published_at
|
9
|
+
@views = views
|
10
|
+
end
|
11
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perpetuity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -95,6 +95,7 @@ files:
|
|
95
95
|
- lib/perpetuity/attribute_set.rb
|
96
96
|
- lib/perpetuity/config.rb
|
97
97
|
- lib/perpetuity/data_injectable.rb
|
98
|
+
- lib/perpetuity/identity_map.rb
|
98
99
|
- lib/perpetuity/mapper.rb
|
99
100
|
- lib/perpetuity/mapper_registry.rb
|
100
101
|
- lib/perpetuity/mongodb.rb
|
@@ -133,7 +134,14 @@ files:
|
|
133
134
|
- spec/perpetuity/validations/presence_spec.rb
|
134
135
|
- spec/perpetuity/validations_spec.rb
|
135
136
|
- spec/perpetuity_spec.rb
|
136
|
-
- spec/test_classes.rb
|
137
|
+
- spec/support/test_classes.rb
|
138
|
+
- spec/support/test_classes/article.rb
|
139
|
+
- spec/support/test_classes/book.rb
|
140
|
+
- spec/support/test_classes/car.rb
|
141
|
+
- spec/support/test_classes/comment.rb
|
142
|
+
- spec/support/test_classes/message.rb
|
143
|
+
- spec/support/test_classes/topic.rb
|
144
|
+
- spec/support/test_classes/user.rb
|
137
145
|
homepage: https://github.com/jgaskins/perpetuity.git
|
138
146
|
licenses: []
|
139
147
|
post_install_message:
|
@@ -146,12 +154,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
146
154
|
- - ! '>='
|
147
155
|
- !ruby/object:Gem::Version
|
148
156
|
version: '0'
|
157
|
+
segments:
|
158
|
+
- 0
|
159
|
+
hash: -4673501112155482
|
149
160
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
161
|
none: false
|
151
162
|
requirements:
|
152
163
|
- - ! '>='
|
153
164
|
- !ruby/object:Gem::Version
|
154
165
|
version: '0'
|
166
|
+
segments:
|
167
|
+
- 0
|
168
|
+
hash: -4673501112155482
|
155
169
|
requirements: []
|
156
170
|
rubyforge_project:
|
157
171
|
rubygems_version: 1.8.24
|
@@ -179,4 +193,11 @@ test_files:
|
|
179
193
|
- spec/perpetuity/validations/presence_spec.rb
|
180
194
|
- spec/perpetuity/validations_spec.rb
|
181
195
|
- spec/perpetuity_spec.rb
|
182
|
-
- spec/test_classes.rb
|
196
|
+
- spec/support/test_classes.rb
|
197
|
+
- spec/support/test_classes/article.rb
|
198
|
+
- spec/support/test_classes/book.rb
|
199
|
+
- spec/support/test_classes/car.rb
|
200
|
+
- spec/support/test_classes/comment.rb
|
201
|
+
- spec/support/test_classes/message.rb
|
202
|
+
- spec/support/test_classes/topic.rb
|
203
|
+
- spec/support/test_classes/user.rb
|