ellington 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
![Ellington Diagram](https://raw.github.com/hopsoft/ellington/master/doc/primary-terms.png)
|
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
|