end_state 0.12.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/Gemfile +1 -0
- data/README.md +9 -11
- data/end_state.gemspec +1 -0
- data/lib/end_state/concluder.rb +0 -1
- data/lib/end_state/graph.rb +16 -12
- data/lib/end_state/no_graph.rb +12 -0
- data/lib/end_state/state_machine.rb +55 -140
- data/lib/end_state/state_machine_configuration.rb +77 -0
- data/lib/end_state/transition.rb +35 -49
- data/lib/end_state/transition_configuration.rb +49 -0
- data/lib/end_state/transition_configuration_set.rb +83 -0
- data/lib/end_state/version.rb +1 -1
- data/lib/end_state.rb +4 -1
- data/lib/end_state_matchers.rb +52 -39
- data/spec/end_state/concluder_spec.rb +9 -3
- data/spec/end_state/graph_spec.rb +35 -0
- data/spec/end_state/guard_spec.rb +29 -3
- data/spec/end_state/state_machine_configuration_spec.rb +183 -0
- data/spec/end_state/state_machine_spec.rb +8 -192
- data/spec/end_state/transition_configuration_set_spec.rb +217 -0
- data/spec/end_state/transition_configuration_spec.rb +65 -0
- data/spec/end_state/transition_spec.rb +83 -96
- data/spec/end_state_matchers_spec.rb +105 -0
- data/spec/spec_helper.rb +17 -0
- metadata +31 -8
- data/lib/end_state/state_mapping.rb +0 -25
- data/spec/end_state/state_mapping_spec.rb +0 -94
- data/spec/end_state_spec.rb +0 -4
@@ -0,0 +1,83 @@
|
|
1
|
+
module EndState
|
2
|
+
class TransitionConfigurationSet
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@end_state_map = { any_state: {} } # [start_state][event] = end_state
|
6
|
+
@configuration_map = { any_state: {} } # [start_state][end_state] = configuration
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(start_state, end_state, configuration, event = nil)
|
10
|
+
if event
|
11
|
+
end_state_map[start_state] ||= {}
|
12
|
+
end_state_map[start_state][event] = end_state
|
13
|
+
end
|
14
|
+
|
15
|
+
configuration_map[start_state] ||= {}
|
16
|
+
configuration_map[start_state][end_state] = configuration
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_configuration(start_state, end_state)
|
20
|
+
local_map = configuration_map[start_state] || {}
|
21
|
+
local_map[end_state] || configuration_map[:any_state][end_state]
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_end_state(start_state, event)
|
25
|
+
local_map = end_state_map[start_state] || {}
|
26
|
+
local_map[event] || end_state_map[:any_state][event]
|
27
|
+
end
|
28
|
+
|
29
|
+
def start_states
|
30
|
+
states = configuration_map.keys
|
31
|
+
states.delete(:any_state)
|
32
|
+
states += end_states unless configuration_map[:any_state].empty?
|
33
|
+
states.uniq
|
34
|
+
end
|
35
|
+
|
36
|
+
def end_states
|
37
|
+
configuration_map.map { |_, v| v.keys }.flatten.uniq
|
38
|
+
end
|
39
|
+
|
40
|
+
def events
|
41
|
+
end_state_map.map { |_, v| v.keys }.flatten.uniq
|
42
|
+
end
|
43
|
+
|
44
|
+
def event_conflicts?(start_state, event)
|
45
|
+
!!get_end_state(start_state, event) || (start_state == :any_state && events.include?(event))
|
46
|
+
end
|
47
|
+
|
48
|
+
def each &block
|
49
|
+
all_transitions.each(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :configuration_map, :end_state_map
|
55
|
+
|
56
|
+
def all_transitions
|
57
|
+
all_start_states = start_states
|
58
|
+
|
59
|
+
configuration_map.map do |start_state, local_config|
|
60
|
+
states = (start_state == :any_state) ? all_start_states : [start_state]
|
61
|
+
states.map { |s| transitions_for s, local_config }
|
62
|
+
end.flatten(2)
|
63
|
+
end
|
64
|
+
|
65
|
+
def transitions_for start_state, local_map
|
66
|
+
local_map.map do |end_state, config|
|
67
|
+
[start_state, end_state, config, event_for(start_state, end_state)]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def event_for start_state, end_state
|
72
|
+
(end_state_map[start_state] || {}).each do |k, v|
|
73
|
+
return k if v == end_state
|
74
|
+
end
|
75
|
+
|
76
|
+
end_state_map[:any_state].each do |k, v|
|
77
|
+
return k if v == end_state
|
78
|
+
end
|
79
|
+
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/end_state/version.rb
CHANGED
data/lib/end_state.rb
CHANGED
@@ -6,14 +6,17 @@ require 'end_state/guard'
|
|
6
6
|
require 'end_state/concluder'
|
7
7
|
require 'end_state/concluders'
|
8
8
|
require 'end_state/transition'
|
9
|
-
require 'end_state/
|
9
|
+
require 'end_state/transition_configuration'
|
10
|
+
require 'end_state/transition_configuration_set'
|
10
11
|
require 'end_state/action'
|
12
|
+
require 'end_state/state_machine_configuration'
|
11
13
|
require 'end_state/state_machine'
|
12
14
|
|
13
15
|
begin
|
14
16
|
require 'graphviz'
|
15
17
|
require 'end_state/graph'
|
16
18
|
rescue LoadError
|
19
|
+
require 'end_state/no_graph'
|
17
20
|
end
|
18
21
|
|
19
22
|
module EndState
|
data/lib/end_state_matchers.rb
CHANGED
@@ -4,14 +4,15 @@ module EndStateMatchers
|
|
4
4
|
end
|
5
5
|
|
6
6
|
class TransitionMatcher
|
7
|
-
attr_reader :
|
7
|
+
attr_reader :start_state, :end_state, :event, :machine, :failure_messages, :guards, :concluders, :required_params
|
8
8
|
|
9
9
|
def initialize(transition)
|
10
|
-
@
|
11
|
-
@
|
10
|
+
@start_state, @end_state = transition.first
|
11
|
+
@event = nil
|
12
12
|
@guards = []
|
13
13
|
@concluders = []
|
14
14
|
@required_params = []
|
15
|
+
@failure_messages = []
|
15
16
|
end
|
16
17
|
|
17
18
|
def matches?(actual)
|
@@ -24,11 +25,11 @@ module EndStateMatchers
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def description
|
27
|
-
"have transition
|
28
|
+
"have transition #{start_state} => #{end_state}"
|
28
29
|
end
|
29
30
|
|
30
|
-
def
|
31
|
-
@
|
31
|
+
def with_event(event)
|
32
|
+
@event = event
|
32
33
|
self
|
33
34
|
end
|
34
35
|
|
@@ -37,11 +38,6 @@ module EndStateMatchers
|
|
37
38
|
self
|
38
39
|
end
|
39
40
|
|
40
|
-
def with_concluder(concluder)
|
41
|
-
@concluders << concluder
|
42
|
-
self
|
43
|
-
end
|
44
|
-
|
45
41
|
def with_concluders(*concluders)
|
46
42
|
@concluders += Array(concluders).flatten
|
47
43
|
self
|
@@ -52,6 +48,10 @@ module EndStateMatchers
|
|
52
48
|
self
|
53
49
|
end
|
54
50
|
|
51
|
+
alias_method :with_guard, :with_guards
|
52
|
+
alias_method :with_concluder, :with_concluders
|
53
|
+
alias_method :with_required_param, :with_required_params
|
54
|
+
|
55
55
|
# Backward compatibility
|
56
56
|
# Finalizer is deprecated
|
57
57
|
alias_method :with_finalizer, :with_concluder
|
@@ -60,50 +60,63 @@ module EndStateMatchers
|
|
60
60
|
|
61
61
|
private
|
62
62
|
|
63
|
+
def transition_configuration
|
64
|
+
@tc = machine.transition_configurations.get_configuration(start_state, end_state)
|
65
|
+
end
|
66
|
+
|
67
|
+
def has_event?(event)
|
68
|
+
machine.transition_configurations.get_end_state(start_state, event) == end_state
|
69
|
+
end
|
70
|
+
|
71
|
+
def has_guard?(guard)
|
72
|
+
transition_configuration.guards.include?(guard)
|
73
|
+
end
|
74
|
+
|
75
|
+
def has_concluder?(concluder)
|
76
|
+
transition_configuration.concluders.include?(concluder)
|
77
|
+
end
|
78
|
+
|
79
|
+
def has_required_param?(param)
|
80
|
+
transition_configuration.required_params.include?(param)
|
81
|
+
end
|
82
|
+
|
83
|
+
def add_failure(suffix)
|
84
|
+
failure_messages << "expected transition #{start_state} => #{end_state} #{suffix}"
|
85
|
+
end
|
86
|
+
|
63
87
|
def verify
|
64
|
-
|
65
|
-
|
66
|
-
result = (result && verify_guards) if guards.any?
|
67
|
-
result = (result && verify_concluders) if concluders.any?
|
68
|
-
result = (result && verify_required_params) if required_params.any?
|
69
|
-
result
|
88
|
+
if transition_configuration.nil?
|
89
|
+
add_failure('to be defined')
|
70
90
|
else
|
71
|
-
|
72
|
-
|
91
|
+
verify_event if event
|
92
|
+
verify_guards
|
93
|
+
verify_concluders
|
94
|
+
verify_required_params
|
73
95
|
end
|
96
|
+
|
97
|
+
failure_messages.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
def verify_event
|
101
|
+
add_failure("to have event name: #{event}") unless has_event?(event)
|
74
102
|
end
|
75
103
|
|
76
104
|
def verify_guards
|
77
|
-
|
78
|
-
|
79
|
-
unless machine.transitions[transition].guards.any? { |g| g == guard }
|
80
|
-
failure_messages << "expected that transition :#{transition.keys.first} => :#{transition.values.first} would have guard #{guard.name}"
|
81
|
-
result = false
|
82
|
-
end
|
105
|
+
guards.map do |guard|
|
106
|
+
add_failure("to have guard #{guard.name}") unless has_guard?(guard)
|
83
107
|
end
|
84
|
-
result
|
85
108
|
end
|
86
109
|
|
87
110
|
def verify_concluders
|
88
|
-
|
89
|
-
|
90
|
-
unless machine.transitions[transition].concluders.any? { |f| f == concluder }
|
91
|
-
failure_messages << "expected that transition :#{transition.keys.first} => :#{transition.values.first} would have concluder #{concluder.name}"
|
92
|
-
result = false
|
93
|
-
end
|
111
|
+
concluders.map do |concluder|
|
112
|
+
add_failure("to have concluder #{concluder.name}") unless has_concluder?(concluder)
|
94
113
|
end
|
95
|
-
result
|
96
114
|
end
|
97
115
|
|
98
116
|
def verify_required_params
|
99
|
-
result = true
|
100
117
|
required_params.each do |param|
|
101
|
-
|
102
|
-
failure_messages << "expected that transition :#{transition.keys.first} => :#{transition.values.first} would have required param #{param}"
|
103
|
-
result = false
|
104
|
-
end
|
118
|
+
add_failure("to have required param #{param}") unless has_required_param?(param)
|
105
119
|
end
|
106
|
-
result
|
107
120
|
end
|
108
121
|
end
|
109
122
|
end
|
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
module EndState
|
4
4
|
describe Concluder do
|
5
5
|
subject(:concluder) { Concluder.new(object, state, params) }
|
6
|
-
let(:object) {
|
6
|
+
let(:object) { OpenStruct.new(failure_messages: [], success_messages: []) }
|
7
7
|
let(:state) { :a }
|
8
8
|
let(:params) { {} }
|
9
9
|
before do
|
@@ -20,8 +20,14 @@ module EndState
|
|
20
20
|
|
21
21
|
describe '#add_success' do
|
22
22
|
it 'adds an success' do
|
23
|
-
concluder.
|
24
|
-
expect(object.
|
23
|
+
concluder.add_success('success')
|
24
|
+
expect(object.success_messages).to eq ['success']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'call' do
|
29
|
+
it 'returns false' do
|
30
|
+
expect(concluder.call).to be false
|
25
31
|
end
|
26
32
|
end
|
27
33
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EndState::Graph do
|
4
|
+
class TestMachine < EndState::StateMachine
|
5
|
+
transition a: :b
|
6
|
+
transition b: :c
|
7
|
+
transition c: :d, as: :go
|
8
|
+
transition any_state: :a
|
9
|
+
transition any_state: :e, as: :exit
|
10
|
+
end
|
11
|
+
|
12
|
+
subject(:graph) { EndState::Graph.new(TestMachine) }
|
13
|
+
|
14
|
+
describe '#draw' do
|
15
|
+
let(:description) { graph.draw.to_s }
|
16
|
+
|
17
|
+
it 'contains all the nodes' do
|
18
|
+
['a [label = "a"];', 'b [label = "b"];', 'c [label = "c"];', 'd [label = "d"];', 'e [label = "e"];'].each do |s|
|
19
|
+
expect(description).to include(s)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'contains all the edges without labels' do
|
24
|
+
['a -> b;', 'b -> c;', 'a -> a;', 'b -> a;', 'c -> a;', 'd -> a;', 'e -> a;'].each do |s|
|
25
|
+
expect(description).to include(s)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'contains all the edges with labels' do
|
30
|
+
['c -> d [label = "go"];', 'a -> e [label = "exit"];', 'b -> e [label = "exit"];', 'c -> e [label = "exit"];', 'd -> e [label = "exit"];', 'e -> e [label = "exit"];'].each do |s|
|
31
|
+
expect(description).to include(s)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
module EndState
|
4
4
|
describe Guard do
|
5
5
|
subject(:guard) { Guard.new(object, state, params) }
|
6
|
-
let(:object) {
|
6
|
+
let(:object) { OpenStruct.new(failure_messages: [], success_messages: []) }
|
7
7
|
let(:state) { :a }
|
8
8
|
let(:params) { {} }
|
9
9
|
before do
|
@@ -20,8 +20,34 @@ module EndState
|
|
20
20
|
|
21
21
|
describe '#add_success' do
|
22
22
|
it 'adds an success' do
|
23
|
-
guard.
|
24
|
-
expect(object.
|
23
|
+
guard.add_success('success')
|
24
|
+
expect(object.success_messages).to eq ['success']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'will_allow?' do
|
29
|
+
it 'returns false' do
|
30
|
+
expect(guard.will_allow?).to be false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'allowed?' do
|
35
|
+
context 'will_allow? returns true' do
|
36
|
+
before { allow(guard).to receive(:will_allow?).and_return(true) }
|
37
|
+
|
38
|
+
it 'calls passed and returns true' do
|
39
|
+
expect(guard).to receive(:passed)
|
40
|
+
expect(guard.allowed?).to be true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'will_allow? returns false' do
|
45
|
+
before { allow(guard).to receive(:will_allow?).and_return(false) }
|
46
|
+
|
47
|
+
it 'calls failed and returns false' do
|
48
|
+
expect(guard).to receive(:failed)
|
49
|
+
expect(guard.allowed?).to be false
|
50
|
+
end
|
25
51
|
end
|
26
52
|
end
|
27
53
|
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module EndState
|
5
|
+
describe StateMachineConfiguration do
|
6
|
+
subject(:machine) { StateMachine.new(object) }
|
7
|
+
let(:object) { OpenStruct.new(state: nil) }
|
8
|
+
before do
|
9
|
+
StateMachine.instance_variable_set '@transition_configurations'.to_sym, nil
|
10
|
+
StateMachine.instance_variable_set '@events'.to_sym, nil
|
11
|
+
StateMachine.instance_variable_set '@store_states_as_strings'.to_sym, nil
|
12
|
+
StateMachine.instance_variable_set '@initial_state'.to_sym, :__nil__
|
13
|
+
StateMachine.instance_variable_set '@mode'.to_sym, :soft
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.transition' do
|
17
|
+
let(:options) { { a: :b } }
|
18
|
+
|
19
|
+
before do
|
20
|
+
@transition_configuration = nil
|
21
|
+
StateMachine.transition(options) { |tc| @transition_configuration = tc }
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'does not require a block' do
|
25
|
+
expect { StateMachine.transition(options) }.not_to raise_error
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'single transition' do
|
29
|
+
it 'yields a transition configuraton' do
|
30
|
+
expect(@transition_configuration).to be_a TransitionConfiguration
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'with as' do
|
34
|
+
let(:options) { { a: :b, as: :go } }
|
35
|
+
|
36
|
+
it 'creates an alias' do
|
37
|
+
expect(StateMachine).to have_transition(a: :b).with_event(:go)
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'another single transition with as' do
|
41
|
+
before { StateMachine.transition({c: :d, as: :go}) }
|
42
|
+
|
43
|
+
it 'appends to the event' do
|
44
|
+
expect(StateMachine).to have_transition(a: :b).with_event(:go)
|
45
|
+
expect(StateMachine).to have_transition(c: :d).with_event(:go)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'another single transition with as that conflicts' do
|
50
|
+
it 'raises an error' do
|
51
|
+
expect{ StateMachine.transition({a: :c, as: :go}) }.to raise_error EventConflict,
|
52
|
+
"Attempting to define event 'go' on state 'a', but it is already defined. " \
|
53
|
+
"(Check duplicates and use of 'any_state')"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'another single transition with as that conflicts' do
|
58
|
+
it 'raises an error' do
|
59
|
+
expect{ StateMachine.transition({any_state: :c, as: :go}) }.to raise_error EventConflict,
|
60
|
+
"Attempting to define event 'go' on state 'any_state', but it is already defined. " \
|
61
|
+
"(Check duplicates and use of 'any_state')"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'multiple start states' do
|
68
|
+
let(:options) { { [:a, :b] => :c } }
|
69
|
+
|
70
|
+
it 'yields a transition configuraton' do
|
71
|
+
expect(@transition_configuration).to be_a TransitionConfiguration
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'has both transitions' do
|
75
|
+
expect(StateMachine).to have_transition(a: :c)
|
76
|
+
expect(StateMachine).to have_transition(b: :c)
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'with as' do
|
80
|
+
let(:options) { { [:a, :b] => :c, as: :go } }
|
81
|
+
|
82
|
+
it 'creates an alias' do
|
83
|
+
expect(StateMachine).to have_transition(a: :c).with_event(:go)
|
84
|
+
expect(StateMachine).to have_transition(b: :c).with_event(:go)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'multiple transitions' do
|
90
|
+
let(:options) { { a: :b, c: :d } }
|
91
|
+
|
92
|
+
it 'yields a transition configuraton' do
|
93
|
+
expect(@transition_configuration).to be_a TransitionConfiguration
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'has both transitions' do
|
97
|
+
expect(StateMachine).to have_transition(a: :b)
|
98
|
+
expect(StateMachine).to have_transition(c: :d)
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'with as' do
|
102
|
+
let(:options) { { a: :b, c: :d, as: :go } }
|
103
|
+
|
104
|
+
it 'creates an alias' do
|
105
|
+
expect(StateMachine).to have_transition(a: :b).with_event(:go)
|
106
|
+
expect(StateMachine).to have_transition(c: :d).with_event(:go)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe '.state_attribute' do
|
113
|
+
context 'when set to :foobar' do
|
114
|
+
let(:object) { OpenStruct.new(foobar: :a) }
|
115
|
+
before { StateMachine.state_attribute :foobar }
|
116
|
+
|
117
|
+
it 'answers state with foobar' do
|
118
|
+
expect(machine.state).to eq object.foobar
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'answers state= with foobar=' do
|
122
|
+
machine.state = :b
|
123
|
+
expect(object.foobar).to eq :b
|
124
|
+
end
|
125
|
+
|
126
|
+
after do
|
127
|
+
StateMachine.send(:remove_method, :state)
|
128
|
+
StateMachine.send(:remove_method, :state=)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe '.states' do
|
134
|
+
before do
|
135
|
+
StateMachine.transition(a: :b)
|
136
|
+
StateMachine.transition(b: :c)
|
137
|
+
end
|
138
|
+
|
139
|
+
specify { expect(StateMachine.states).to eq [:a, :b, :c] }
|
140
|
+
end
|
141
|
+
|
142
|
+
describe '.start_states' do
|
143
|
+
before do
|
144
|
+
StateMachine.transition(a: :b)
|
145
|
+
StateMachine.transition(b: :c)
|
146
|
+
end
|
147
|
+
|
148
|
+
specify { expect(StateMachine.start_states).to eq [:a, :b] }
|
149
|
+
end
|
150
|
+
|
151
|
+
describe '.end_states' do
|
152
|
+
before do
|
153
|
+
StateMachine.transition(a: :b)
|
154
|
+
StateMachine.transition(b: :c)
|
155
|
+
end
|
156
|
+
|
157
|
+
specify { expect(StateMachine.end_states).to eq [:b, :c] }
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '.store_states_as_strings!' do
|
161
|
+
it 'sets the flag' do
|
162
|
+
StateMachine.store_states_as_strings!
|
163
|
+
expect(StateMachine.store_states_as_strings).to be true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe '.store_states_as_strings' do
|
168
|
+
it 'is false by default' do
|
169
|
+
expect(StateMachine.store_states_as_strings).to be false
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe '.initial_state' do
|
174
|
+
context 'when set to :first' do
|
175
|
+
before { StateMachine.set_initial_state :first }
|
176
|
+
|
177
|
+
it 'has that initial state' do
|
178
|
+
expect(machine.state).to eq :first
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|