praxis-mapper 3.4.0 → 4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -192,7 +192,7 @@ module Praxis::Mapper
192
192
  end
193
193
 
194
194
  def to_records(rows)
195
- rows.collect do |row|
195
+ rows.collect do |row|
196
196
  m = model.new(row)
197
197
  m._query = self
198
198
  m
@@ -92,6 +92,17 @@ module Praxis::Mapper
92
92
  end
93
93
  end
94
94
 
95
+ def to_records(rows)
96
+ if model < ::Sequel::Model
97
+ rows.collect do |row|
98
+ m = model.call(row)
99
+ m._query = self
100
+ m
101
+ end
102
+ else
103
+ super
104
+ end
105
+ end
95
106
 
96
107
  end
97
108
 
@@ -0,0 +1,99 @@
1
+ require 'active_support/concern'
2
+
3
+ module Praxis::Mapper
4
+ module SequelCompat
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_accessor :_resource
9
+ attr_accessor :_query
10
+ attr_accessor :identity_map
11
+
12
+ @repository_name = :default
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ def identities
18
+ [primary_key]
19
+ end
20
+
21
+ def finalized?
22
+ true
23
+ end
24
+
25
+ def associations
26
+ orig = self.association_reflections.clone
27
+
28
+ orig.each do |k,v|
29
+ v[:model] = v.associated_class
30
+ if v.respond_to?(:primary_key)
31
+ v[:primary_key] = v.primary_key
32
+ else
33
+ # FIXME: figure out exactly what to do here.
34
+ # not super critical, as we can't track these associations
35
+ # directly, but it would be nice to traverse these
36
+ # properly.
37
+ v[:primary_key] = :unsupported
38
+ end
39
+ end
40
+ orig
41
+ end
42
+
43
+ def repository_name(name=nil)
44
+ return @repository_name if name.nil?
45
+
46
+ @repository_name = name
47
+ end
48
+
49
+ end
50
+
51
+
52
+ def _load_associated_objects(opts, dynamic_opts=OPTS)
53
+ return super if self.identity_map.nil?
54
+ target = opts.associated_class
55
+ key = opts[:key]
56
+
57
+ case opts[:type]
58
+ when :many_to_one
59
+ val = if key.kind_of?(Array)
60
+ @values.values_at(*key)
61
+ else
62
+ @values[key]
63
+ end
64
+ return nil if val.nil?
65
+ self.identity_map.get(target, target.primary_key => val)
66
+ when :one_to_many
67
+ self.identity_map.all(target, key => [pk] )
68
+ when :many_to_many
69
+ # OPTIMIZE: cache this result
70
+ join_model = opts[:join_model].constantize
71
+
72
+ left_key = opts[:left_key]
73
+ right_key = opts[:right_key]
74
+
75
+ right_values = self.identity_map.
76
+ all(join_model, left_key => Array(values[primary_key])).
77
+ collect(&right_key)
78
+
79
+ self.identity_map.all(target, target.primary_key => right_values )
80
+ else
81
+ raise "#{opts[:type]} is not currently supported"
82
+ end
83
+ end
84
+
85
+
86
+ def identities
87
+ self.class.identities.each_with_object(Hash.new) do |identity, hash|
88
+ case identity
89
+ when Symbol
90
+ hash[identity] = values[identity].freeze
91
+ else
92
+ hash[identity] = values.values_at(*identity).collect(&:freeze)
93
+ end
94
+ end
95
+ end
96
+
97
+
98
+ end
99
+ end
@@ -1,5 +1,5 @@
1
1
  module Praxis
2
2
  module Mapper
3
- VERSION = "3.4.0"
3
+ VERSION = "4.0"
4
4
  end
5
5
  end
@@ -34,4 +34,6 @@ Gem::Specification.new do |spec|
34
34
  spec.add_development_dependency(%q<pry-byebug>, ["~> 1"])
35
35
  spec.add_development_dependency(%q<pry-stack_explorer>, ["~> 0"])
36
36
  spec.add_development_dependency(%q<fuubar>, ["~> 1"])
37
+ spec.add_development_dependency('sqlite3')
38
+ spec.add_development_dependency('factory_girl')
37
39
  end
