perpetuity 0.4.2 → 0.4.3
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/.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
|