ellington 0.0.2 → 0.0.3
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/README.md +2 -44
- data/lib/ellington/conductor.rb +16 -8
- data/lib/ellington/connection.rb +18 -3
- data/lib/ellington/errros.rb +6 -6
- data/lib/ellington/line.rb +19 -8
- data/lib/ellington/line_info.rb +2 -1
- data/lib/ellington/line_list.rb +6 -6
- data/lib/ellington/passenger.rb +6 -0
- data/lib/ellington/route.rb +28 -18
- data/lib/ellington/route_info.rb +1 -1
- data/lib/ellington/station.rb +6 -6
- data/lib/ellington/station_list.rb +4 -4
- data/lib/ellington/unique_type_array.rb +1 -4
- data/lib/ellington/version.rb +1 -1
- data/lib/ellington.rb +1 -1
- data/test/attendant_test.rb +5 -5
- data/test/conductor_test.rb +55 -26
- data/test/connection_test.rb +19 -0
- data/test/example.rb +51 -47
- data/test/line_info_test.rb +78 -0
- data/test/line_test.rb +60 -1
- data/test/logger_test.rb +20 -0
- data/test/passenger_test.rb +9 -0
- data/test/route_info_test.rb +63 -0
- data/test/route_test.rb +80 -0
- data/test/station_info_test.rb +31 -0
- data/test/station_test.rb +7 -7
- data/test/test_helper.rb +0 -1
- data/test/transition_info_test.rb +25 -0
- data/test/unique_type_array_test.rb +32 -0
- metadata +24 -14
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Ellington
|
1
|
+
# Ellington
|
2
2
|
Named after [Duke Ellington](http://www.dukeellington.com/) whose signature tune was ["Take the 'A' Train"](http://en.wikipedia.org/wiki/Take_the_%22A%22_Train).
|
3
3
|
The song was written about [New York City's A train](http://en.wikipedia.org/wiki/A_%28New_York_City_Subway_service%29).
|
4
4
|
|
@@ -7,7 +7,7 @@ The song was written about [New York City's A train](http://en.wikipedia.org/wik
|
|
7
7
|
#### Ellington is an architecture for modeling complex business processes.
|
8
8
|
|
9
9
|
Ellington is a collection of simple concepts designed to bring discipline, organization, and modularity to a project.
|
10
|
-
|
10
|
+
([View the slides from the intro talk.](https://speakerdeck.com/hopsoft/ellington-intro))
|
11
11
|
|
12
12
|
The nomenclature is taken from [New York's subway system](http://en.wikipedia.org/wiki/New_York_City_Subway).
|
13
13
|
We've found that using cohesive physical metaphors helps people reason more clearly about the complexities of software.
|
@@ -34,45 +34,3 @@ The Ellington architecture should only be applied **after** a good understanding
|
|
34
34
|
|
35
35
|

|
36
36
|
|
37
|
-
#### Additional Terms
|
38
|
-
|
39
|
-
- **[State Catalog](https://github.com/hopsoft/ellington/wiki/State)** - A collection of states and their transitions.
|
40
|
-
- **[State Transition](https://github.com/hopsoft/ellington/wiki/State)** - The `transition`, performed on the `passenger`, from one `state` to another.
|
41
|
-
- **[Ticket](https://github.com/hopsoft/ellington/wiki/Ticket)** - An authorization token that indicates the `passenger` can ride a specific `route`.
|
42
|
-
- **[Goal](https://github.com/hopsoft/ellington/wiki/Goal)** - A list of expected `states`.
|
43
|
-
- **[Connection](https://github.com/hopsoft/ellington/wiki/Connection)** - A link between `lines`.
|
44
|
-
- **[Transfer](Transfer)** - A link between `routes`.
|
45
|
-
- **[Network](Network)** - An entire system of `routes` & `transfers` designed to work together.
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
```
|
68
|
-
Route class
|
69
|
-
composed of line instances
|
70
|
-
each line instance holds a ref to this route's class
|
71
|
-
|
72
|
-
Line class
|
73
|
-
composed of station instances
|
74
|
-
each station instance holds a ref to this line's class
|
75
|
-
|
76
|
-
Station class
|
77
|
-
```
|
78
|
-
|
data/lib/ellington/conductor.rb
CHANGED
@@ -2,40 +2,46 @@ require "thread"
|
|
2
2
|
|
3
3
|
module Ellington
|
4
4
|
class Conductor
|
5
|
-
attr_reader :route
|
5
|
+
attr_reader :route
|
6
6
|
|
7
7
|
def initialize(route)
|
8
8
|
@route = route
|
9
9
|
@conducting = false
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
|
12
|
+
def conducting?
|
13
|
+
@conducting
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
return if conducting?
|
14
18
|
|
15
19
|
mutex.synchronize do
|
16
20
|
@stop = false
|
17
21
|
@conducting = true
|
18
22
|
end
|
19
23
|
|
20
|
-
thread = Thread.new do
|
24
|
+
@thread = Thread.new do
|
21
25
|
loop do
|
22
26
|
if @stop
|
23
27
|
mutex.synchronize { @conducting = false }
|
24
|
-
|
28
|
+
Thread.current.exit
|
25
29
|
end
|
26
30
|
gather_passengers.each do |passenger|
|
27
31
|
escort(passenger)
|
28
32
|
end
|
29
|
-
sleep delay
|
30
33
|
end
|
31
34
|
end
|
32
|
-
thread.join
|
33
35
|
end
|
34
36
|
|
35
37
|
def stop
|
36
38
|
mutex.synchronize { @stop = true }
|
37
39
|
end
|
38
40
|
|
41
|
+
def wait
|
42
|
+
thread.join
|
43
|
+
end
|
44
|
+
|
39
45
|
def verify(passenger)
|
40
46
|
true
|
41
47
|
end
|
@@ -49,7 +55,9 @@ module Ellington
|
|
49
55
|
route.lines.first.board passenger
|
50
56
|
end
|
51
57
|
|
52
|
-
|
58
|
+
protected
|
59
|
+
|
60
|
+
attr_reader :thread
|
53
61
|
|
54
62
|
def mutex
|
55
63
|
@mutex ||= Mutex.new
|
data/lib/ellington/connection.rb
CHANGED
@@ -1,10 +1,25 @@
|
|
1
1
|
module Ellington
|
2
2
|
class Connection
|
3
|
-
attr_reader :line, :states
|
3
|
+
attr_reader :line, :type, :states
|
4
4
|
|
5
|
-
def initialize(line, states)
|
5
|
+
def initialize(line, type, *states)
|
6
6
|
@line = line
|
7
|
-
@
|
7
|
+
@type = type
|
8
|
+
@states = Ellington::Target.new(*states)
|
9
|
+
end
|
10
|
+
|
11
|
+
def required?(passenger)
|
12
|
+
return false if line.boarded?(passenger)
|
13
|
+
|
14
|
+
if type == :if_any
|
15
|
+
return states.satisfied?(passenger)
|
16
|
+
end
|
17
|
+
|
18
|
+
if type == :if_all
|
19
|
+
return (passenger.state_history & states).length == states.length
|
20
|
+
end
|
21
|
+
|
22
|
+
false
|
8
23
|
end
|
9
24
|
|
10
25
|
end
|
data/lib/ellington/errros.rb
CHANGED
@@ -2,10 +2,7 @@ module Ellington
|
|
2
2
|
class ListAlreadyContainsType < StandardError
|
3
3
|
end
|
4
4
|
|
5
|
-
class
|
6
|
-
end
|
7
|
-
|
8
|
-
class AttendandDisapproves < StandardError
|
5
|
+
class AttendantDisapproves < StandardError
|
9
6
|
end
|
10
7
|
|
11
8
|
class InvalidStateTransition < StandardError
|
@@ -14,9 +11,12 @@ module Ellington
|
|
14
11
|
class NotImplementedError < StandardError
|
15
12
|
end
|
16
13
|
|
17
|
-
class
|
14
|
+
class NoStationsDeclared < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class NoLinesDeclared < StandardError
|
18
18
|
end
|
19
19
|
|
20
|
-
class
|
20
|
+
class NoGoalDeclared < StandardError
|
21
21
|
end
|
22
22
|
end
|
data/lib/ellington/line.rb
CHANGED
@@ -5,7 +5,6 @@ module Ellington
|
|
5
5
|
class Line
|
6
6
|
class << self
|
7
7
|
include Observable
|
8
|
-
attr_accessor :route
|
9
8
|
|
10
9
|
def stations
|
11
10
|
@stations ||= Ellington::StationList.new(self)
|
@@ -16,30 +15,42 @@ module Ellington
|
|
16
15
|
end
|
17
16
|
alias_method :passed, :pass_target
|
18
17
|
alias_method :goal, :pass_target
|
19
|
-
|
20
|
-
def connections
|
21
|
-
@connections ||= {}
|
22
|
-
end
|
23
18
|
end
|
24
19
|
|
25
20
|
extend Forwardable
|
26
21
|
include Observable
|
27
22
|
include HasTargets
|
28
23
|
|
24
|
+
attr_accessor :route_class, :route
|
25
|
+
|
29
26
|
def_delegators :"self.class",
|
30
|
-
:route,
|
31
|
-
:route=,
|
32
27
|
:stations,
|
33
28
|
:pass_target,
|
34
29
|
:passed,
|
35
30
|
:goal
|
36
31
|
|
32
|
+
def initialize
|
33
|
+
if stations.empty?
|
34
|
+
message = "#{self.class.name} has no stations!"
|
35
|
+
raise Ellington::NoStationsDeclared.new(message)
|
36
|
+
end
|
37
|
+
|
38
|
+
if goal.empty?
|
39
|
+
message = "#{self.class.name} has no goal!"
|
40
|
+
raise Ellington::NoGoalDeclared.new(message)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
37
44
|
def board(passenger, options={})
|
38
45
|
formula.run passenger, options
|
39
46
|
end
|
40
47
|
|
48
|
+
def boarded?(passenger)
|
49
|
+
!(passenger.state_history & states.keys).empty?
|
50
|
+
end
|
51
|
+
|
41
52
|
def name
|
42
|
-
@name ||= "#{self.class.name}::#{
|
53
|
+
@name ||= "#{self.class.name}::#{route_class.name}"
|
43
54
|
end
|
44
55
|
|
45
56
|
def formula
|
data/lib/ellington/line_info.rb
CHANGED
@@ -19,12 +19,13 @@ module Ellington
|
|
19
19
|
if options[:line_completed]
|
20
20
|
message << "[LINE COMPLETED]"
|
21
21
|
message << "[#{line.state(passenger)}]"
|
22
|
+
message << "[#{line.name}]"
|
22
23
|
end
|
23
24
|
if options[:station_completed]
|
24
25
|
message << "[STATION COMPLETED]"
|
25
26
|
message << "[#{station.state(passenger)}]"
|
27
|
+
message << "[#{station_full_name}]"
|
26
28
|
end
|
27
|
-
message << "[#{station_full_name}]"
|
28
29
|
line.route.log_passenger_attrs.each do |attr|
|
29
30
|
message << "[#{attr}:#{passenger.send(attr)}]"
|
30
31
|
end
|
data/lib/ellington/line_list.rb
CHANGED
@@ -2,22 +2,22 @@ require "delegate"
|
|
2
2
|
|
3
3
|
module Ellington
|
4
4
|
class LineList < SimpleDelegator
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :route_class
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
7
|
+
def initialize(route_class)
|
8
|
+
@route_class = route_class
|
9
9
|
@inner_list = UniqueTypeArray.new
|
10
10
|
super @inner_list
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
def push(line)
|
14
14
|
value = inner_list << line
|
15
|
-
line.
|
15
|
+
line.route_class = route_class
|
16
16
|
value
|
17
17
|
end
|
18
18
|
alias_method :<<, :push
|
19
19
|
|
20
|
-
protected
|
20
|
+
protected
|
21
21
|
|
22
22
|
attr_reader :inner_list
|
23
23
|
end
|
data/lib/ellington/passenger.rb
CHANGED
@@ -39,6 +39,10 @@ module Ellington
|
|
39
39
|
@current_state = value
|
40
40
|
end
|
41
41
|
|
42
|
+
def state_history
|
43
|
+
@state_history ||= []
|
44
|
+
end
|
45
|
+
|
42
46
|
def transition_to(new_state)
|
43
47
|
if !locked?
|
44
48
|
message = "Cannot transition an unlocked #{self.class.name}'s state"
|
@@ -58,6 +62,8 @@ module Ellington
|
|
58
62
|
self.current_state = new_state
|
59
63
|
end
|
60
64
|
|
65
|
+
state_history << new_state
|
66
|
+
|
61
67
|
changed
|
62
68
|
notify_observers Ellington::TransitionInfo.new(self, old_state, new_state)
|
63
69
|
return_value || new_state
|
data/lib/ellington/route.rb
CHANGED
@@ -5,7 +5,30 @@ module Ellington
|
|
5
5
|
|
6
6
|
def initialize
|
7
7
|
super self.class
|
8
|
-
|
8
|
+
|
9
|
+
if lines.empty?
|
10
|
+
message = "#{self.class.name} has no lines!"
|
11
|
+
raise Ellington::NoLinesDeclared.new(message)
|
12
|
+
end
|
13
|
+
|
14
|
+
if goal.empty?
|
15
|
+
message = "#{self.class.name} has no lines!"
|
16
|
+
raise Ellington::NoGoalDeclared.new(message)
|
17
|
+
end
|
18
|
+
|
19
|
+
initialize_lines
|
20
|
+
states
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize_lines
|
24
|
+
lines.each do |line|
|
25
|
+
line.route = self
|
26
|
+
line.add_observer self, :line_completed
|
27
|
+
line.stations.each do |station|
|
28
|
+
station.line = line
|
29
|
+
station.add_observer line, :station_completed
|
30
|
+
end
|
31
|
+
end
|
9
32
|
end
|
10
33
|
|
11
34
|
class << self
|
@@ -16,21 +39,6 @@ module Ellington
|
|
16
39
|
@initialized
|
17
40
|
end
|
18
41
|
|
19
|
-
def init
|
20
|
-
initialize_lines
|
21
|
-
states
|
22
|
-
@initialized = true
|
23
|
-
end
|
24
|
-
|
25
|
-
def initialize_lines
|
26
|
-
lines.each do |line|
|
27
|
-
line.add_observer self, :line_completed
|
28
|
-
line.stations.each do |station|
|
29
|
-
station.add_observer line, :station_completed
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
42
|
def board(passenger, options={})
|
35
43
|
lines.first.board passenger, options
|
36
44
|
end
|
@@ -75,7 +83,9 @@ module Ellington
|
|
75
83
|
end
|
76
84
|
|
77
85
|
def connect_to(line, options)
|
78
|
-
|
86
|
+
type = options.keys.first
|
87
|
+
states = options[type]
|
88
|
+
connections << Ellington::Connection.new(line, type, states)
|
79
89
|
end
|
80
90
|
|
81
91
|
def log_passenger_attrs(*attrs)
|
@@ -86,7 +96,7 @@ module Ellington
|
|
86
96
|
route_info = Ellington::RouteInfo.new(self, line_info)
|
87
97
|
|
88
98
|
required_connections = connections.select do |connection|
|
89
|
-
connection.
|
99
|
+
connection.required?(route_info.passenger)
|
90
100
|
end
|
91
101
|
|
92
102
|
if required_connections.empty?
|
data/lib/ellington/route_info.rb
CHANGED
@@ -14,7 +14,7 @@ module Ellington
|
|
14
14
|
message = []
|
15
15
|
message << "[ROUTE COMPLETED]"
|
16
16
|
message << "[#{route.state(passenger)}]"
|
17
|
-
message << "[#{
|
17
|
+
message << "[#{route.name}]"
|
18
18
|
line.route.log_passenger_attrs.each do |attr|
|
19
19
|
message << "[#{attr}:#{passenger.send(attr)}]"
|
20
20
|
end
|
data/lib/ellington/station.rb
CHANGED
@@ -6,11 +6,11 @@ module Ellington
|
|
6
6
|
class Station
|
7
7
|
extend Forwardable
|
8
8
|
include Observable
|
9
|
-
attr_accessor :line
|
9
|
+
attr_accessor :line_class, :line
|
10
10
|
def_delegators :line, :route
|
11
11
|
|
12
12
|
def name
|
13
|
-
@name ||= "#{self.class.name}::#{
|
13
|
+
@name ||= "#{self.class.name}::#{line_class.name}"
|
14
14
|
end
|
15
15
|
|
16
16
|
def state_name(state)
|
@@ -55,7 +55,7 @@ module Ellington
|
|
55
55
|
passenger.add_observer attendant
|
56
56
|
engage passenger, options
|
57
57
|
passenger.delete_observer attendant
|
58
|
-
raise Ellington::
|
58
|
+
raise Ellington::AttendantDisapproves unless attendant.approve?
|
59
59
|
changed
|
60
60
|
notify_observers Ellington::StationInfo.new(self, passenger, attendant.passenger_transitions.first, options)
|
61
61
|
end
|
@@ -63,15 +63,15 @@ module Ellington
|
|
63
63
|
passenger
|
64
64
|
end
|
65
65
|
|
66
|
-
def
|
66
|
+
def pass_passenger(passenger)
|
67
67
|
passenger.transition_to passed
|
68
68
|
end
|
69
69
|
|
70
|
-
def
|
70
|
+
def fail_passenger(passenger)
|
71
71
|
passenger.transition_to failed
|
72
72
|
end
|
73
73
|
|
74
|
-
def
|
74
|
+
def error_passenger(passenger)
|
75
75
|
passenger.transition_to errored
|
76
76
|
end
|
77
77
|
|
@@ -2,17 +2,17 @@ require "delegate"
|
|
2
2
|
|
3
3
|
module Ellington
|
4
4
|
class StationList < SimpleDelegator
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :line_class
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
7
|
+
def initialize(line_class)
|
8
|
+
@line_class = line_class
|
9
9
|
@inner_list = UniqueTypeArray.new
|
10
10
|
super @inner_list
|
11
11
|
end
|
12
12
|
|
13
13
|
def push(station)
|
14
14
|
value = inner_list << station
|
15
|
-
station.
|
15
|
+
station.line_class = line_class
|
16
16
|
value
|
17
17
|
end
|
18
18
|
alias_method :<<, :push
|
data/lib/ellington/version.rb
CHANGED
data/lib/ellington.rb
CHANGED
data/test/attendant_test.rb
CHANGED
@@ -3,11 +3,11 @@ require_relative "test_helper"
|
|
3
3
|
class AttendantTest < MicroTest::Test
|
4
4
|
|
5
5
|
before do
|
6
|
-
|
7
|
-
|
8
|
-
@station =
|
9
|
-
@passenger = Ellington::Passenger.new(NumberWithHistory.new(0),
|
10
|
-
@passenger.current_state =
|
6
|
+
route = BasicMath.new
|
7
|
+
line = route.lines.first
|
8
|
+
@station = line.stations.first
|
9
|
+
@passenger = Ellington::Passenger.new(NumberWithHistory.new(0), route)
|
10
|
+
@passenger.current_state = route.initial_state
|
11
11
|
@passenger.lock
|
12
12
|
@attendant = Ellington::Attendant.new(@station)
|
13
13
|
@passenger.add_observer @attendant
|
data/test/conductor_test.rb
CHANGED
@@ -2,31 +2,60 @@ require_relative "test_helper"
|
|
2
2
|
|
3
3
|
class ConductorTest < MicroTest::Test
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
5
|
+
before do
|
6
|
+
@route = BasicMath.new
|
7
|
+
@conductor = Ellington::Conductor.new(@route)
|
8
|
+
@passenger = Ellington::Passenger.new(NumberWithHistory.new(0), @route)
|
9
|
+
@passenger.current_state = @route.initial_state
|
10
|
+
@passenger.lock
|
11
|
+
end
|
12
|
+
|
13
|
+
test "not conducting" do
|
14
|
+
assert !@conductor.conducting?
|
15
|
+
end
|
16
|
+
|
17
|
+
test "verify" do
|
18
|
+
assert @conductor.verify(@passenger)
|
19
|
+
end
|
20
|
+
|
21
|
+
test "gather_passengers is abstract" do
|
22
|
+
error = nil
|
23
|
+
begin
|
24
|
+
@conductor.gather_passengers
|
25
|
+
rescue Ellington::NotImplementedError => e
|
26
|
+
error = e
|
27
|
+
end
|
28
|
+
assert !error.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
test "escort" do
|
32
|
+
@conductor.escort(@passenger)
|
33
|
+
assert @passenger.current_state != @route.initial_state
|
34
|
+
end
|
35
|
+
|
36
|
+
test "escort prevented when passenger doesn't verify" do
|
37
|
+
def @conductor.verify(passenger)
|
38
|
+
false
|
39
|
+
end
|
40
|
+
@conductor.escort(@passenger)
|
41
|
+
assert @passenger.current_state == @route.initial_state
|
42
|
+
end
|
43
|
+
|
44
|
+
test "escort prevented when passenger is not locked" do
|
45
|
+
@passenger.unlock
|
46
|
+
assert @passenger.current_state == @route.initial_state
|
47
|
+
end
|
48
|
+
|
49
|
+
test "start/stop with conducting? checks" do
|
50
|
+
def @conductor.gather_passengers
|
51
|
+
sleep 0.1
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
@conductor.start
|
55
|
+
assert @conductor.conducting?
|
56
|
+
@conductor.stop
|
57
|
+
sleep 0.2
|
58
|
+
puts !@conductor.conducting?
|
59
|
+
end
|
31
60
|
|
32
61
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class ConnectionTest < MicroTest::Test
|
4
|
+
|
5
|
+
before do
|
6
|
+
@route = BasicMath.new
|
7
|
+
@line = @route.lines.first
|
8
|
+
@connection = Ellington::Connection.new(@route.lines[1], :if_currently, @line.passed)
|
9
|
+
end
|
10
|
+
|
11
|
+
test "line" do
|
12
|
+
assert @connection.line == @route.lines[1]
|
13
|
+
end
|
14
|
+
|
15
|
+
test "states" do
|
16
|
+
assert @connection.states == @line.passed
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|