perpetuity 1.0.0.beta3 → 1.0.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7cf95b4e9ae21a8017556d5928b72d0ce4a23e94
4
- data.tar.gz: 0ac11a4957b4b6a8b3f991b35b726b0f0da1533f
3
+ metadata.gz: c67c2d6a7eed0f92f1b9878069d05697405363da
4
+ data.tar.gz: 21cddf8c8f2440bea99722e28fefeca46f9ba4f6
5
5
  SHA512:
6
- metadata.gz: 07de997c8ddb238ae077bc8154a58e0d4f7a422c465bf4e98b303ecf6082701e5c1230cb70728b3e37d5124751c3b4ba90d55e44837d61407aac05a9eeb37bfd
7
- data.tar.gz: 627e785dbb378e4e00e5de44645635f33d600376f19ed562eb906b19e265919a3ea13aaf24221df7a02dfb1fce1617ab12a85c67ff82c7350ac46c2da8e7a825
6
+ metadata.gz: eab30eabb92736be84697d75277242d77014f2fd71cf629fcec51b064f8a6f50a87e11643793773d49a8ca513f44cc9c9ae989d0ba1c8cebf1d4f94a6eb4edb5
7
+ data.tar.gz: b463f4b0a47ae396095b7d700b28bf3a5474a227a4ccb7b96ffa54a0175d39944d83b33012d1e0cc372d10f2df02e1a3f1623ceaf7eb3c03a9a8f6bc0c3b4004
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## Version 1.0.0.beta5
2
+
3
+ - Don't stringify ids in `IdentityMap`. This should result in a little better performance. To be honest, I'm not even sure anymore why I put that in there.
4
+ - Use the IdentityMap when retrieving multiple objects by id.
5
+
6
+ ## Version 1.0.0.beta4
7
+
8
+ - Remove in-memory updating of objects using the `Mapper#update` method.
9
+ - Separate dirty tracking from `IdentityMap` and return to a real Identity Map.
10
+ - Pass IdentityMap around when loading associations
11
+ - Add `Perpetuity.register_adapter` method to allow various data-source adapters to register without Perpetuity knowing about them.
12
+
1
13
  ## Version 1.0.0.beta3
2
14
 
3
15
  - Fix title-case -> snake-case support to convert something like `UserRegistration` to `user_registration`. Previously, it would return `userregistration`.
data/README.md CHANGED
@@ -248,3 +248,11 @@ This will let Rails know how to talk to your models in the way that Perpetuity h
248
248
  ## Contributing
249
249
 
250
250
  There are plenty of opportunities to improve what's here and possibly some design decisions that need some more refinement. You can help. If you have ideas to build on this, send some love in the form of pull requests, issues or [tweets](http://twitter.com/jamie_gaskins) and I'll do what I can for them.
251
+
252
+ Please be sure that the tests run before submitting a pull request. Just run `rspec`.
253
+
254
+ The tests include integration with an adapter. By default, this is the MongoDB adapter, but you can change that to Postgres by setting the `PERPETUITY_ADAPTER` environment variable to `postgres`.
255
+
256
+ When testing with the MongoDB adapter, you'll need to have MongoDB running. On Mac OS X, you can install MongoDB via Homebrew and start it with `mongod`. No configuration is necessary.
257
+
258
+ When testing with the Postgres adapter, you'll need to have PostgreSQL running. On Mac OS X, you can install PostgreSQL via Homebrew and start it with `pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start`. No other configuration is necessary, as long as the user has rights to create a database. NOTE: The Postgres adapter is incomplete at this time, and the tests do not yet pass with this adapter.
data/lib/perpetuity.rb CHANGED
@@ -5,6 +5,7 @@ require "perpetuity/mapper_registry"
5
5
 
6
6
  module Perpetuity
7
7
  def self.configure &block
8
+ register_standard_adapters
8
9
  detect_rails
9
10
  configuration.instance_exec(&block)
10
11
  end
