ego 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/.travis.yml +4 -0
- data/.yardopts +1 -0
- data/README.md +40 -35
- data/ego.gemspec +1 -0
- data/lib/ego.rb +40 -3
- data/lib/ego/capability.rb +30 -0
- data/lib/ego/filesystem.rb +58 -29
- data/lib/ego/handler.rb +62 -59
- data/lib/ego/options.rb +14 -2
- data/lib/ego/plugin.rb +58 -0
- data/lib/ego/plugins/capabilities.rb +17 -0
- data/lib/ego/plugins/fallback.rb +36 -0
- data/lib/ego/plugins/robot_io.rb +16 -0
- data/lib/ego/plugins/social.rb +19 -0
- data/lib/ego/plugins/system.rb +21 -0
- data/lib/ego/printer.rb +95 -0
- data/lib/ego/robot.rb +157 -10
- data/lib/ego/robot_error.rb +8 -0
- data/lib/ego/runner.rb +51 -20
- data/lib/ego/version.rb +2 -1
- data/spec/ego/capability_spec.rb +23 -0
- data/spec/ego/handler_spec.rb +63 -0
- data/spec/ego/options_spec.rb +23 -13
- data/spec/ego/plugin_spec.rb +68 -0
- data/spec/ego/printer_spec.rb +120 -0
- data/spec/ego/robot_error_spec.rb +12 -0
- data/spec/ego/robot_spec.rb +283 -26
- metadata +36 -10
- data/lib/ego/formatter.rb +0 -36
- data/lib/ego/handler/default.rb +0 -31
- data/lib/ego/handler/echo.rb +0 -9
- data/lib/ego/handler/greet.rb +0 -17
- data/lib/ego/handler/handlers.rb +0 -15
- data/lib/ego/handler/self.rb +0 -9
- data/lib/ego/listener.rb +0 -25
- data/spec/ego/formatter_spec.rb +0 -53
data/lib/ego/options.rb
CHANGED
@@ -1,29 +1,42 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
|
3
3
|
module Ego
|
4
|
+
# Parse command-line options and set defaults.
|
4
5
|
class Options
|
5
6
|
|
6
7
|
attr_reader :mode,
|
8
|
+
:plugins,
|
7
9
|
:robot_name,
|
8
10
|
:verbose,
|
9
11
|
:query,
|
10
12
|
:usage,
|
11
13
|
:usage_error
|
12
14
|
|
15
|
+
# @param argv [Array] command-line arguments
|
13
16
|
def initialize(argv)
|
14
17
|
@mode = :interpret
|
18
|
+
@plugins = true
|
15
19
|
@verbose = false
|
16
20
|
parse(argv)
|
17
21
|
@query = argv.join(" ")
|
18
22
|
end
|
19
23
|
|
20
|
-
|
24
|
+
private
|
21
25
|
|
26
|
+
# Parse the arguments supplied at the command line and set options
|
27
|
+
# accordingly.
|
28
|
+
#
|
29
|
+
# @param argv [Array] command-line arguments
|
30
|
+
# @return [void]
|
22
31
|
def parse(argv)
|
23
32
|
OptionParser.new do |opts|
|
24
33
|
@robot_name = opts.program_name.capitalize
|
25
34
|
opts.banner = "Usage: #{opts.program_name} [ options ] query..."
|
26
35
|
|
36
|
+
opts.on("-n", "--no-plugins", "Skip loading user plug-ins") do
|
37
|
+
@plugins = false
|
38
|
+
end
|
39
|
+
|
27
40
|
opts.on("-s", "--shell", "Start in REPL-mode") do
|
28
41
|
@mode = :shell
|
29
42
|
end
|
@@ -51,6 +64,5 @@ module Ego
|
|
51
64
|
end
|
52
65
|
end
|
53
66
|
end
|
54
|
-
|
55
67
|
end
|
56
68
|
end
|
data/lib/ego/plugin.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Ego
|
2
|
+
# A plug-in extends Ego with new handlers and other functionality, through a
|
3
|
+
# domain-specific language (DSL).
|
4
|
+
#
|
5
|
+
# @see Robot
|
6
|
+
class Plugin
|
7
|
+
# Manifest of all registered plug-ins
|
8
|
+
@@plugins = {}
|
9
|
+
|
10
|
+
attr_reader :name, :body, :builtin
|
11
|
+
|
12
|
+
# @param name [String] the plug-in name
|
13
|
+
# @param body the plug-in body
|
14
|
+
# @param builtin [Boolean] whether this is a built-in plug-in
|
15
|
+
def initialize(name, body, builtin: false)
|
16
|
+
@name = name
|
17
|
+
@body = body
|
18
|
+
@builtin = builtin
|
19
|
+
end
|
20
|
+
|
21
|
+
# Require all given plug-in paths
|
22
|
+
#
|
23
|
+
# @param paths [Array] absolute paths to plug-in files
|
24
|
+
# @return [void]
|
25
|
+
def self.load(paths)
|
26
|
+
paths.each { |path| require path }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Register a new plug-in
|
30
|
+
#
|
31
|
+
# @note You should use `Ego.plugin` in plug-in scripts, which sets a
|
32
|
+
# plug-in name for you automatically.
|
33
|
+
#
|
34
|
+
# @param name [String] the plug-in name
|
35
|
+
# @param body the plug-in body
|
36
|
+
# @param builtin [Boolean] whether to register as a built-in plug-in
|
37
|
+
# @return [Plugin] the instantiated plug-in
|
38
|
+
def self.register(name, body, builtin: false)
|
39
|
+
@@plugins[name] = Plugin.new(name, body, builtin: builtin)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Yield each plug-in body passing `obj` as a parameter and calling
|
43
|
+
# `obj#context=`.
|
44
|
+
#
|
45
|
+
# @param obj [Object] the object to decorate
|
46
|
+
# @return [Object] the decorated object
|
47
|
+
def self.decorate(obj)
|
48
|
+
@@plugins.each do |name, plugin|
|
49
|
+
if obj.respond_to?(:context)
|
50
|
+
obj.context = plugin
|
51
|
+
end
|
52
|
+
plugin.body.call(obj)
|
53
|
+
end
|
54
|
+
|
55
|
+
obj
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Ego.plugin builtin: true do |robot|
|
2
|
+
robot.can 'list capabilities'
|
3
|
+
|
4
|
+
robot.on(
|
5
|
+
/^(show me|show|tell me|list)\s+(handlers|what you can do|what (?:you are|you're) able to do|what you do|what (?:queries )?you (?:can )?understand)$/i => 5,
|
6
|
+
/^what (?:can you|are you able to|do you) (?:do|handle|understand)\??$/i => 5,
|
7
|
+
/^help/i => 2,
|
8
|
+
) do
|
9
|
+
say 'I can...'
|
10
|
+
|
11
|
+
@capabilities.each do |cap|
|
12
|
+
builtin = cap.plugin.builtin ? '*' : ''
|
13
|
+
plugin = sprintf('(%s%s)', cap.plugin.name, builtin).magenta
|
14
|
+
printf("- %s %s\n", cap.to_s, plugin)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
Ego.plugin builtin: true do |robot|
|
2
|
+
robot.can 'help you write extensions'
|
3
|
+
|
4
|
+
robot.on_unhandled_query do |query|
|
5
|
+
plugin_slug = query
|
6
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
7
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
8
|
+
.tr('\'', '')
|
9
|
+
.gsub(/\W+/, '_')
|
10
|
+
.gsub(/__+/, '_')
|
11
|
+
.downcase
|
12
|
+
|
13
|
+
plugin_path = Ego::Filesystem.config("plugins/#{plugin_slug}.rb")
|
14
|
+
plugin_path.sub!(/^#{ENV['HOME']}/, '~')
|
15
|
+
|
16
|
+
if $stdout.isatty
|
17
|
+
require 'shellwords'
|
18
|
+
alert %q(I don't understand "%s".), query
|
19
|
+
alert ''
|
20
|
+
alert 'If you would like to add this capability, start by running:'
|
21
|
+
alert ' %s %s > %s', $PROGRAM_NAME, query.shellescape, plugin_path
|
22
|
+
end
|
23
|
+
|
24
|
+
if verbose? || !$stdout.isatty
|
25
|
+
puts <<~EOF
|
26
|
+
Ego.plugin do |robot|
|
27
|
+
robot.can 'do something new'
|
28
|
+
|
29
|
+
robot.on(/^#{query}$/i) do |params|
|
30
|
+
alert 'Not implemented yet. Go ahead and edit #{plugin_path}.'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
EOF
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Ego.plugin builtin: true do |robot|
|
2
|
+
robot.can 'output text to the terminal'
|
3
|
+
|
4
|
+
# Provide #say, #emote, #alert, and #debug
|
5
|
+
robot.extend(Ego::Printer)
|
6
|
+
|
7
|
+
# A verbose robot doesn't suppress #debug output
|
8
|
+
robot.provide :verbose? do
|
9
|
+
@options.verbose
|
10
|
+
end
|
11
|
+
|
12
|
+
robot.can 'repeat what you say'
|
13
|
+
robot.on(/^(?:say|echo)\s+(?<input>.+)/i) do |match|
|
14
|
+
say match[:input]
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Ego.plugin builtin: true do |robot|
|
2
|
+
robot.can 'socialize'
|
3
|
+
|
4
|
+
robot.on(/^((who|what) are you|what('?s| is) your name)/i) do
|
5
|
+
say ["I'm #{name}.", "This is #{robot.name}, a robot."].sample
|
6
|
+
end
|
7
|
+
|
8
|
+
robot.on(/^(hello|salve|ave|hi|hey|ciao|hej)/i => 3) do
|
9
|
+
say [
|
10
|
+
'Hello.',
|
11
|
+
'Salve tu.',
|
12
|
+
'Ave.',
|
13
|
+
'Hi.',
|
14
|
+
'Hey.',
|
15
|
+
'Ciao.',
|
16
|
+
'Hej.',
|
17
|
+
].sample
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Ego.plugin builtin: true do |robot|
|
2
|
+
robot.can 'execute system commands'
|
3
|
+
|
4
|
+
robot.provide :system do |*args|
|
5
|
+
debug 'Running system with arguments %s.', args
|
6
|
+
|
7
|
+
unless Kernel.system(*args)
|
8
|
+
alert 'Sorry, there was a problem running %s.', args.first
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
robot.can 'tell you your login name'
|
13
|
+
|
14
|
+
robot.on(
|
15
|
+
/^what(?:'?s| is) my (?:user|login)? ?name/i => 5,
|
16
|
+
/^who am I(?: logged in as)?/i => 5,
|
17
|
+
) do
|
18
|
+
say 'You are currently logged in as:'
|
19
|
+
system 'who'
|
20
|
+
end
|
21
|
+
end
|
data/lib/ego/printer.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module Ego
|
4
|
+
# Utility methods for writing output with formatting.
|
5
|
+
module Printer
|
6
|
+
String.disable_colorization = !$stdout.isatty
|
7
|
+
|
8
|
+
# Write stylized message to `$stdout` indicating speech.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# name = 'world'
|
12
|
+
# robot.say 'Hello, %s.', name
|
13
|
+
# # => "Hello, world."
|
14
|
+
#
|
15
|
+
# @param message [Object] message to write
|
16
|
+
# @param *replacements [Object, ...] `printf`-style replacements
|
17
|
+
# @return [nil]
|
18
|
+
def say(message, *replacements)
|
19
|
+
puts sprintf(message, *replacements).bold
|
20
|
+
end
|
21
|
+
|
22
|
+
# Write stylized message to `$stdout` indicating an emote.
|
23
|
+
#
|
24
|
+
# Plug-ins may use this method to indicating what the robot is doing.
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# robot.emote 'runs away'
|
28
|
+
# # => "*runs away*"
|
29
|
+
#
|
30
|
+
# @param message [Object] message to write
|
31
|
+
# @return [nil]
|
32
|
+
def emote(message)
|
33
|
+
puts "*#{message}*".magenta
|
34
|
+
end
|
35
|
+
|
36
|
+
# Write stylized message to `$stderr` indicating an error or warning
|
37
|
+
# message.
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# name = 'world'
|
41
|
+
# robot.alert 'Hello, %s.', name
|
42
|
+
# # => "Hello, world."
|
43
|
+
#
|
44
|
+
# @param message [Object] message to write
|
45
|
+
# @param *replacements [Object, ...] `printf`-style replacements
|
46
|
+
# @return [nil]
|
47
|
+
def alert(message, *replacements)
|
48
|
+
errs sprintf(message, *replacements).light_red
|
49
|
+
end
|
50
|
+
|
51
|
+
# Write stylized message to `$stderr` indicating a debugging message
|
52
|
+
# message.
|
53
|
+
#
|
54
|
+
# Plug-ins may use this method to provide extra information when the
|
55
|
+
# `--verbose` flag is supplied at the command-line.
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# name = 'world'
|
59
|
+
# robot.alert 'Hello, %s.', name
|
60
|
+
# # => "Hello, world."
|
61
|
+
#
|
62
|
+
# @param message [Object] message to write
|
63
|
+
# @param *replacements [Object, ...] `printf`-style replacements
|
64
|
+
# @return [nil]
|
65
|
+
def debug(message, *replacements)
|
66
|
+
errs sprintf(message, *replacements) if verbose?
|
67
|
+
end
|
68
|
+
|
69
|
+
# Whether to print debugging messages. Can be overridden by classes that
|
70
|
+
# include `Printer`.
|
71
|
+
#
|
72
|
+
# @return [false] should print debugging messages?
|
73
|
+
def verbose?; false; end
|
74
|
+
|
75
|
+
module_function
|
76
|
+
|
77
|
+
# Writes the given message(s) to `$stdout`, appending a newline if not
|
78
|
+
# already included.
|
79
|
+
#
|
80
|
+
# @param *message [Object, ...] message(s) to write
|
81
|
+
# @return [nil]
|
82
|
+
def puts(*message)
|
83
|
+
$stdout.puts(*message)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Writes the given message(s) to `$stderr`, appending a newline if not
|
87
|
+
# already included.
|
88
|
+
#
|
89
|
+
# @param *message [Object, ...] message(s) to write
|
90
|
+
# @return [nil]
|
91
|
+
def errs(*message)
|
92
|
+
$stderr.puts(*message)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/ego/robot.rb
CHANGED
@@ -1,24 +1,171 @@
|
|
1
|
+
require_relative 'capability'
|
2
|
+
require_relative 'handler'
|
3
|
+
require_relative 'robot_error'
|
4
|
+
require 'hooks'
|
5
|
+
|
1
6
|
module Ego
|
7
|
+
# The robot instance provides a DSL for plug-ins to use to implement new
|
8
|
+
# functionality and also share that functionality with other plug-ins.
|
9
|
+
#
|
10
|
+
# Much built-in functionality of Ego is also implemented using the plug-in
|
11
|
+
# DSL.
|
12
|
+
#
|
13
|
+
# @see Ego.plugin
|
2
14
|
class Robot
|
3
|
-
|
15
|
+
include Hooks
|
16
|
+
include Hooks::InstanceHooks
|
17
|
+
|
18
|
+
attr_reader :name, :options, :capabilities
|
19
|
+
# Set/get currently executing plug-in
|
20
|
+
attr_accessor :context
|
21
|
+
|
22
|
+
alias_method :provide, :define_singleton_method
|
23
|
+
|
24
|
+
define_hooks :on_ready, :on_shutdown
|
25
|
+
define_hooks :before_handle_query, :after_handle_query, :on_unhandled_query
|
26
|
+
define_hooks :before_action, :after_action
|
4
27
|
|
5
|
-
|
28
|
+
# @param options [Options] the options to create a robot with
|
29
|
+
#
|
30
|
+
# @see Options
|
31
|
+
def initialize(options)
|
6
32
|
@name = options.robot_name
|
7
33
|
@options = options
|
8
|
-
@
|
34
|
+
@context = nil
|
35
|
+
@capabilities = []
|
36
|
+
@handlers = []
|
9
37
|
end
|
10
38
|
|
11
|
-
|
12
|
-
|
39
|
+
# Run `on_ready` hook.
|
40
|
+
#
|
41
|
+
# Should be called after plug-ins are registered, before handling queries.
|
42
|
+
#
|
43
|
+
# @hook on_ready
|
44
|
+
#
|
45
|
+
# @return [self]
|
46
|
+
def ready
|
47
|
+
run_hook :on_ready
|
48
|
+
self
|
13
49
|
end
|
14
50
|
|
15
|
-
|
16
|
-
|
51
|
+
# Run `on_shutdown` hook.
|
52
|
+
#
|
53
|
+
# Should be called after all queries are handled, before program
|
54
|
+
# termination.
|
55
|
+
#
|
56
|
+
# @hook on_shutdown
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
def shutdown
|
60
|
+
run_hook :on_shutdown
|
17
61
|
end
|
18
62
|
|
19
|
-
|
20
|
-
|
21
|
-
|
63
|
+
# Adds a new capability, which documents functionality added by a plug-in.
|
64
|
+
#
|
65
|
+
# @example Add a capability to the robot instance
|
66
|
+
# robot.can 'repeat what you say'
|
67
|
+
#
|
68
|
+
# @param desc [String] capability description
|
69
|
+
# @return [Array] all capabilities registered to the robot
|
70
|
+
#
|
71
|
+
# @see Capability
|
72
|
+
def can(desc)
|
73
|
+
unless @context
|
74
|
+
raise RobotError, 'Cannot add capability outside of plug-in context'
|
75
|
+
end
|
76
|
+
@capabilities << Capability.new(desc, @context)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Register a new query handler.
|
80
|
+
#
|
81
|
+
# The robot will execute the given block (the "action") when the given
|
82
|
+
# pattern (the "condition") matches. Conditions are assigned priorities,
|
83
|
+
# which determined in what order conditions are checked against the query.
|
84
|
+
#
|
85
|
+
# @example Add a handler to the robot instance
|
86
|
+
# robot.on(/^pattern/) do
|
87
|
+
# # ...
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# @example Add a handler with priority to the robot instance
|
91
|
+
# robot.on(/^pattern/, 7) do
|
92
|
+
# # ...
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# @example Add multiple handlers for the same action
|
96
|
+
# robot.on(
|
97
|
+
# /pattern/ => 6,
|
98
|
+
# /other pattern/ => 7,
|
99
|
+
# /another/ => 3,
|
100
|
+
# ) do
|
101
|
+
# # ...
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# @example Passing a lambda as a condition
|
105
|
+
# robot.on(->(query) { query.length > 10 }) do
|
106
|
+
# # ...
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# @param condition [Proc, #match] the condition that triggers the supplied action
|
110
|
+
# @param priority [Integer] the handler priority (higher number = higher priority)
|
111
|
+
# @param action [Proc] the block to be executed when condition is met
|
112
|
+
# @return [void]
|
113
|
+
#
|
114
|
+
# @see Handler#initialize
|
115
|
+
def on(condition, priority = 5, &action)
|
116
|
+
unless action
|
117
|
+
raise RobotError, "Hook requires an action: robot.on #{condition.inspect}"
|
118
|
+
end
|
119
|
+
|
120
|
+
if condition.respond_to?(:each_pair)
|
121
|
+
# Condition is a hash of conditions and priorities
|
122
|
+
condition.each_pair { |c, p| on(c, p, &action) }
|
123
|
+
else
|
124
|
+
# Register a new handler with condition
|
125
|
+
@handlers << Handler.new(condition, action, priority)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Run action with given parameters in the context of the robot instance.
|
130
|
+
#
|
131
|
+
# @hook before_action
|
132
|
+
# @hook after_action
|
133
|
+
#
|
134
|
+
# @param action [#call] the action
|
135
|
+
# @param params the action parameters
|
136
|
+
# @return result of the action
|
137
|
+
def run_action(action, params)
|
138
|
+
run_hook :before_action, action, params
|
139
|
+
result = instance_exec(params, &action)
|
140
|
+
run_hook :after_action, action, params, result
|
141
|
+
result
|
142
|
+
end
|
143
|
+
|
144
|
+
# Call `#handle` on each registered handler until a truthy value is
|
145
|
+
# returned, then run the associated action.
|
146
|
+
#
|
147
|
+
# @hook before_handle_query
|
148
|
+
# @hook after_handle_query
|
149
|
+
# @hook on_unhandled_query
|
150
|
+
#
|
151
|
+
# @param query [String] user query
|
152
|
+
# @return [false] if query is not handled
|
153
|
+
# @return result of the action
|
154
|
+
#
|
155
|
+
# @see Handler#handle
|
156
|
+
def handle(query)
|
157
|
+
run_hook :before_handle_query, query
|
158
|
+
|
159
|
+
@handlers.sort.reverse_each do |handler|
|
160
|
+
if params = handler.handle(query)
|
161
|
+
result = run_action(handler.action, params)
|
162
|
+
run_hook :after_handle_query, query, handler
|
163
|
+
return result
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
run_hook :on_unhandled_query, query
|
168
|
+
false
|
22
169
|
end
|
23
170
|
end
|
24
171
|
end
|