rom-session 0.1.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.
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +22 -0
- data/Gemfile +30 -0
- data/Gemfile.devtools +55 -0
- data/Guardfile +19 -0
- data/LICENSE +20 -0
- data/README.md +21 -0
- data/Rakefile +4 -0
- data/config/devtools.yml +2 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +96 -0
- data/config/rubocop.yml +45 -0
- data/lib/rom-session.rb +43 -0
- data/lib/rom/session.rb +62 -0
- data/lib/rom/session/environment.rb +66 -0
- data/lib/rom/session/identity_map.rb +43 -0
- data/lib/rom/session/mapper.rb +43 -0
- data/lib/rom/session/relation.rb +126 -0
- data/lib/rom/session/state.rb +50 -0
- data/lib/rom/session/state/created.rb +22 -0
- data/lib/rom/session/state/deleted.rb +25 -0
- data/lib/rom/session/state/persisted.rb +29 -0
- data/lib/rom/session/state/transient.rb +20 -0
- data/lib/rom/session/state/updated.rb +29 -0
- data/lib/rom/session/tracker.rb +69 -0
- data/lib/rom/session/version.rb +9 -0
- data/lib/rom/support/proxy.rb +50 -0
- data/rom-session.gemspec +24 -0
- data/spec/integration/session_spec.rb +71 -0
- data/spec/rcov.opts +7 -0
- data/spec/shared/unit/environment_context.rb +11 -0
- data/spec/shared/unit/relation_context.rb +18 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/unit/rom/session/class_methods/start_spec.rb +23 -0
- data/spec/unit/rom/session/clean_predicate_spec.rb +21 -0
- data/spec/unit/rom/session/environment/element_reader_spec.rb +13 -0
- data/spec/unit/rom/session/flush_spec.rb +58 -0
- data/spec/unit/rom/session/mapper/load_spec.rb +44 -0
- data/spec/unit/rom/session/relation/delete_spec.rb +28 -0
- data/spec/unit/rom/session/relation/dirty_predicate_spec.rb +38 -0
- data/spec/unit/rom/session/relation/identity_spec.rb +11 -0
- data/spec/unit/rom/session/relation/new_spec.rb +50 -0
- data/spec/unit/rom/session/relation/save_spec.rb +50 -0
- data/spec/unit/rom/session/relation/state_spec.rb +26 -0
- data/spec/unit/rom/session/relation/track_spec.rb +23 -0
- data/spec/unit/rom/session/relation/tracking_predicate_spec.rb +23 -0
- data/spec/unit/rom/session/state_spec.rb +79 -0
- metadata +167 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Session
|
5
|
+
|
6
|
+
# @api private
|
7
|
+
class Tracker
|
8
|
+
attr_reader :objects, :changelog
|
9
|
+
private :objects, :changelog
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
def initialize
|
13
|
+
@objects = {}
|
14
|
+
@changelog = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
def commit
|
19
|
+
@changelog.each { |state| update(state.commit) }
|
20
|
+
@changelog = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
def fetch(object)
|
25
|
+
@objects.fetch(object.__id__) { raise ObjectNotTrackedError, object }
|
26
|
+
end
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def include?(object)
|
30
|
+
@objects.key?(object.__id__)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
def clean?
|
35
|
+
changelog.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
# @api private
|
39
|
+
def queue(state)
|
40
|
+
@changelog << state
|
41
|
+
update(state)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def update(state)
|
46
|
+
store(state.object, state)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @api private
|
50
|
+
def store_transient(object, mapper)
|
51
|
+
store(object, State::Transient.new(object, mapper))
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
def store_persisted(object, mapper)
|
56
|
+
store(object, State::Persisted.new(object, mapper))
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# @api private
|
62
|
+
def store(object, state)
|
63
|
+
@objects[object.__id__] = state
|
64
|
+
end
|
65
|
+
|
66
|
+
end # Tracker
|
67
|
+
|
68
|
+
end # Session
|
69
|
+
end # ROM
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
|
5
|
+
module Proxy
|
6
|
+
|
7
|
+
def self.included(descendant)
|
8
|
+
descendant.send :undef_method, *descendant.superclass.public_instance_methods(false).map(&:to_s)
|
9
|
+
descendant.extend(Constructor)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
module Constructor
|
14
|
+
|
15
|
+
def new(*args)
|
16
|
+
proxy = super(*args)
|
17
|
+
decorated_object = args.first
|
18
|
+
proxy.instance_variable_set '@__decorated_class', decorated_object.class
|
19
|
+
proxy.instance_variable_set '@__decorated_object', decorated_object
|
20
|
+
proxy.instance_variable_set '@__args', args[1..args.size]
|
21
|
+
proxy
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def method_missing(method, *args, &block)
|
29
|
+
forwardable?(method) ? forward(method, *args, &block) : super
|
30
|
+
end
|
31
|
+
|
32
|
+
def forwardable?(method)
|
33
|
+
@__decorated_object.respond_to?(method)
|
34
|
+
end
|
35
|
+
|
36
|
+
def forward(*args, &block)
|
37
|
+
response = @__decorated_object.public_send(*args, &block)
|
38
|
+
|
39
|
+
if response.equal?(@__decorated_object)
|
40
|
+
self
|
41
|
+
elsif response.kind_of?(@__decorated_class)
|
42
|
+
self.class.new(response, *@__args)
|
43
|
+
else
|
44
|
+
response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end # Proxy
|
49
|
+
|
50
|
+
end # ROM
|
data/rom-session.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.expand_path('../lib/rom/session/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = 'rom-session'
|
7
|
+
gem.description = 'Session for ROM'
|
8
|
+
gem.summary = gem.description
|
9
|
+
gem.authors = ['Markus Schirp', 'Piotr Solnica']
|
10
|
+
gem.email = ['mbj@schirp-dso.com', 'piotr.solnica@gmail.com']
|
11
|
+
gem.version = ROM::Session::VERSION.dup
|
12
|
+
gem.homepage = 'http://rom-rb.org'
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split("\n")
|
15
|
+
gem.test_files = `git ls-files -- {spec,features}/*`.split("\n")
|
16
|
+
gem.require_paths = %w(lib)
|
17
|
+
gem.extra_rdoc_files = %w(README.md LICENSE)
|
18
|
+
gem.license = 'MIT'
|
19
|
+
|
20
|
+
gem.add_dependency('adamantium', '~> 0.1')
|
21
|
+
gem.add_dependency('equalizer', '~> 0.0.7')
|
22
|
+
gem.add_dependency('abstract_type', '~> 0.0.6')
|
23
|
+
gem.add_dependency('concord', '~> 0.1.4')
|
24
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe 'Session' do
|
6
|
+
let(:users) { TEST_ENV.schema[:users] }
|
7
|
+
let(:relation) { TEST_ENV[:users] }
|
8
|
+
let(:mapper) { relation.mapper }
|
9
|
+
let(:model) { mapper.loader.model }
|
10
|
+
|
11
|
+
before do
|
12
|
+
users.insert([[1, 'John'], [2, 'Jane']])
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
users.delete([[1, 'John'], [2, 'Jane'], [3, 'Piotr']])
|
17
|
+
end
|
18
|
+
|
19
|
+
specify 'fetching an object from a relation' do
|
20
|
+
Session.start(users: relation) do |session|
|
21
|
+
# fetch user for the first time
|
22
|
+
jane1 = session[:users].restrict(name: 'Jane').one
|
23
|
+
|
24
|
+
expect(jane1).to eq(model.new(id: 2, name: 'Jane'))
|
25
|
+
|
26
|
+
# here IM-powered loader kicks in
|
27
|
+
jane2 = session[:users].restrict(name: 'Jane').one
|
28
|
+
|
29
|
+
expect(jane1).to be(jane2)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
specify 'deleting an object from a relation' do
|
34
|
+
Session.start(users: relation) do |session|
|
35
|
+
jane = session[:users].restrict(name: 'Jane').one
|
36
|
+
|
37
|
+
session[:users].delete(jane)
|
38
|
+
|
39
|
+
session.flush
|
40
|
+
|
41
|
+
expect(relation.to_a).not_to include(jane)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
specify 'saving an object to a relation' do
|
46
|
+
Session.start(users: relation) do |session|
|
47
|
+
piotr = session[:users].new(id: 3, name: 'Piotr')
|
48
|
+
|
49
|
+
session[:users].save(piotr)
|
50
|
+
|
51
|
+
session.flush
|
52
|
+
|
53
|
+
expect(relation.to_a).to include(piotr)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
specify 'updating an object in a relation' do
|
58
|
+
Session.start(users: relation) do |session|
|
59
|
+
jane = session[:users].restrict(id: 2).one
|
60
|
+
jane.name = 'Jane Doe'
|
61
|
+
|
62
|
+
session[:users].save(jane)
|
63
|
+
|
64
|
+
session.flush
|
65
|
+
|
66
|
+
expect(relation.count).to be(2)
|
67
|
+
|
68
|
+
expect(relation.to_a.last).to eql(jane)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/spec/rcov.opts
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_context 'Session::Environment' do
|
4
|
+
let(:object) { described_class.new({ users: users }, Session::Tracker.new) }
|
5
|
+
|
6
|
+
let(:users) {
|
7
|
+
relation = TEST_ENV.repository(:test)[:users]
|
8
|
+
mapper = Mapper.build(relation.header, mock_model(:id, :name))
|
9
|
+
Relation.new(relation, mapper)
|
10
|
+
}
|
11
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_context 'Session::Relation' do
|
4
|
+
let(:users) { session[:users] }
|
5
|
+
let(:object) { users }
|
6
|
+
|
7
|
+
let(:session) { Session.new(env) }
|
8
|
+
let(:env) { Session::Environment.new({ users: relation }, tracker) }
|
9
|
+
let(:tracker) { Session::Tracker.new }
|
10
|
+
|
11
|
+
let(:mapper) { Mapper.build([[:id, Integer], [:name, String]], model, keys: [:id]) }
|
12
|
+
let(:model) { mock_model(:id, :name) }
|
13
|
+
let(:header) { TEST_ENV.schema[:users].header }
|
14
|
+
let(:axiom) { Axiom::Relation::Variable.new(Axiom::Relation.new(header, [[1, 'John'], [2, 'Jane']])) }
|
15
|
+
let(:relation) { Relation.new(axiom, mapper) }
|
16
|
+
|
17
|
+
let(:user) { session[:users].to_a.first }
|
18
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
if ENV['COVERAGE'] == 'true'
|
4
|
+
require 'simplecov'
|
5
|
+
require 'coveralls'
|
6
|
+
|
7
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
8
|
+
SimpleCov::Formatter::HTMLFormatter,
|
9
|
+
Coveralls::SimpleCov::Formatter
|
10
|
+
]
|
11
|
+
|
12
|
+
SimpleCov.start do
|
13
|
+
command_name 'spec:unit'
|
14
|
+
|
15
|
+
add_filter 'config'
|
16
|
+
add_filter 'lib/rom/support'
|
17
|
+
add_filter 'spec'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class BasicObject
|
22
|
+
def self.freeze
|
23
|
+
# FIXME: remove this when axiom no longer freezes classes
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'devtools/spec_helper'
|
28
|
+
|
29
|
+
require 'rom-session'
|
30
|
+
|
31
|
+
require 'axiom'
|
32
|
+
require 'rom-relation'
|
33
|
+
require 'rom-mapper'
|
34
|
+
require 'rom/support/axiom/adapter/memory'
|
35
|
+
|
36
|
+
require 'bogus/rspec'
|
37
|
+
|
38
|
+
include ROM
|
39
|
+
|
40
|
+
def mock_model(*attributes)
|
41
|
+
Class.new {
|
42
|
+
include Equalizer.new(*attributes)
|
43
|
+
|
44
|
+
attributes.each { |attribute| attr_accessor attribute }
|
45
|
+
|
46
|
+
def initialize(attrs = {})
|
47
|
+
attrs.each { |name, value| send("#{name}=", value) }
|
48
|
+
end
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
TEST_ENV = Environment.setup(test: 'memory://test')
|
53
|
+
|
54
|
+
TEST_ENV.schema do
|
55
|
+
base_relation :users do
|
56
|
+
repository :test
|
57
|
+
|
58
|
+
attribute :id, Integer
|
59
|
+
attribute :name, String
|
60
|
+
|
61
|
+
key :id
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
TEST_ENV.mapping do
|
66
|
+
users do
|
67
|
+
model mock_model(:id, :name)
|
68
|
+
map :id, :name
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Session, '.start' do
|
6
|
+
include_context 'Session::Relation'
|
7
|
+
|
8
|
+
let(:user) { model.new(id: 3, name: 'Piotr') }
|
9
|
+
|
10
|
+
it 'starts a new session' do
|
11
|
+
Session.start(users: relation) do |session|
|
12
|
+
expect(session).to be_clean
|
13
|
+
expect(session).to be_instance_of(Session)
|
14
|
+
expect(session[:users]).to be_instance_of(Session::Relation)
|
15
|
+
|
16
|
+
session[:users].track(user).save(user)
|
17
|
+
|
18
|
+
session.flush
|
19
|
+
end
|
20
|
+
|
21
|
+
expect(relation.to_a).to include(user)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Session, '#clean?' do
|
6
|
+
subject { session.clean? }
|
7
|
+
|
8
|
+
include_context 'Session::Relation'
|
9
|
+
|
10
|
+
context 'when tracker has no pending state changes' do
|
11
|
+
it { should be_true }
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'when tracker has pending state changes' do
|
15
|
+
before do
|
16
|
+
session[:users].delete(user)
|
17
|
+
end
|
18
|
+
|
19
|
+
it { should be_false }
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Session::Environment, '#[]' do
|
6
|
+
subject { object[:users] }
|
7
|
+
|
8
|
+
include_context 'Session::Environment'
|
9
|
+
|
10
|
+
it 'returns session relation proxy' do
|
11
|
+
expect(subject).to be_kind_of(Session::Relation)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Session, '#flush' do
|
6
|
+
subject { session.flush }
|
7
|
+
|
8
|
+
include_context 'Session::Relation'
|
9
|
+
|
10
|
+
let(:object) { session }
|
11
|
+
|
12
|
+
let(:john) { session[:users].to_a.first }
|
13
|
+
let(:jane) { session[:users].to_a.last }
|
14
|
+
let(:piotr) { session[:users].new(id: 3, name: 'Piotr') }
|
15
|
+
|
16
|
+
before do
|
17
|
+
session[:users].delete(john)
|
18
|
+
|
19
|
+
jane.name = 'Jane Doe'
|
20
|
+
session[:users].save(jane)
|
21
|
+
|
22
|
+
session[:users].save(piotr)
|
23
|
+
end
|
24
|
+
|
25
|
+
it_behaves_like 'a command method'
|
26
|
+
|
27
|
+
it { should be_clean }
|
28
|
+
|
29
|
+
it 'commits all deletes' do
|
30
|
+
expect(subject[:users].to_a).to_not include(john)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'commits all updates' do
|
34
|
+
expect(subject[:users].to_a.first).to eq(relation.to_a.first)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'commits all inserts' do
|
38
|
+
expect(subject[:users].to_a).to include(piotr)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'sets correct state for created objects' do
|
42
|
+
expect(subject[:users].state(piotr)).to be_persisted
|
43
|
+
expect(subject[:users].dirty?(piotr)).to be(false)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'registers newly created object in the IM' do
|
47
|
+
expect(subject[:users].restrict { |r| r.name.eq('Piotr') }.to_a.first).to be(piotr)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'sets correct state for updated objects' do
|
51
|
+
expect(subject[:users].state(jane)).to be_persisted
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'sets correct state for deleted objects' do
|
55
|
+
expect(subject[:users].state(john)).to be_frozen
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|