james 0.1.1-universal-darwin-10 → 0.2.0-universal-darwin-10
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/aux/james/cli.rb +21 -4
- data/lib/james/builtin/core_dialog.rb +4 -0
- data/lib/james/controller.rb +60 -51
- data/lib/james/conversation.rb +62 -0
- data/lib/james/dialog_internals.rb +62 -9
- data/lib/james/dialogs.rb +6 -28
- data/lib/james/markers/current.rb +34 -0
- data/lib/james/markers/marker.rb +92 -0
- data/lib/james/markers/memory.rb +37 -0
- data/lib/james/state_api.rb +39 -11
- data/lib/james/state_internals.rb +35 -14
- data/lib/james.rb +5 -5
- metadata +7 -23
- data/lib/james/visitor.rb +0 -80
- data/lib/james/visitors.rb +0 -62
- data/spec/aux/james/cli_spec.rb +0 -19
- data/spec/integration/test_dialogue_spec.rb +0 -67
- data/spec/lib/james/controller_spec.rb +0 -35
- data/spec/lib/james/dialog_spec.rb +0 -90
- data/spec/lib/james/inputs/audio_spec.rb +0 -18
- data/spec/lib/james/inputs/terminal_spec.rb +0 -12
- data/spec/lib/james/state_spec.rb +0 -156
- data/spec/lib/james/visitor_spec.rb +0 -101
- data/spec/lib/james/visitors_spec.rb +0 -64
data/aux/james/cli.rb
CHANGED
@@ -5,9 +5,7 @@ module James
|
|
5
5
|
class CLI
|
6
6
|
|
7
7
|
def execute *patterns
|
8
|
-
|
9
|
-
silent_input = patterns.delete '-si'
|
10
|
-
silent_output = patterns.delete '-so'
|
8
|
+
options = extract_options patterns
|
11
9
|
|
12
10
|
dialogs = find_dialogs_for patterns
|
13
11
|
|
@@ -16,16 +14,35 @@ module James
|
|
16
14
|
|
17
15
|
load_all dialogs
|
18
16
|
|
17
|
+
James.listen options
|
18
|
+
end
|
19
|
+
|
20
|
+
# Defines default options and extracts options from
|
21
|
+
# command line.
|
22
|
+
#
|
23
|
+
# Sadly needs to be run before processing the dialog file names.
|
24
|
+
#
|
25
|
+
def extract_options patterns
|
26
|
+
silent = patterns.delete '-s'
|
27
|
+
silent_input = patterns.delete '-si'
|
28
|
+
silent_output = patterns.delete '-so'
|
29
|
+
|
19
30
|
options = {}
|
20
31
|
options[:input] = Inputs::Terminal if silent || silent_input
|
21
32
|
options[:output] = Outputs::Terminal if silent || silent_output
|
22
33
|
|
23
|
-
|
34
|
+
options
|
24
35
|
end
|
36
|
+
|
37
|
+
#
|
38
|
+
#
|
25
39
|
def find_dialogs_for patterns
|
26
40
|
patterns = ["**/*_dialog{,ue}.rb"] if patterns.empty?
|
27
41
|
Dir[*patterns]
|
28
42
|
end
|
43
|
+
|
44
|
+
#
|
45
|
+
#
|
29
46
|
def load_all dialogs
|
30
47
|
dialogs.each do |dialog|
|
31
48
|
load File.expand_path dialog, Dir.pwd
|
data/lib/james/controller.rb
CHANGED
@@ -2,7 +2,7 @@ module James
|
|
2
2
|
|
3
3
|
class Controller
|
4
4
|
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :conversation, :listening, :initial
|
6
6
|
|
7
7
|
# Singleton reader.
|
8
8
|
#
|
@@ -10,63 +10,33 @@ module James
|
|
10
10
|
@controller ||= new
|
11
11
|
end
|
12
12
|
|
13
|
-
# This puts together the
|
13
|
+
# This puts together the initial dialog and the user
|
14
14
|
# ones that are hooked into it.
|
15
15
|
#
|
16
|
-
#
|
17
|
-
#
|
16
|
+
# The initial dialog needs an state defined as initially.
|
17
|
+
# This is where it will start.
|
18
18
|
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
Visitor.new CoreDialog.new.state_for(:awake)
|
25
|
-
end
|
26
|
-
def user_visitor
|
27
|
-
@user_dialogs.visitor
|
28
|
-
end
|
29
|
-
|
30
|
-
# MacRuby callback functions.
|
19
|
+
# Example:
|
20
|
+
# initially :awake
|
21
|
+
# state :awake do
|
22
|
+
# # ...
|
23
|
+
# end
|
31
24
|
#
|
32
|
-
|
33
|
-
|
34
|
-
start_input
|
35
|
-
end
|
36
|
-
def windowWillClose notification
|
37
|
-
exit
|
38
|
-
end
|
39
|
-
|
40
|
-
# Add a dialog to the current system.
|
25
|
+
# If you don't give it an initial dialog,
|
26
|
+
# James will simply use the built-in CoreDialog.
|
41
27
|
#
|
42
|
-
def
|
43
|
-
@
|
28
|
+
def initialize dialog = nil
|
29
|
+
@initial = dialog || CoreDialog.new
|
30
|
+
@conversation = Conversation.new @initial.current
|
44
31
|
end
|
45
32
|
|
46
|
-
#
|
33
|
+
# Convenience method to add a dialog to the current system.
|
47
34
|
#
|
48
|
-
|
49
|
-
|
50
|
-
@input.listen
|
51
|
-
end
|
52
|
-
# Start speaking.
|
35
|
+
# Will add the dialog to the initial dialog passed into the
|
36
|
+
# controller.
|
53
37
|
#
|
54
|
-
def
|
55
|
-
@
|
56
|
-
end
|
57
|
-
|
58
|
-
# Callback method from dialog.
|
59
|
-
#
|
60
|
-
def say text
|
61
|
-
@output.say text
|
62
|
-
end
|
63
|
-
def hear text
|
64
|
-
visitor.hear text do |response|
|
65
|
-
say response
|
66
|
-
end
|
67
|
-
end
|
68
|
-
def expects
|
69
|
-
visitor.expects
|
38
|
+
def << dialog
|
39
|
+
@initial << dialog
|
70
40
|
end
|
71
41
|
|
72
42
|
# Start listening using the provided options.
|
@@ -78,6 +48,8 @@ module James
|
|
78
48
|
def listen options = {}
|
79
49
|
return if listening
|
80
50
|
|
51
|
+
@listening = true
|
52
|
+
|
81
53
|
@input_class = options[:input] || Inputs::Audio
|
82
54
|
@output_class = options[:output] || Outputs::Audio
|
83
55
|
|
@@ -87,11 +59,48 @@ module James
|
|
87
59
|
app = NSApplication.sharedApplication
|
88
60
|
app.delegate = self
|
89
61
|
|
90
|
-
@listening = true
|
91
|
-
|
92
62
|
app.run
|
93
63
|
end
|
94
64
|
|
65
|
+
# The naughty privates of this class.
|
66
|
+
#
|
67
|
+
|
68
|
+
# MacRuby callback functions.
|
69
|
+
#
|
70
|
+
def applicationDidFinishLaunching notification
|
71
|
+
start_output
|
72
|
+
start_input
|
73
|
+
end
|
74
|
+
def windowWillClose notification
|
75
|
+
exit
|
76
|
+
end
|
77
|
+
|
78
|
+
# Start recognizing words.
|
79
|
+
#
|
80
|
+
def start_input
|
81
|
+
@input = @input_class.new self
|
82
|
+
@input.listen
|
83
|
+
end
|
84
|
+
# Start speaking.
|
85
|
+
#
|
86
|
+
def start_output
|
87
|
+
@output = @output_class.new @output_options
|
88
|
+
end
|
89
|
+
|
90
|
+
# Callback method from dialog.
|
91
|
+
#
|
92
|
+
def say text
|
93
|
+
@output.say text
|
94
|
+
end
|
95
|
+
def hear text
|
96
|
+
conversation.hear text do |response|
|
97
|
+
say response
|
98
|
+
end
|
99
|
+
end
|
100
|
+
def expects
|
101
|
+
conversation.expects
|
102
|
+
end
|
103
|
+
|
95
104
|
end
|
96
105
|
|
97
106
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module James
|
2
|
+
|
3
|
+
# A conversation has a number of markers (position in dialog),
|
4
|
+
# whose dialogs are visited in order of preference.
|
5
|
+
#
|
6
|
+
# Why?
|
7
|
+
# Conversations have multiple points where they can be.
|
8
|
+
# (Politics, then this joke, then back again, finally "Oh, bye I have to go!")
|
9
|
+
#
|
10
|
+
class Conversation
|
11
|
+
|
12
|
+
attr_accessor :markers
|
13
|
+
|
14
|
+
# A Conversation keeps a stack of markers with
|
15
|
+
# an initial one.
|
16
|
+
#
|
17
|
+
def initialize initial
|
18
|
+
@markers = [initial]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Hear tries all visitors in order
|
22
|
+
# until one hears a phrase he knows.
|
23
|
+
#
|
24
|
+
# If a dialog boundary has been crossed:
|
25
|
+
# A new visitor is added with the target
|
26
|
+
# state of that heard phrase at the position.
|
27
|
+
#
|
28
|
+
# After that, all remaining visitors are
|
29
|
+
# removed from the current stack (since
|
30
|
+
# we are obviously not in one of the later
|
31
|
+
# dialogs anymore).
|
32
|
+
#
|
33
|
+
def hear phrase, &block
|
34
|
+
self.markers = markers.inject([]) do |remaining, marker|
|
35
|
+
markers = marker.hear phrase, &block
|
36
|
+
remaining = remaining + markers
|
37
|
+
break remaining if remaining.last && remaining.last.current?
|
38
|
+
remaining
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Enter enters the first visitor.
|
43
|
+
#
|
44
|
+
def enter
|
45
|
+
markers.first.enter
|
46
|
+
end
|
47
|
+
|
48
|
+
# Simply returns the sum of what phrases all dialogs do expect, front-to-back.
|
49
|
+
#
|
50
|
+
# Stops as soon as a marker is not on a chainable state anymore.
|
51
|
+
#
|
52
|
+
def expects
|
53
|
+
markers.inject([]) do |expects, marker|
|
54
|
+
total = marker.expects + expects
|
55
|
+
break total unless marker.chainable?
|
56
|
+
total
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -9,35 +9,88 @@ module James
|
|
9
9
|
into.extend ClassMethods
|
10
10
|
end
|
11
11
|
|
12
|
+
# Returns a state instance for the given state / or state name.
|
12
13
|
#
|
14
|
+
# Note: Lazily creates the state instances.
|
13
15
|
#
|
14
|
-
def state_for
|
15
|
-
|
16
|
+
def state_for possible_state
|
17
|
+
return possible_state if possible_state.respond_to?(:expects)
|
18
|
+
self.class.state_for possible_state, self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Chain (the states of) this dialog to the given state.
|
22
|
+
#
|
23
|
+
# Creates state instances if it is given names.
|
24
|
+
#
|
25
|
+
# Note: Be careful not to create circular
|
26
|
+
# state chaining. Except if you really
|
27
|
+
# want that.
|
28
|
+
#
|
29
|
+
def chain_to state
|
30
|
+
warn "Define a hear => :some_state_name in a dialog to have it be able to chain to another." && return unless respond_to?(:entry_phrases)
|
31
|
+
entry_phrases.each do |(phrases, entry_state)|
|
32
|
+
state.hear phrases => state_for(entry_state)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Chain the given Dialog(s) to all chainable
|
37
|
+
# states in this Dialog.
|
38
|
+
#
|
39
|
+
# Note: If you only want one state to chain,
|
40
|
+
# then get it from the otiginating dialog
|
41
|
+
# using dialog.state_for(:name) and
|
42
|
+
# append the dialog there:
|
43
|
+
# dialog.follows preceding_dialog.state_for(:name)
|
44
|
+
#
|
45
|
+
def << dialog_s
|
46
|
+
self.class.states.each do |(name, definition)|
|
47
|
+
state = state_for name # TODO Do not call this everywhere.
|
48
|
+
dialog_s.chain_to(state) if state.chainable?
|
49
|
+
end
|
16
50
|
end
|
17
51
|
|
18
52
|
module ClassMethods
|
19
53
|
|
20
|
-
|
54
|
+
def initially state_name
|
55
|
+
define_method :current do
|
56
|
+
Markers::Current.new state_for(state_name)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Defines the entry phrases into this dialog.
|
61
|
+
#
|
62
|
+
# Example:
|
63
|
+
# hear 'Hello, James!' => :start
|
21
64
|
#
|
22
65
|
def hear definition
|
23
|
-
define_method :
|
66
|
+
define_method :entry_phrases do
|
24
67
|
definition
|
25
68
|
end
|
26
69
|
end
|
27
70
|
|
28
71
|
# Defines a state with transitions.
|
29
72
|
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
73
|
+
# Example:
|
74
|
+
# state :name do
|
75
|
+
# # state properties (hear, into, exit) go here.
|
76
|
+
# end
|
33
77
|
#
|
34
|
-
attr_reader :states
|
35
78
|
def state name, &block
|
36
79
|
@states ||= {}
|
37
80
|
@states[name] ||= block if block_given?
|
81
|
+
define_method name do
|
82
|
+
state_for name
|
83
|
+
end unless instance_methods.include? name
|
38
84
|
end
|
85
|
+
|
86
|
+
#
|
87
|
+
#
|
88
|
+
attr_reader :states
|
89
|
+
|
90
|
+
# Return a state for this name (and dialog instance).
|
91
|
+
#
|
39
92
|
def state_for name, instance
|
40
|
-
# Lazily wrap.
|
93
|
+
# Lazily wrap in State instance.
|
41
94
|
#
|
42
95
|
if states[name].respond_to?(:call)
|
43
96
|
states[name] = State.new(name, instance, &states[name])
|
data/lib/james/dialogs.rb
CHANGED
@@ -1,41 +1,19 @@
|
|
1
1
|
module James
|
2
2
|
|
3
|
-
#
|
3
|
+
# Bundles a bunch of dialogs.
|
4
4
|
#
|
5
5
|
class Dialogs
|
6
6
|
|
7
|
-
attr_reader :
|
7
|
+
attr_reader :dialogs
|
8
8
|
|
9
|
-
def initialize
|
10
|
-
@
|
9
|
+
def initialize *dialogs
|
10
|
+
@dialogs = dialogs
|
11
11
|
end
|
12
12
|
|
13
|
-
# Generate the graph for the dialogs.
|
14
13
|
#
|
15
|
-
# Hooks up the entry phrases of the dialog
|
16
|
-
# into the main dialog.
|
17
14
|
#
|
18
|
-
|
19
|
-
|
20
|
-
#
|
21
|
-
def << dialog
|
22
|
-
resolved_entries = {}
|
23
|
-
|
24
|
-
dialog.entries.each do |(phrases, state)|
|
25
|
-
resolved_entries[phrases] = state.respond_to?(:phrases) ? state : dialog.state_for(state)
|
26
|
-
end
|
27
|
-
|
28
|
-
# Hook the dialog into the initial state.
|
29
|
-
#
|
30
|
-
initial.hear resolved_entries
|
31
|
-
end
|
32
|
-
|
33
|
-
# Get the visitor.
|
34
|
-
#
|
35
|
-
# Initialized on the initial state.
|
36
|
-
#
|
37
|
-
def visitor
|
38
|
-
@visitor ||= Visitor.new initial
|
15
|
+
def chain_to incoming_dialog
|
16
|
+
dialogs.each { |dialog| incoming_dialog << dialog }
|
39
17
|
end
|
40
18
|
|
41
19
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module James
|
2
|
+
|
3
|
+
module Markers
|
4
|
+
|
5
|
+
# The visitor knows where in the conversation we are.
|
6
|
+
#
|
7
|
+
class Current < Marker
|
8
|
+
|
9
|
+
# Hear a phrase.
|
10
|
+
#
|
11
|
+
# Returns a new marker and self if it crossed a boundary.
|
12
|
+
# Returns itself if not.
|
13
|
+
#
|
14
|
+
def hear phrase, &block
|
15
|
+
return [self] unless hears? phrase
|
16
|
+
last = current
|
17
|
+
process(phrase, &block) ? [Memory.new(last), self] : [self]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Expects all phrases, not just internal.
|
21
|
+
#
|
22
|
+
def expects
|
23
|
+
current.expects
|
24
|
+
end
|
25
|
+
|
26
|
+
def current?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module James
|
2
|
+
|
3
|
+
module Markers
|
4
|
+
|
5
|
+
# A marker is a point in conversation
|
6
|
+
# where we once were and might go back.
|
7
|
+
#
|
8
|
+
# TODO: Rename to ?.
|
9
|
+
#
|
10
|
+
class Marker
|
11
|
+
|
12
|
+
attr_accessor :current
|
13
|
+
|
14
|
+
# Pass in an current state.
|
15
|
+
#
|
16
|
+
def initialize current
|
17
|
+
@current = current
|
18
|
+
end
|
19
|
+
|
20
|
+
# Resets the current state back to the initial.
|
21
|
+
#
|
22
|
+
def reset
|
23
|
+
# Never moves, thus never reset.
|
24
|
+
end
|
25
|
+
|
26
|
+
# We hear a phrase.
|
27
|
+
#
|
28
|
+
# Also used to start the whole process.
|
29
|
+
#
|
30
|
+
def enter
|
31
|
+
result = current.__into__
|
32
|
+
yield result if result && block_given?
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
#
|
38
|
+
def exit
|
39
|
+
result = current.__exit__
|
40
|
+
yield result if result && block_given?
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
#
|
46
|
+
def transition phrase
|
47
|
+
state_or_lambda = current.next_for phrase
|
48
|
+
if state_or_lambda.respond_to?(:call)
|
49
|
+
current.__transition__ &state_or_lambda # Don't transition.
|
50
|
+
else
|
51
|
+
self.current = state_or_lambda
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
#
|
57
|
+
def check
|
58
|
+
yield("Whoops. That led nowhere. Perhaps you didn't define the target state?") unless self.current
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns falsy if it stays the same.
|
62
|
+
#
|
63
|
+
def process phrase, &block
|
64
|
+
exit_text = exit &block
|
65
|
+
last_context = current.context
|
66
|
+
transition phrase
|
67
|
+
check &block
|
68
|
+
into_text = enter &block
|
69
|
+
last_context != current.context
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
#
|
74
|
+
def hears? phrase
|
75
|
+
expects.include? phrase
|
76
|
+
end
|
77
|
+
|
78
|
+
# Does the current state allow penetration into another dialog?
|
79
|
+
#
|
80
|
+
def chainable?
|
81
|
+
current.chainable?
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_s
|
85
|
+
"#{self.class.name}(#{initial}, current: #{current})"
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module James
|
2
|
+
|
3
|
+
module Markers
|
4
|
+
|
5
|
+
# A marker is a point in conversation
|
6
|
+
# where we once were and might go back.
|
7
|
+
#
|
8
|
+
# TODO: Rename to ?.
|
9
|
+
#
|
10
|
+
class Memory < Marker
|
11
|
+
|
12
|
+
# Hear a phrase.
|
13
|
+
#
|
14
|
+
# Returns a new Current if it heard.
|
15
|
+
# Returns itself if not.
|
16
|
+
#
|
17
|
+
def hear phrase, &block
|
18
|
+
return [self] unless hears? phrase
|
19
|
+
last = current
|
20
|
+
process(phrase, &block) ? [Memory.new(last), Current.new(current)] : [Current.new(current)]
|
21
|
+
end
|
22
|
+
|
23
|
+
# A marker does not care about phrases that cross dialog boundaries.
|
24
|
+
#
|
25
|
+
def expects
|
26
|
+
current.internal_expects
|
27
|
+
end
|
28
|
+
|
29
|
+
def current?
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/lib/james/state_api.rb
CHANGED
@@ -43,26 +43,42 @@ module James
|
|
43
43
|
#
|
44
44
|
def hear transitions
|
45
45
|
transitions = { transitions => name } unless transitions.respond_to?(:to_hash)
|
46
|
-
@transitions
|
46
|
+
@transitions = expand(transitions).merge @transitions
|
47
47
|
end
|
48
48
|
|
49
|
-
# Execute this block when entering this state.
|
49
|
+
# Execute this block or say the text when entering this state.
|
50
50
|
#
|
51
|
-
|
52
|
-
|
51
|
+
# Examples:
|
52
|
+
# into "Yes, Sir?"
|
53
|
+
# into { "A random number is #{rand(10)}" }
|
54
|
+
#
|
55
|
+
def into text = nil, &block
|
56
|
+
@into_block = block ||
|
57
|
+
text && lambda { text } ||
|
58
|
+
raise_no_text_or_block(__method__)
|
53
59
|
end
|
54
60
|
|
55
|
-
# Execute this block when exiting this state.
|
61
|
+
# Execute this block or say the text when exiting this state.
|
62
|
+
#
|
63
|
+
# Examples:
|
64
|
+
# exit "Yes, Sir?"
|
65
|
+
# exit { "A random number is #{rand(10)}" }
|
56
66
|
#
|
57
|
-
def exit &block
|
58
|
-
@exit_block = block
|
67
|
+
def exit text = nil, &block
|
68
|
+
@exit_block = block ||
|
69
|
+
text && lambda { text } ||
|
70
|
+
raise_no_text_or_block(__method__)
|
59
71
|
end
|
60
72
|
|
61
|
-
#
|
73
|
+
# Chain the given dialog to this state.
|
62
74
|
#
|
63
|
-
def
|
64
|
-
|
75
|
+
def << dialog
|
76
|
+
dialog.chain_to self
|
65
77
|
end
|
78
|
+
|
79
|
+
# A chainable state is a state from which other
|
80
|
+
# Dialogs can be reached.
|
81
|
+
#
|
66
82
|
def chainable
|
67
83
|
@chainable = true
|
68
84
|
end
|
@@ -70,7 +86,7 @@ module James
|
|
70
86
|
# Description of self using name and transitions.
|
71
87
|
#
|
72
88
|
def to_s
|
73
|
-
"#{self.class.name}(#{name}, #{context}, #{transitions})"
|
89
|
+
"#{self.class.name}(#{name}, #{context}, #{@transitions})"
|
74
90
|
end
|
75
91
|
|
76
92
|
# The naughty privates of this class.
|
@@ -90,6 +106,18 @@ module James
|
|
90
106
|
results
|
91
107
|
end
|
92
108
|
|
109
|
+
# Raise an ArgumentError for the given method if it needs either a text or a block.
|
110
|
+
#
|
111
|
+
def raise_no_text_or_block on_method
|
112
|
+
raise ArgumentError.new("Neither block nor text given to ##{on_method} call in #{caller[1]}.")
|
113
|
+
end
|
114
|
+
|
115
|
+
# By default, a state is not chainable.
|
116
|
+
#
|
117
|
+
def chainable?
|
118
|
+
!!@chainable
|
119
|
+
end
|
120
|
+
|
93
121
|
end
|
94
122
|
|
95
123
|
end
|