@@ -31,8 +32,28 @@ module Perpetuity
31
32
  configure { data_source *args }
32
33
  end
33
34
 
35
+ def self.register_adapter adapters
36
+ config_adapters = Perpetuity::Configuration.adapters
37
+ adapters.each do |adapter_name, adapter_class|
38
+ if config_adapters.has_key?(adapter_name) && config_adapters[adapter_name] != adapter_class
39
+ raise "That adapter name has already been registered for #{config_adapters[adapter_name]}"
40
+ else
41
+ config_adapters[adapter_name] = adapter_class
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
34
48
  # Necessary to be able to check whether Rails is loaded and initialized
35
49
  def self.detect_rails
36
50
  require File.expand_path('../perpetuity/rails.rb', __FILE__) if defined? Rails
37
51
  end
52
+
53
+ # Necessary until these adapters are updated to register themselves.
54
+ def self.register_standard_adapters
55
+ Perpetuity.register_adapter :mongodb => Perpetuity::MongoDB if defined?(Perpetuity::MongoDB)
56
+ Perpetuity.register_adapter :postgres => Perpetuity::Postgres if defined?(Perpetuity::Postgres)
57
+ Perpetuity.register_adapter :dynamodb => Perpetuity::DynamoDB if defined?(Perpetuity::DynamoDB)
58
+ end
38
59
  end
@@ -14,7 +14,7 @@ module Perpetuity
14
14
  adapter = args.shift
15
15
  db_name = args.shift
16
16
  options = args.shift || {}
17
- adapter_class = Perpetuity.const_get(adapter(adapter))
17
+ adapter_class = adapter(adapter)
18
18
 
19
19
  @db = adapter_class.new(options.merge(db: db_name))
20
20
  end
@@ -28,7 +28,7 @@ module Perpetuity
28
28
  options = args.shift || {}
29
29
 
30
30
  protocol = uri.scheme
