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.
Files changed (53) hide show
  1. data/.gitignore +9 -0
  2. data/.rspec +2 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +22 -0
  6. data/Gemfile +30 -0
  7. data/Gemfile.devtools +55 -0
  8. data/Guardfile +19 -0
  9. data/LICENSE +20 -0
  10. data/README.md +21 -0
  11. data/Rakefile +4 -0
  12. data/config/devtools.yml +2 -0
  13. data/config/flay.yml +3 -0
  14. data/config/flog.yml +2 -0
  15. data/config/mutant.yml +3 -0
  16. data/config/reek.yml +96 -0
  17. data/config/rubocop.yml +45 -0
  18. data/lib/rom-session.rb +43 -0
  19. data/lib/rom/session.rb +62 -0
  20. data/lib/rom/session/environment.rb +66 -0
  21. data/lib/rom/session/identity_map.rb +43 -0
  22. data/lib/rom/session/mapper.rb +43 -0
  23. data/lib/rom/session/relation.rb +126 -0
  24. data/lib/rom/session/state.rb +50 -0
  25. data/lib/rom/session/state/created.rb +22 -0
  26. data/lib/rom/session/state/deleted.rb +25 -0
  27. data/lib/rom/session/state/persisted.rb +29 -0
  28. data/lib/rom/session/state/transient.rb +20 -0
  29. data/lib/rom/session/state/updated.rb +29 -0
  30. data/lib/rom/session/tracker.rb +69 -0
  31. data/lib/rom/session/version.rb +9 -0
  32. data/lib/rom/support/proxy.rb +50 -0
  33. data/rom-session.gemspec +24 -0
  34. data/spec/integration/session_spec.rb +71 -0
  35. data/spec/rcov.opts +7 -0
  36. data/spec/shared/unit/environment_context.rb +11 -0
  37. data/spec/shared/unit/relation_context.rb +18 -0
  38. data/spec/spec_helper.rb +70 -0
  39. data/spec/unit/rom/session/class_methods/start_spec.rb +23 -0
  40. data/spec/unit/rom/session/clean_predicate_spec.rb +21 -0
  41. data/spec/unit/rom/session/environment/element_reader_spec.rb +13 -0
  42. data/spec/unit/rom/session/flush_spec.rb +58 -0
  43. data/spec/unit/rom/session/mapper/load_spec.rb +44 -0
  44. data/spec/unit/rom/session/relation/delete_spec.rb +28 -0
  45. data/spec/unit/rom/session/relation/dirty_predicate_spec.rb +38 -0
  46. data/spec/unit/rom/session/relation/identity_spec.rb +11 -0
  47. data/spec/unit/rom/session/relation/new_spec.rb +50 -0
  48. data/spec/unit/rom/session/relation/save_spec.rb +50 -0
  49. data/spec/unit/rom/session/relation/state_spec.rb +26 -0
  50. data/spec/unit/rom/session/relation/track_spec.rb +23 -0
  51. data/spec/unit/rom/session/relation/tracking_predicate_spec.rb +23 -0
  52. data/spec/unit/rom/session/state_spec.rb +79 -0
  53. 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,9 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+
6
+ VERSION = '0.1.0'.freeze
7
+
8
+ end # Session
9
+ 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
@@ -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,7 @@
1
+ --exclude-only "spec/,^/"
2
+ --sort coverage
3
+ --callsites
4
+ --xrefs
5
+ --profile
6
+ --text-summary
7
+ --failure-threshold 100
@@ -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
@@ -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