praxis-mapper 3.4.0 → 4.0

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