@@ -0,0 +1,32 @@
1
+ FactoryGirl.define do
2
+
3
+ to_create { |i| i.save }
4
+
5
+ factory :user, class: UserModel, aliases: [:author] do
6
+ name { /[:name:]/.gen }
7
+ email { /[:email:]/.gen }
8
+ end
9
+
10
+ factory :post, class: PostModel do
11
+ title { /\w+/.gen }
12
+ body { /\w+/.gen }
13
+ author
14
+ end
15
+
16
+ factory :comment, class: CommentModel do
17
+ author
18
+ post
19
+ end
20
+
21
+ factory :composite, class: CompositeIdSequelModel do
22
+ id { /\w+/.gen }
23
+ type { /\w+/.gen }
24
+
25
+ name { /\w+/.gen }
26
+ end
27
+
28
+ factory :other, class: OtherSequelModel do
29
+ composite
30
+ end
31
+
32
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ describe Praxis::Mapper::ConnectionFactories::Sequel do
4
+
5
+ let(:database) { Sequel.mock}
6
+ let(:thread) { Thread.current }
7
+ let(:connection_manager) { double('Praxis::Mapper::ConnectionManager', thread: thread) }
8
+
9
+ subject(:factory) { described_class.new(connection:database) }
10
+
11
+ context 'checkout' do
12
+ it 'returns the connection' do
13
+ factory.checkout(connection_manager).should be database
14
+ end
15
+
16
+ it 'allocates a connection to the thread of the manager' do
17
+ database.pool.allocated.should_not have_key(thread)
18
+ factory.checkout(connection_manager)
19
+ database.pool.allocated.should have_key(thread)
20
+ end
21
+
22
+ it 'does not acquire a new connection if the thread already has one' do
23
+ connection = double('connection')
24
+ database.pool.allocated[thread] = connection
25
+
26
+ factory.checkout(connection_manager)
27
+ database.pool.allocated[thread].should be connection
28
+ end
29
+
30
+ end
31
+
32
+ context 'release' do
33
+ before do
34
+ factory.checkout(connection_manager)
35
+ end
36
+
37
+ it 'releases the connection' do
38
+ database.pool.allocated.should have_key(thread)
39
+ factory.release(connection_manager, database)
40
+ database.pool.allocated.should_not have_key(thread)
41
+ end
42
+ end
43
+
44
+ context 'across multiple threads' do
45
+ before do
46
+ factory.checkout(connection_manager)
47
+ end
48
+
49
+ it 'acquires a connection in a separate thread' do
50
+ database.pool.allocated.should have_key(thread)
51
+
52
+ thread_2 = Thread.new do
53
+ connection_manager_2 = Praxis::Mapper::ConnectionManager.new
54
+ database.pool.allocated.should_not have_key(Thread.current)
55
+ database.pool.allocated.should have_key(thread)
56
+
57
+ factory.checkout(connection_manager_2)
58
+ database.pool.allocated.should have_key(Thread.current)
59
+ database.pool.allocated.should have_key(thread)
60
+ end
61
+
62
+ thread_2.join
63
+ end
64
+ end
65
+
66
+
67
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Praxis::Mapper::ConnectionFactories::Simple do
4
+
5
+ let(:connection) { double("connection") }
6
+
7
+ let(:connection_manager) { double('Praxis::Mapper::ConnectionManager') }
8
+
9
+ context 'with a raw connection' do
10
+ subject(:factory) { described_class.new(connection:connection) }
11
+
12
+ it 'returns the connection on checkout' do
13
+ factory.checkout(connection_manager).should be connection
14
+ end
15
+
16
+ end
17
+
18
+ context 'with a proc' do
19
+ let(:block) { Proc.new { connection } }
20
+
21
+ subject(:factory) { described_class.new(&block) }
22
+
23
+ it 'calls the block on checkout' do
24
+ block.should_receive(:call).and_call_original
25
+ factory.checkout(connection_manager).should be connection
26
+ end
27
+ end
28
+
29
+ end
@@ -1,117 +1,95 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
2
 
3
3
  class DummyFactory
4
+ attr_reader :opts
4
5
  def initialize(opts)
5
6
  @opts = opts
6
7
  end
7
8
 
8
- def opts
9
- @opts
9
+ def checkout(connection_manager)
10
10
  end
11
11
 
12
- def checkout
13
- end
14
-
15
- def release(connection)
12
+ def release(connection_manager, connection)
16
13
  end
17
14
 
18
15
  end
19
16
 
20
-
21
17
  describe Praxis::Mapper::ConnectionManager do
22
18
  let(:mock_connection) { double("connection") }
23
19
 
24
20
  let(:default_hash) { Hash.new }
25
21
  let(:factory_opts) { {:foo => "bar"} }
26
- let(:config) { {:dummy => {:connection_factory => "DummyFactory", :connection_opts => factory_opts}} }
27
22
 
23
+ let(:dummy_connection) { double("dummy connection")}
28
24
  let(:dummy_factory_mock) { double("dummy_factory")}
29
25
 
30
- subject { Praxis::Mapper::ConnectionManager }
31
-
26
+ subject(:connection_manager) { Praxis::Mapper::ConnectionManager }
27
+
32
28
  before do
