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,66 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+
6
+ # Session-specific environment wrapping ROM's environment
7
+ #
8
+ # It works exactly the same as ROM::Environment except it returns
9
+ # session relations
10
+ #
11
+ # @api public
12
+ class Environment < ROM::Environment
13
+ include Proxy
14
+
15
+ attr_reader :environment, :tracker, :memory
16
+ private :environment, :tracker, :memory
17
+
18
+ # @api private
19
+ def self.build(environment, tracker = Tracker.new)
20
+ new(environment, tracker)
21
+ end
22
+
23
+ # @api private
24
+ def initialize(environment, tracker)
25
+ @environment = environment
26
+ @tracker = tracker
27
+ initialize_memory
28
+ end
29
+
30
+ # Return a relation identified by name
31
+ #
32
+ # @param [Symbol] name of a relation
33
+ #
34
+ # @return [Session::Relation] rom's relation wrapped by session
35
+ #
36
+ # @api public
37
+ def [](name)
38
+ memory[name]
39
+ end
40
+
41
+ # @api private
42
+ def commit
43
+ tracker.commit
44
+ end
45
+
46
+ # @api private
47
+ def clean?
48
+ tracker.clean?
49
+ end
50
+
51
+ private
52
+
53
+ # @api private
54
+ def initialize_memory
55
+ @memory = Hash.new { |_, name| memory[name] = build_relation(name) }
56
+ end
57
+
58
+ # @api private
59
+ def build_relation(name)
60
+ Relation.build(environment[name], tracker)
61
+ end
62
+
63
+ end # Environment
64
+
65
+ end # Session
66
+ end # ROM
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+
6
+ # @api private
7
+ class IdentityMap
8
+ include Concord.new(:objects)
9
+
10
+ class LoadedObject
11
+ include Concord::Public.new(:object, :tuple)
12
+ end
13
+
14
+ # @api private
15
+ def self.build(objects = {})
16
+ new(objects)
17
+ end
18
+
19
+ # @api private
20
+ def [](identity)
21
+ objects[identity]
22
+ end
23
+
24
+ # @api private
25
+ def fetch_object(identity, &block)
26
+ objects.fetch(identity, &block).object
27
+ end
28
+
29
+ # @api private
30
+ def fetch_tuple(identity)
31
+ self[identity].tuple
32
+ end
33
+
34
+ # @api private
35
+ def store(identity, object, tuple)
36
+ objects[identity] = LoadedObject.new(object, tuple)
37
+ self
38
+ end
39
+
40
+ end # IdentityMap
41
+
42
+ end # Session
43
+ end # ROM
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+
6
+ # @api private
7
+ class Mapper < ROM::Mapper
8
+ include Proxy, Concord::Public.new(:mapper, :tracker, :identity_map)
9
+
10
+ # @api private
11
+ def self.build(mapper, tracker)
12
+ new(mapper, tracker, IdentityMap.build)
13
+ end
14
+
15
+ # @api private
16
+ def dirty?(object)
17
+ identity_map.fetch_tuple(identity(object)) != dump(object)
18
+ end
19
+
20
+ # @api private
21
+ def load(tuple)
22
+ identity = mapper.identity_from_tuple(tuple)
23
+ identity_map.fetch_object(identity) { load_and_track(identity, tuple) }
24
+ end
25
+
26
+ # @api private
27
+ def store_in_identity_map(object)
28
+ identity_map.store(identity(object), object, dump(object))
29
+ end
30
+
31
+ private
32
+
33
+ # @api private
34
+ def load_and_track(identity, tuple)
35
+ object = mapper.load(tuple)
36
+ tracker.store_persisted(object, self)
37
+ identity_map.store(identity, object, tuple)[identity]
38
+ end
39
+
40
+ end # Mapper
41
+
42
+ end # Session
43
+ end # ROM
@@ -0,0 +1,126 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+
6
+ # Adds session-specific functionality on top of ROM's relation.
7
+ #
8
+ # A session relation builds a queue of state changes that will be committed
9
+ # when a session is flushed.
10
+ #
11
+ # @api public
12
+ class Relation < ROM::Relation
13
+ include Proxy
14
+
15
+ attr_reader :relation, :tracker
16
+ private :relation, :tracker
17
+
18
+ # @api private
19
+ def self.build(relation, tracker)
20
+ mapper = Mapper.build(relation.mapper, tracker)
21
+ new(relation.inject_mapper(mapper), tracker)
22
+ end
23
+
24
+ # @api private
25
+ def initialize(relation, tracker)
26
+ @relation, @tracker = relation, tracker
27
+ end
28
+
29
+ # Transition an object into a saved state
30
+ #
31
+ # Transient object's state turns into Created
32
+ # Persisted object's state turns into Updated
33
+ #
34
+ # @param [Object] an object to be saved
35
+ #
36
+ # @return [Session::Relation]
37
+ #
38
+ # @api public
39
+ def save(object)
40
+ tracker.queue(state(object).save(relation))
41
+ self
42
+ end
43
+
44
+ # Transient an object into a deleted state
45
+ #
46
+ # @param [Object] an object to be deleted
47
+ #
48
+ # @return [Session::Relation]
49
+ #
50
+ # @api public
51
+ def delete(object)
52
+ tracker.queue(state(object).delete(relation))
53
+ self
54
+ end
55
+
56
+ # Return current state of the tracked object
57
+ #
58
+ # @param [Object] an object
59
+ #
60
+ # @return [Session::State]
61
+ #
62
+ # @api public
63
+ def state(object)
64
+ tracker.fetch(object)
65
+ end
66
+
67
+ # Return object's identity
68
+ #
69
+ # @param [Object] an object
70
+ #
71
+ # @return [Array]
72
+ #
73
+ # @api public
74
+ def identity(object)
75
+ mapper.identity(object)
76
+ end
77
+
78
+ # Start tracking an object within this session
79
+ #
80
+ # @param [Object] an object to be track
81
+ #
82
+ # @return [Session::Relation]
83
+ #
84
+ # @api public
85
+ def track(object)
86
+ tracker.store_transient(object, mapper)
87
+ self
88
+ end
89
+
90
+ # Build a new object instance and start tracking
91
+ #
92
+ # @return [Object]
93
+ #
94
+ # @api public
95
+ def new(*args, &block)
96
+ object = mapper.new_object(*args, &block)
97
+ track(object)
98
+ object
99
+ end
100
+
101
+ # Check if a tracked object is dirty
102
+ #
103
+ # @param [Object] an object
104
+ #
105
+ # @return [Boolean]
106
+ #
107
+ # @api public
108
+ def dirty?(object)
109
+ state(object).transient? || mapper.dirty?(object)
110
+ end
111
+
112
+ # Check if an object is being tracked
113
+ #
114
+ # @param [Object]
115
+ #
116
+ # @return [Boolean]
117
+ #
118
+ # @api public
119
+ def tracking?(object)
120
+ tracker.include?(object)
121
+ end
122
+
123
+ end # Relation
124
+
125
+ end # Session
126
+ end # ROM
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+
6
+ # @api private
7
+ class State
8
+ include Concord::Public.new(:object)
9
+
10
+ TransitionError = Class.new(StandardError)
11
+
12
+ # @api private
13
+ def save(*)
14
+ raise TransitionError, "cannot save object with #{self.class} state"
15
+ end
16
+
17
+ # @api private
18
+ def delete(*)
19
+ raise TransitionError, "cannot delete object with #{self.class} state"
20
+ end
21
+
22
+ # @api private
23
+ def transient?
24
+ instance_of?(Transient)
25
+ end
26
+
27
+ # @api private
28
+ def created?
29
+ instance_of?(Created)
30
+ end
31
+
32
+ # @api private
33
+ def persisted?
34
+ instance_of?(Persisted)
35
+ end
36
+
37
+ # @api private
38
+ def updated?
39
+ instance_of?(Updated)
40
+ end
41
+
42
+ # @api private
43
+ def deleted?
44
+ instance_of?(Deleted)
45
+ end
46
+
47
+ end # State
48
+
49
+ end # Session
50
+ end # ROM
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+ class State
6
+
7
+ # @api private
8
+ class Created < self
9
+ include Concord::Public.new(:object, :mapper, :relation), Adamantium::Flat
10
+
11
+ # @api private
12
+ def commit
13
+ relation.insert(object)
14
+ mapper.store_in_identity_map(object)
15
+ Persisted.new(object, mapper)
16
+ end
17
+
18
+ end # Created
19
+
20
+ end # State
21
+ end # Session
22
+ end # ROM
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+ class State
6
+
7
+ # @api private
8
+ class Deleted < self
9
+ include Concord::Public.new(:object, :relation), Adamantium::Flat
10
+
11
+ class Committed < State
12
+ include Adamantium
13
+ end
14
+
15
+ # @api private
16
+ def commit
17
+ relation.delete(object)
18
+ Committed.new(object)
19
+ end
20
+
21
+ end # Deleted
22
+
23
+ end # State
24
+ end # Session
25
+ end # ROM
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+ class State
6
+
7
+ # @api private
8
+ class Persisted < self
9
+ include Concord::Public.new(:object, :mapper)
10
+
11
+ # @api private
12
+ def save(relation)
13
+ if mapper.dirty?(object)
14
+ Updated.new(object, mapper, relation)
15
+ else
16
+ self
17
+ end
18
+ end
19
+
20
+ # @api private
21
+ def delete(relation)
22
+ Deleted.new(object, relation)
23
+ end
24
+
25
+ end # Persisted
26
+
27
+ end # State
28
+ end # Session
29
+ end # ROM
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+ class State
6
+
7
+ # @api private
8
+ class Transient < self
9
+ include Concord::Public.new(:object, :mapper)
10
+
11
+ # @api private
12
+ def save(relation)
13
+ Created.new(object, mapper, relation)
14
+ end
15
+
16
+ end # Transient
17
+
18
+ end # State
19
+ end # Session
20
+ end # ROM
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Session
5
+ class State
6
+
7
+ # @api private
8
+ class Updated < self
9
+ include Adamantium::Flat
10
+ include Concord::Public.new(:object, :mapper, :relation)
11
+
12
+ # @api private
13
+ def commit
14
+ relation.update(object, original_tuple)
15
+ Persisted.new(object, mapper)
16
+ end
17
+
18
+ private
19
+
20
+ # @api private
21
+ def original_tuple
22
+ mapper.identity_map.fetch_tuple(mapper.identity(object))
23
+ end
24
+
25
+ end # Updated
26
+
27
+ end # State
28
+ end # Session
29
+ end # ROM