31
- klass = Perpetuity.const_get(adapter(protocol))
31
+ klass = adapter(protocol)
32
32
  db_options = {
33
33
  db: uri.path[1..-1],
34
34
  username: uri.user,
@@ -44,11 +44,7 @@ module Perpetuity
44
44
  end
45
45
 
46
46
  def self.adapters
47
- @adapters ||= {
48
- dynamodb: 'DynamoDB',
49
- mongodb: 'MongoDB',
50
- postgres: 'Postgres',
51
- }
47
+ @adapters ||= {}
52
48
  end
53
49
 
54
50
  def adapter name
@@ -5,8 +5,8 @@ module Perpetuity
5
5
  class Dereferencer
6
6
  attr_reader :map, :mapper_registry
7
7
 
8
- def initialize mapper_registry
9
- @map = IdentityMap.new
8
+ def initialize mapper_registry, identity_map=IdentityMap.new
9
+ @map = identity_map
10
10
  @mapper_registry = mapper_registry
11
11
  end
12
12
 
@@ -37,13 +37,11 @@ module Perpetuity
37
37
  def objects klass, ids
38
38
  ids = ids.uniq
39
39
  if ids.one?
40
- mapper_registry[klass].find(ids.first)
40
+ mapper_registry.mapper_for(klass, identity_map: map).find(ids.first)
41
41
  elsif ids.none?
42
42
  []
43
43
  else
44
- mapper_registry[klass].select { |object|
45
- object.id.in ids.uniq
46
- }.to_a
44
+ mapper_registry[klass].find(ids.uniq).to_a
47
45
  end
48
46
  end
49
47
 
@@ -0,0 +1,19 @@
1
+ require 'perpetuity/duplicator'
2
+
3
+ module Perpetuity
4
+ class DirtyTracker
5
+ def initialize
6
+ @map = Hash.new { |hash, key| hash[key] = {} }
7
+ end
8
+
9
+ def [] klass, id
10
+ @map[klass][id.to_s]
11
+ end
12
+
13
+ def << object
14
+ klass = object.class
15
+ id = object.instance_variable_get(:@id)
16
+ @map[klass][id.to_s] = object.dup
17
+ end
18
+ end
19
+ end
@@ -1,19 +1,21 @@
1
1
  module Perpetuity
2
2
  class IdentityMap
3
- attr_reader :map
4
-
5
3
  def initialize
6
4
  @map = Hash.new { |hash, key| hash[key] = {} }
7
5
  end
8
6
 
9
7
  def [] klass, id
10
- map[klass][id.to_s]
8
+ @map[klass][id]
11
9
  end
12
10
 
13
11
  def << object
14
12
  klass = object.class
15
13
  id = object.instance_variable_get(:@id)
16
- map[klass][id.to_s] = object.dup
14
+ @map[klass][id] = object
15
+ end
16
+
17
+ def ids_for klass
18
+ @map[klass].keys
17
19
  end
18
20
  end
19
21
  end
@@ -3,16 +3,17 @@ require 'perpetuity/attribute'
3
3
  require 'perpetuity/data_injectable'
4
4
  require 'perpetuity/dereferencer'
5
5
  require 'perpetuity/retrieval'
6
- require 'perpetuity/duplicator'
6
+ require 'perpetuity/dirty_tracker'
7
7
 
8
8
  module Perpetuity
9
9
  class Mapper
10
10
  include DataInjectable
11
- attr_reader :mapper_registry, :identity_map
11
+ attr_reader :mapper_registry, :identity_map, :dirty_tracker
12
12
 
13
- def initialize registry=Perpetuity.mapper_registry
13
+ def initialize registry=Perpetuity.mapper_registry, id_map=IdentityMap.new
14
14
  @mapper_registry = registry
15
- @identity_map = IdentityMap.new
15
+ @identity_map = id_map
16
+ @dirty_tracker = DirtyTracker.new
16
17
  end
17
18
 
18
19
  def self.map klass, registry=Perpetuity.mapper_registry
@@ -128,17 +129,27 @@ module Perpetuity
128
129
 
129
130
  alias :find_all :select
130
131
 
131
- def find id=nil, cache_result=true, &block
132
+ def find id=nil, &block
132
133
  if block_given?
133
134
  select(&block).first
134
135
  else
135
136
  cached_value = identity_map[mapped_class, id]
136
137
  return cached_value if cached_value
137
138
 
138
- result = select { |object| object.id == id }.first
139
-
140
- if cache_result and !result.nil?
141
- identity_map << Duplicator.new(result).object
139
+ result = if id.is_a? Array
140
+ ids = id
141
+ ids_in_map = ids & identity_map.ids_for(mapped_class)
142
+ ids_to_select = ids - ids_in_map
143
+ retrieved = select { |object| object.id.in ids_to_select }.to_a
144
+ from_map = ids_in_map.map { |id| identity_map[mapped_class, id] }
145
+ retrieved + from_map
146
+ else
147
+ select { |object| object.id == id }.first
148
+ end
149
+
150
+ Array(result).each do |r|
151
+ identity_map << r
152
+ dirty_tracker << r
142
153
  end
143
154
 
144
155
  result
@@ -158,7 +169,7 @@ module Perpetuity
158
169
 
159
170
  def load_association! object, attribute
160
171
  objects = Array(object)
161
- dereferencer = Dereferencer.new(mapper_registry)
172
+ dereferencer = Dereferencer.new(mapper_registry, identity_map)
162
173
  dereferencer.load objects.map { |obj| obj.instance_variable_get("@#{attribute}") }
163
174
 
164
175
  objects.each do |obj|
@@ -185,10 +196,8 @@ module Perpetuity
185
196
  end
186
197
  end
187
198
 
188
- def update object, new_data, update_in_memory = true
199
+ def update object, new_data
189
200
  id = object.is_a?(mapped_class) ? id_for(object) : object
190
-
191
- inject_data object, new_data if update_in_memory
192
201
  data_source.update mapped_class, id, new_data
193
202
  end
194
203
 
@@ -197,7 +206,7 @@ module Perpetuity
197
206
  if changed_attributes && changed_attributes.any?
198
207
  update object, changed_attributes
199
208
  else
200
- update object, serialize(object), false
209
+ update object, serialize(object)
201
210
  end
202
211
  end
203
212
 
@@ -237,7 +246,7 @@ module Perpetuity
237
246
  end
238
247
 
239
248
  def serialize_changed_attributes object
240
- cached = identity_map[object.class, id_for(object)]
249
+ cached = dirty_tracker[object.class, id_for(object)]
241
250
  if cached
242
251
  data_source.serialize_changed_attributes(object, cached, self)
243
252
  end
@@ -11,15 +11,22 @@ module Perpetuity
11
11
  end
12
12
 
13
13
  def [] klass
14
- mapper_class = @mappers.fetch(klass) do
14
+ mapper_class(klass).new(self)
15
+ end
16
+
17
+ def mapper_for klass, options={}
18
+ identity_map = options[:identity_map]
19
+ mapper_class(klass).new(self, *Array(identity_map))
20
+ end
21
+
22
+ def mapper_class klass
23
+ @mappers.fetch(klass) do
15
24
  load_mappers
16
25
  unless @mappers.has_key? klass
17
26
  raise KeyError, "No mapper for #{klass}"
18
27
  end
19
28
  @mappers[klass]
20
29
  end
21
-
22
- mapper_class.new(self)
23
30
  end
24
31
 
25
32
  def []= klass, mapper
@@ -1,3 +1,3 @@
1
1
  module Perpetuity
2
- VERSION = "1.0.0.beta3"
2
+ VERSION = "1.0.0.beta5"
3
3
  end
@@ -15,11 +15,6 @@ describe 'updating' do
15
15
  mapper.find(mapper.id_for article).title.should eq new_title
16
16
  end
17
17
 
18
- it 'updates the object in memory' do
19
- mapper.update article, title: new_title
20
- article.title.should eq new_title
21
- end
22
-
23
18
  it 'resaves the object in the database' do
24
19
  article.title = new_title
25
20
  mapper.save article
@@ -16,6 +16,10 @@ module Perpetuity
16
16
  it 'loads objects based on the specified objects and attribute' do
17
17
  first.instance_variable_set :@id, 1
18
18
  mapper.should_receive(:find).with(1) { first }
19
+ id_map = IdentityMap.new
20
+ derefer.stub(map: id_map)
21
+ registry.stub(:mapper_for)
22
+ .with(Object, identity_map: id_map) { mapper }
19
23
 
20
24
  derefer.load first_ref
21
25
  id = derefer[first_ref].instance_variable_get(:@id)
@@ -31,9 +35,7 @@ module Perpetuity
31
35
 
32
36
  context 'with multiple references' do
33
37
  it 'returns the array of dereferenced objects' do
34
- first.instance_variable_set :@id, 1
35
- second.instance_variable_set :@id, 2
36
- mapper.should_receive(:select) { objects }
38
+ mapper.should_receive(:find).with([1, 2]) { objects }
37
39
  derefer.load([first_ref, second_ref]).should == objects
38
40
  end
39
41
  end
@@ -0,0 +1,49 @@
1
+ require 'perpetuity/dirty_tracker'
2
+
3
+ module Perpetuity
4
+ describe DirtyTracker do
5
+ let(:mapper) { double('ObjectMapper') }
6
+ let(:tracker) { DirtyTracker.new }
7
+
8
+ context 'when the object exists in the IdentityMap' do
9
+ let(:klass) do
10
+ Class.new do
11
+ attr_accessor :id, :name
12
+ def initialize id, name
13
+ @id = id
14
+ @name = name
15
+ end
16
+ end
17
+ end
18
+ let(:object) { klass.new(1, 'foo') }
19
+
20
+ before do
21
+ mapper.stub(:id_for).with(object) { object.id }
22
+ tracker << object
23
+ end
24
+
25
+ it 'returns the object with the given class and id' do
26
+ object.name = 'bar'
27
+ retrieved = tracker[klass, 1]
28
+
29
+ retrieved.id.should == 1
30
+ retrieved.name.should == 'foo'
31
+ end
32
+
33
+ specify 'the object returned is a duplicate' do
34
+ tracker[klass, 1].should_not be object
35
+ end
36
+
37
+ it 'stringifies keys when checking' do
38
+ retrieved = tracker[klass, '1']
39
+ retrieved.id.should == 1
40
+ end
41
+ end
42
+
43
+ context 'when the object does not exist in the IdentityMap' do
44
+ it 'returns nil' do
45
+ tracker[Object, 1].should be_nil
46
+ end
47
+ end
48
+ end
49
+ end
@@ -3,28 +3,29 @@ require 'perpetuity/identity_map'
3
3
  module Perpetuity
4
4
  describe IdentityMap do
5
5
  let(:id_map) { IdentityMap.new }
6
+ let(:klass) do
7
+ Class.new do
8
+ attr_reader :id
9
+ def initialize id
10
+ @id = id
11
+ end
12
+ end
13
+ end
14
+ let(:object) { klass.new(1) }
6
15
 
7
16
  context 'when the object exists in the IdentityMap' do
8
- let(:object) { Object.new }
9
-
10
17
  before do
11
- object.instance_variable_set :@id, 1
12
18
  id_map << object
13
19
  end
14
20
 
15
21
  it 'returns the object with the given class and id' do
16
- retrieved = id_map[Object, 1]
22
+ retrieved = id_map[klass, 1]
17
23
 
18
- retrieved.instance_variable_get(:@id).should == 1
24
+ retrieved.id.should == 1
19
25
  end
20
26
 
21
- specify 'the object returned is a duplicate, not the same object' do
22
- id_map[Object, 1].should_not be object
23
- end
24
-
25
- it 'stringifies keys when checking' do
26
- retrieved = id_map[Object, '1']
27
- retrieved.instance_variable_get(:@id).should == 1
27
+ specify 'the object returned is the same object' do
28
+ id_map[klass, 1].should be object
28
29
  end
29
30
  end
30
31
 
@@ -33,5 +34,10 @@ module Perpetuity
33
34
  id_map[Object, 1].should be_nil
34
35
  end
35
36
  end
37
+
38
+ it 'returns all of the ids it contains' do
39
+ id_map << object
40
+ id_map.ids_for(klass).should == [1]
41
+ end
36
42
  end
37
43
  end
@@ -1,9 +1,10 @@
1
1
  require 'perpetuity/mapper_registry'
2
+ require 'perpetuity/mapper'
2
3
 
3
4
  module Perpetuity
4
5
  describe MapperRegistry do
5
6
  let(:registry) { MapperRegistry.new }
6
- let(:mapper) { Class.new { def initialize(map_reg); end } }
7
+ let(:mapper) { Mapper }
7
8
  subject { registry }
8
9
 
9
10
  before { registry[Object] = mapper }
@@ -30,5 +31,11 @@ module Perpetuity
30
31
  registry.load_mappers
31
32
  end
32
33
  end
34
+
35
+ it 'returns a mapper initialized with the specified identity map' do
36
+ identity_map = double('IdentityMap')
37
+ mapper = registry.mapper_for(Object, identity_map: identity_map)
38
+ mapper.identity_map.should be identity_map
39
+ end
33
40
  end
34
41
  end
@@ -71,6 +71,17 @@ module Perpetuity
71
71
  mapper.find(1).should be == returned_object
72
72
  end
73
73
 
74
+ it 'finds multiple objects by ID' do
75
+ first, second = double, double
76
+ criteria = data_source.query { |o| o.id.in [1, 2] }
77
+ options.merge! limit: nil
78
+ data_source.should_receive(:retrieve)
79
+ .with(Object, criteria, options)
80
+ .and_return [first, second]
81
+
82
+ mapper.find([1, 2]).to_a.should be == [first, second]
83
+ end
84
+
74
85
  it 'finds multiple objects with a block' do
75
86
  criteria = data_source.query { |o| o.name == 'foo' }
76
87
  options = self.options.merge(limit: nil)
@@ -132,7 +143,7 @@ module Perpetuity
132
143
  object.instance_variable_set :@id, 1
133
144
  object.instance_variable_set :@modified, false
134
145
  object.instance_variable_set :@unmodified, false
135
- mapper.identity_map << object
146
+ mapper.dirty_tracker << object
136
147
 
137
148
  object.instance_variable_set :@modified, true
138
149
 
@@ -196,5 +207,14 @@ module Perpetuity
196
207
  end
197
208
  end
198
209
  end
210
+
211
+ describe 'using an existing identity map' do
212
+ it 'is initialized with an existing map' do
213
+ registry = Object.new
214
+ id_map = Object.new
215
+ mapper = Mapper.new(registry, id_map)
216
+ mapper.identity_map.should be id_map
217
+ end
218
+ end
199
219
  end
200
220
  end
@@ -49,4 +49,28 @@ describe Perpetuity do
49
49
  unpublished_ids.should include draft_id, not_yet_published_id
50
50
  end
51
51
  end
52
+
53
+ describe 'adapter registration' do
54
+ before do
55
+ class ExampleAdapter; end
56
+ end
57
+
58
+ it 'registers an adapter' do
59
+ Perpetuity.register_adapter :example => ExampleAdapter
60
+ Perpetuity::Configuration.adapters[:example].should == ExampleAdapter
61
+ end
62
+
63
+ it 'can re-register an adapter' do
64
+ Perpetuity.register_adapter :example => ExampleAdapter
65
+ Perpetuity.register_adapter :example => ExampleAdapter
66
+ Perpetuity::Configuration.adapters[:example].should == ExampleAdapter
67
+ end
68
+
69
+ it 'cannot re-register an adapter with a different class than originally registered' do
70
+ Perpetuity.register_adapter :example => ExampleAdapter
71
+ expect { Perpetuity.register_adapter :example => TrueClass }.to raise_exception
72
+ Perpetuity::Configuration.adapters[:example].should == ExampleAdapter
73
+ end
74
+ end
75
+
52
76
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perpetuity
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta3
4
+ version: 1.0.0.beta5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamie Gaskins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-03 00:00:00.000000000 Z
11
+ date: 2014-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -59,6 +59,7 @@ files:
59
59
  - lib/perpetuity/config.rb
60
60
  - lib/perpetuity/data_injectable.rb
61
61
  - lib/perpetuity/dereferencer.rb
62
+ - lib/perpetuity/dirty_tracker.rb
62
63
  - lib/perpetuity/duplicator.rb
63
64
  - lib/perpetuity/exceptions/duplicate_key_error.rb
64
65
  - lib/perpetuity/identity_map.rb
@@ -84,6 +85,7 @@ files:
84
85
  - spec/perpetuity/config_spec.rb
85
86
  - spec/perpetuity/data_injectable_spec.rb
86
87
  - spec/perpetuity/dereferencer_spec.rb
88
+ - spec/perpetuity/dirty_tracker_spec.rb
87
89
  - spec/perpetuity/duplicator_spec.rb
88
90
  - spec/perpetuity/identity_map_spec.rb
89
91
  - spec/perpetuity/mapper_registry_spec.rb
@@ -143,6 +145,7 @@ test_files:
143
145
  - spec/perpetuity/config_spec.rb
144
146
  - spec/perpetuity/data_injectable_spec.rb
145
147
  - spec/perpetuity/dereferencer_spec.rb
148
+ - spec/perpetuity/dirty_tracker_spec.rb
146
149
  - spec/perpetuity/duplicator_spec.rb
147
150
  - spec/perpetuity/identity_map_spec.rb
148
151
  - spec/perpetuity/mapper_registry_spec.rb