33
- Praxis::Mapper::ConnectionManager.setup(config)
29
+ opts = factory_opts
30
+ block = Proc.new { mock_connection }
31
+
32
+ Praxis::Mapper::ConnectionManager.setup do
33
+ repository :foo, &block
34
+ repository :bar, :factory => "DummyFactory", :opts => opts
35
+ end
34
36
  end
35
37
 
36
- it 'has a :dummy repository' do
37
- repository = subject.repository(:dummy)
38
+ it 'supports proc-based repostories' do
39
+ subject.repository(:foo)[:factory].should be_kind_of(Praxis::Mapper::ConnectionFactories::Simple)
40
+ end
38
41
 
39
- repository[:connection_factory].should be_kind_of(DummyFactory)
40
- repository[:connection_factory].opts.should == factory_opts
42
+ it 'supports config-based repositories' do
43
+ subject.repository(:bar)[:factory].should be_kind_of(DummyFactory)
44
+ subject.repository(:bar)[:factory].opts.should == factory_opts
41
45
  end
42
46
 
43
- context "with repositories specified in a block for setup" do
44
- let(:dummy_connection) { double("dummy connection")}
45
-
46
- before do
47
- opts = factory_opts
48
-
49
- block = Proc.new { mock_connection }
50
- Praxis::Mapper::ConnectionManager.setup do
51
- repository :foo, &block
52
- repository :bar, :connection_factory => "DummyFactory", :connection_opts => opts
53
- end
54
- end
47
+ context 'getting connections' do
55
48
 
56
- it 'supports proc-based repostories' do
57
- subject.repository(:foo)[:connection_factory].should be_kind_of(Proc)
58
- end
49
+ subject { Praxis::Mapper::ConnectionManager.new }
59
50
 
60
- it 'supports config-based repositories' do
61
- subject.repository(:bar)[:connection_factory].should be_kind_of(DummyFactory)
62
- subject.repository(:bar)[:connection_factory].opts.should == factory_opts
51
+ it 'gets connections from proc-based repositories' do
52
+ subject.checkout(:foo).should == mock_connection
63
53
  end
64
54
 
65
- context 'getting connections' do
66
-
67
- subject { Praxis::Mapper::ConnectionManager.new }
68
-
69
- it 'gets connections from proc-based repositories' do
70
- subject.checkout(:foo).should == mock_connection
71
- end
55
+ it 'gets connections from config-based repositories' do
56
+ DummyFactory.any_instance.should_receive(:checkout).with(subject).and_return(dummy_connection)
57
+ subject.checkout(:bar).should == dummy_connection
58
+ end
72
59
 
73
- it 'gets connections from config-based repositories' do
74
- DummyFactory.any_instance.should_receive(:checkout).and_return(dummy_connection)
75
- subject.checkout(:bar).should == dummy_connection
76
- end
60
+ end
77
61
 
78
- end
62
+ context 'releasing connections' do
63
+ subject { Praxis::Mapper::ConnectionManager.new }
79
64
 
80
- context 'releasing connections' do
81
- subject { Praxis::Mapper::ConnectionManager.new }
65
+ it 'releases connections from config-based repositories' do
66
+ DummyFactory.any_instance.should_receive(:checkout).with(subject).exactly(2).times.and_return(dummy_connection)
67
+ DummyFactory.any_instance.should_receive(:release).with(subject, dummy_connection).and_return(true)
82
68
 
83
- it 'releases connections from config-based repositories' do
69
+ subject.checkout(:bar)
70
+ subject.checkout(:bar)
84
71
 
85
- DummyFactory.any_instance.should_receive(:checkout).exactly(2).times.and_return(dummy_connection)
86
- DummyFactory.any_instance.should_receive(:release).with(dummy_connection).and_return(true)
72
+ subject.release(:bar)
87
73
 
88
- subject.checkout(:bar)
89
- subject.checkout(:bar)
90
-
91
- subject.release(:bar)
92
-
93
- subject.checkout(:bar)
94
- end
74
+ subject.checkout(:bar)
75
+ end
95
76
 
96
- it 'releases connections from proc-based repositories' do
97
- subject.checkout(:foo)
98
- subject.release(:foo)
99
- end
77
+ it 'releases connections from proc-based repositories' do
78
+ subject.checkout(:foo)
79
+ subject.release(:foo)
80
+ end
100
81
 
101
- it 'releases all connections' do
102
- DummyFactory.any_instance.should_receive(:checkout).exactly(1).times.and_return(dummy_connection)
103
- DummyFactory.any_instance.should_receive(:release).with(dummy_connection).and_return(true)
82
+ it 'releases all connections' do
83
+ DummyFactory.any_instance.should_receive(:checkout).with(subject).exactly(1).times.and_return(dummy_connection)
84
+ DummyFactory.any_instance.should_receive(:release).with(subject, dummy_connection).and_return(true)
104
85
 
105
- subject.checkout(:bar)
86
+ subject.checkout(:bar)
106
87
 
107
- subject.should_not_receive(:release_one).with(:foo).and_call_original
108
- subject.should_receive(:release_one).with(:bar).and_call_original
109
-
110
- subject.release
111
- end
88
+ subject.should_not_receive(:release_one).with(:foo).and_call_original
89
+ subject.should_receive(:release_one).with(:bar).and_call_original
112
90
 
91
+ subject.release
113
92
  end
114
93
 
115
-
116
94
  end
117
95
  end
@@ -0,0 +1,122 @@
1
+ require 'spec_helper'
2
+
3
+ describe Praxis::Mapper::IdentityMapExtensions::Persistence do
4
+
5
+ let(:user) { UserModel.create }
6
+ let(:post) { PostModel.create(title: 'title', author: user) }
7
+
8
+ before do
9
+ identity_map.add_record(post)
10
+ identity_map.add_record(user)
11
+ identity_map.reindex!(PostModel, :author_id)
12
+ end
13
+
14
+ subject(:identity_map) { Praxis::Mapper::IdentityMap.new }
15
+
16
+ context '#attach(record)' do
17
+ context 'for a record that is missing an identity' do
18
+ let(:post) { PostModel.new(title: 'title') }
19
+
20
+ it 'saves the record before adding it ' do
21
+ post.should_receive(:save).and_call_original
22
+ identity_map.should_receive(:add_record).with(post).and_call_original
23
+
24
+ identity_map.attach(post)
25
+
26
+ identity_map.get(PostModel, id: post.id).should be post
27
+ end
28
+ end
29
+
30
+ context 'for a record that is not missing an identity' do
31
+ let(:post) { PostModel.create(title: 'title') }
32
+
33
+ it 'does not save the record before adding it' do
34
+ post.should_not_receive(:save).and_call_original
35
+ identity_map.should_receive(:add_record).with(post).and_call_original
36
+
37
+ identity_map.attach(post)
38
+
39
+ identity_map.get(PostModel, id: post.id).should be post
40
+ end
41
+ end
42
+ end
43
+
44
+ context '#flush!(object=nil)' do
45
+
46
+ context 'with a single record' do
47
+ it 'saves the changes' do
48
+ post.title = 'something else'
49
+
50
+ identity_map.flush!(post)
51
+ post.modified?.should be false
52
+ end
53
+ end
54
+
55
+ context 'for an entire model class' do
56
+ it 'flushes every changed record' do
57
+ post.title = 'something else'
58
+
59
+ identity_map.flush!(PostModel)
60
+ post.modified?.should be false
61
+ end
62
+ end
63
+
64
+ context 'with no object' do
65
+ it 'flushes every class' do
66
+ identity_map.should_receive(:flush!).with(no_args).and_call_original
67
+ identity_map.should_receive(:flush!).with(PostModel)
68
+ identity_map.should_receive(:flush!).with(UserModel)
69
+
70
+
71
+ identity_map.flush!
72
+ end
73
+ end
74
+ end
75
+
76
+ context '#remove(record)' do
77
+ it 'detaches and deletes the record' do
78
+ identity_map.should_receive(:detach).with(post).and_call_original
79
+ post.should_receive(:delete)
80
+
81
+ identity_map.remove(post)
82
+ end
83
+ end
84
+
85
+ context '#detach(record)' do
86
+
87
+ it 'unsets record identity_map and deindexes the record' do
88
+ identity_map.should_receive(:deindex).with(post)
89
+ identity_map.detach(post)
90
+ post.identity_map.should be nil
91
+ end
92
+ end
93
+
94
+ context '#deindex(record)' do
95
+ let!(:original_id) { post.id }
96
+
97
+ before do
98
+ post.id = 100
99
+
100
+ # build secondary index and ensure it's populated correctly
101
+ identity_map.all(PostModel, title: [post.title]).should include post
102
+
103
+ identity_map.deindex(post)
104
+ end
105
+
106
+ it 'cleans up all-rows index' do
107
+ identity_map.all(PostModel).should be_empty
108
+ end
109
+
110
+ it 'cleans up identity indexes' do
111
+ identity_map.all(PostModel, id: [original_id]).should be_empty
112
+ identity_map.all(PostModel, id: [post.id]).should be_empty
113
+ end
114
+
115
+ it 'cleans up secondary indexes' do
116
+ identity_map.all(PostModel, title: [post.title]).should_not include post
117
+ end
118
+
119
+ end
120
+
121
+
122
+ end