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.
@@ -0,0 +1,8 @@
1
+ module Ego
2
+ # Error type raised by robot plug-in DSL.
3
+ #
4
+ # `RobotError`s print their message to STDERR and halt execution of the
5
+ # program.
6
+ class RobotError < RuntimeError
7
+ end
8
+ end
data/lib/ego/runner.rb CHANGED
@@ -1,73 +1,104 @@
1
1
  require_relative '../ego'
2
+ require_relative 'options'
2
3
 
3
4
  module Ego
4
- # The Ego::Runner class, given an array of arguments, initializes the
5
- # required objects and executes the request.
5
+ # The Runner class, given an array of arguments, initializes the required
6
+ # objects and executes the request.
6
7
  class Runner
8
+ # Prompt to display in shell-mode
7
9
  PROMPT = 'ego, '.green.freeze
10
+ # Pattern that triggers shell-mode to exit
8
11
  QUIT = /^q(uit)?|exit|(good)?bye$/.freeze
9
12
 
10
13
  # Takes an array of arguments and parses them into options:
11
14
  #
15
+ # @example
12
16
  # runner = Ego::Runner.new(ARGV)
13
17
  #
18
+ # @param argv [Array] command-line arguments
14
19
  def initialize(argv)
15
20
  @options = Options.new(argv)
16
- @formatter = Ego::Formatter.new
17
21
  end
18
22
 
19
23
  # Run the appropriate action based on the arguments provided to
20
- # #initialize.
24
+ # `#initialize`.
25
+ #
26
+ # @return [void]
21
27
  def run
22
28
  case @options.mode
23
29
  when :help
24
30
  if @options.usage_error
25
- STDERR.puts @options.usage_error, "\n"
31
+ Printer.errs @options.usage_error, "\n"
26
32
  end
27
33
 
28
- @formatter.puts @options.usage
34
+ Printer.puts @options.usage
29
35
 
30
36
  exit(-1) if @options.usage_error
31
37
  when :version
32
- @formatter.puts "ego v#{Ego::VERSION}"
38
+ Printer.puts "ego v#{Ego::VERSION}"
33
39
  when :shell
34
- init_robot
35
- start_repl
40
+ start_shell(robot_factory)
36
41
  else
37
- init_robot
38
- handle_query @options.query
42
+ handle_query(robot_factory, @options.query)
39
43
  end
44
+ rescue RobotError => e
45
+ Printer.errs e.message
46
+ exit(-2)
40
47
  end
41
48
 
42
49
  protected
43
50
 
44
- def init_robot
45
- @robot = Ego::Robot.new(@options, @formatter)
46
- Ego::Handler.load Ego::Filesystem.user_handlers
47
- Ego::Handler.load Ego::Filesystem.builtin_handlers
51
+ # Get a robot and enhance it with plug-ins.
52
+ #
53
+ # @return [Robot] a decorated robot instance
54
+ def robot_factory
55
+ Plugin.load Filesystem.builtin_plugins
56
+
57
+ if @options.plugins
58
+ Plugin.load Filesystem.user_plugins
59
+ end
60
+
61
+ Plugin.decorate(Robot.new(@options)).ready
48
62
  end
49
63
 
50
- def handle_query(query)
51
- Ego::Handler.dispatch @robot, query
64
+ # Handle and single query and shut down.
65
+ #
66
+ # @param robot [Robot] the robot to handle the query
67
+ # @param query [String] the query to handle
68
+ # @return [void]
69
+ def handle_query(robot, query)
70
+ robot.handle(query)
71
+ ensure
72
+ robot.shutdown
52
73
  end
53
74
 
75
+ # Show a prompt and read input from the user.
76
+ #
77
+ # @return [String, nil] the user query
54
78
  def prompt
55
79
  Readline.readline(PROMPT, true)
56
80
  end
57
81
 
58
- def start_repl
82
+ # Start a REPL for handling multiple queries in one session.
83
+ #
84
+ # @param robot [Robot] the robot to handle the query
85
+ # @return [void]
86
+ def start_shell(robot)
59
87
  require 'readline'
60
88
 
61
- # Store the state of the terminal
89
+ # Save the state of the terminal
62
90
  stty_save = `stty -g`.chomp
63
91
 
64
92
  loop do
65
93
  query = prompt
66
94
  break if query.nil? || query.strip =~ QUIT
67
- handle_query query.strip
95
+ robot.handle(query.strip)
68
96
  end
69
97
  rescue Interrupt => e
70
98
  system('stty', stty_save) # Restore state
99
+ robot.say e.message
100
+ ensure
101
+ robot.shutdown
71
102
  end
72
103
  end
73
104
  end
data/lib/ego/version.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  module Ego
2
- VERSION = '0.3.0'
2
+ # Gem version
3
+ VERSION = '0.4.0'
3
4
  end
@@ -0,0 +1,23 @@
1
+ require 'ego/capability'
2
+
3
+ RSpec.describe Ego::Capability do
4
+ let(:desc) { 'my desc' }
5
+ let(:plugin) { double('Ego::Plugin') }
6
+ subject { described_class.new(desc, plugin) }
7
+
8
+ context 'on initization' do
9
+ it 'sets its description' do
10
+ expect(subject.desc).to eq(desc)
11
+ end
12
+
13
+ it 'sets its plug-in' do
14
+ expect(subject.plugin).to be plugin
15
+ end
16
+ end
17
+
18
+ describe '#to_s' do
19
+ it 'returns the description' do
20
+ expect(subject.to_s).to eq(desc)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ require 'ego/handler'
2
+
3
+ RSpec.describe Ego::Handler do
4
+ let(:condition) { ->(q) { 'bar' if q == 'foo' } }
5
+ let(:regexp) { /^baz/i }
6
+ let(:action) { ->(p) { puts p } }
7
+ let(:priority) { 2 }
8
+ subject { described_class.new(condition, action, priority) }
9
+
10
+ context 'on initization' do
11
+ it 'sets its condition' do
12
+ expect(subject.condition).to be condition
13
+ end
14
+
15
+ it 'sets its action' do
16
+ expect(subject.action).to be action
17
+ end
18
+
19
+ it 'sets its priority' do
20
+ expect(subject.priority).to be priority
21
+ end
22
+
23
+ it 'defaults its priority to 5' do
24
+ default_handler = described_class.new(condition, action)
25
+ expect(default_handler.priority).to eq(5)
26
+ end
27
+
28
+ describe 'given something matchable as the condition' do
29
+ it 'encloses it' do
30
+ expect(regexp.respond_to?(:match)).to be true
31
+ expect(regexp.respond_to?(:call)).to be false
32
+ regexp_handler = described_class.new(regexp, action, priority)
33
+ expect(regexp_handler.condition.respond_to?(:match)).to be false
34
+ expect(regexp_handler.condition.respond_to?(:call)).to be true
35
+ end
36
+ end
37
+ end
38
+
39
+ it 'is comparable' do
40
+ expect(subject.respond_to?(:'<=>')).to be true
41
+ end
42
+
43
+ it 'compares priorities' do
44
+ handler1 = described_class.new(condition, action, 1)
45
+ handler2 = described_class.new(condition, action, 2)
46
+ expect(handler2).to be > handler1
47
+ expect(handler1).to be < handler2
48
+ end
49
+
50
+ describe '#handle' do
51
+ context 'when the query cannot be handled' do
52
+ it 'returns false' do
53
+ expect(subject.handle('fail')).to be false
54
+ end
55
+ end
56
+
57
+ context 'when the query can be handled' do
58
+ it 'returns the result of the condition closure' do
59
+ expect(subject.handle('foo')).to eq('bar')
60
+ end
61
+ end
62
+ end
63
+ end
@@ -2,68 +2,78 @@ require 'ego/options'
2
2
 
3
3
  RSpec.describe Ego::Options do
4
4
  it 'defaults to interpret-mode' do
5
- opts = Ego::Options.new(['foo'])
5
+ opts = described_class.new(['foo'])
6
6
  expect(opts.mode).to eq(:interpret)
7
7
  end
8
8
 
9
9
  it 'can be set to shell-mode' do
10
- opts = Ego::Options.new(['-s'])
10
+ opts = described_class.new(['-s'])
11
11
  expect(opts.mode).to eq(:shell)
12
12
  end
13
13
 
14
14
  it 'can be set to version-mode' do
15
- opts = Ego::Options.new(['-v'])
15
+ opts = described_class.new(['-v'])
16
16
  expect(opts.mode).to eq(:version)
17
17
  end
18
18
 
19
19
  it 'can be set to help-mode' do
20
- opts = Ego::Options.new(['-h'])
20
+ opts = described_class.new(['-h'])
21
21
  expect(opts.mode).to eq(:help)
22
22
  end
23
23
 
24
24
  it 'defaults to help-mode with no arguments' do
25
- opts = Ego::Options.new([])
25
+ opts = described_class.new([])
26
26
  expect(opts.mode).to eq(:help)
27
27
  end
28
28
 
29
+ it 'defaults to loading user plug-ins' do
30
+ opts = described_class.new(['foo'])
31
+ expect(opts.plugins).to be true
32
+ end
33
+
34
+ it 'can be set to not load user plug-ins' do
35
+ opts = described_class.new(['-n', 'foo'])
36
+ expect(opts.plugins).to be false
37
+ end
38
+
29
39
  it 'defaults to not verbose' do
30
- opts = Ego::Options.new(['foo'])
40
+ opts = described_class.new(['foo'])
31
41
  expect(opts.verbose).to be false
32
42
  end
33
43
 
34
44
  it 'can be set to verbose' do
35
- opts = Ego::Options.new(['-V', 'foo'])
45
+ opts = described_class.new(['-V', 'foo'])
36
46
  expect(opts.verbose).to be true
37
47
  end
38
48
 
39
49
  it 'sets the query to remaining args join with spaces' do
40
- opts = Ego::Options.new(['foo', 'bar'])
50
+ opts = described_class.new(['foo', 'bar'])
41
51
  expect(opts.query).to eq('foo bar')
42
52
  end
43
53
 
44
54
  it 'sets the robot name' do
45
- opts = Ego::Options.new(['foo'])
55
+ opts = described_class.new(['foo'])
46
56
  expect(opts.robot_name).to_not be_nil
47
57
  end
48
58
 
49
59
  it 'sets the usage' do
50
- opts = Ego::Options.new(['foo'])
60
+ opts = described_class.new(['foo'])
51
61
  expect(opts.usage).to be_instance_of OptionParser
52
62
  end
53
63
 
54
64
  it 'does not set a usage error normally' do
55
- opts = Ego::Options.new(['foo'])
65
+ opts = described_class.new(['foo'])
56
66
  expect(opts.usage_error).to be_nil
57
67
  end
58
68
 
59
69
  context 'with invalid option' do
60
70
  it 'sets a usage error message' do
61
- opts = Ego::Options.new(['--does-not-exist'])
71
+ opts = described_class.new(['--does-not-exist'])
62
72
  expect(opts.usage_error).to eq('invalid option: --does-not-exist')
63
73
  end
64
74
 
65
75
  it 'sets help-mode' do
66
- opts = Ego::Options.new(['--does-not-exist'])
76
+ opts = described_class.new(['--does-not-exist'])
67
77
  expect(opts.mode).to eq(:help)
68
78
  end
69
79
  end
@@ -0,0 +1,68 @@
1
+ require 'ego/plugin'
2
+
3
+ RSpec.describe Ego::Plugin do
4
+ let(:name) { 'my_plug' }
5
+ let(:body) { proc { true } }
6
+ let(:plugin) { described_class.new(name, body) }
7
+ let(:builtin_plugin) { described_class.new(name, body, builtin: true) }
8
+
9
+ context 'on initization' do
10
+ it 'sets its name' do
11
+ expect(plugin.name).to eq(name)
12
+ end
13
+
14
+ it 'sets its body' do
15
+ expect(plugin.body).to be body
16
+ end
17
+
18
+ it 'sets its builtin flag' do
19
+ expect(builtin_plugin.builtin).to be true
20
+ end
21
+
22
+ it 'defaults its builtin flag to false' do
23
+ expect(plugin.builtin).to be false
24
+ end
25
+ end
26
+
27
+ describe '.register' do
28
+ it 'returns a new plugin' do
29
+ expect(described_class.register(name, body)).to be_instance_of(described_class)
30
+ end
31
+ end
32
+
33
+
34
+ describe '.decorate' do
35
+ let(:obj) do
36
+ Class.new { attr_accessor :context, :a, :b }.new
37
+ end
38
+
39
+ before do
40
+ described_class.class_variable_set :@@plugins, {}
41
+ described_class.register('a', proc { |obj|
42
+ obj.a = 'foo'
43
+ })
44
+ described_class.register('b', proc { |obj|
45
+ obj.b = 'bar'
46
+ })
47
+ end
48
+
49
+ it 'sets obj#context to each registered plugin' do
50
+ expect(obj).to receive(:context=).twice
51
+ described_class.decorate(obj)
52
+ end
53
+
54
+ it 'calls each plugin body passing the obj' do
55
+ expect(obj).to receive_messages({
56
+ :a= => 'foo',
57
+ :b= => 'bar',
58
+ })
59
+ described_class.decorate(obj)
60
+ end
61
+
62
+ it 'returns the decorated obj' do
63
+ decorated = described_class.decorate(obj)
64
+ expect(decorated.a).to eq('foo')
65
+ expect(decorated.b).to eq('bar')
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,120 @@
1
+ require 'ego/printer'
2
+
3
+ module Ego
4
+ RSpec.describe Printer do
5
+ let(:verbose_printer) { (Class.new { include Printer; def verbose?; true; end }).new }
6
+ let(:printer) { (Class.new { include Printer }).new }
7
+
8
+ describe '#puts' do
9
+ it 'is callable on the module' do
10
+ expect(described_class.respond_to?(:puts)).to be true
11
+ end
12
+
13
+ it 'is private on the instance' do
14
+ expect(printer.respond_to?(:puts)).to be false
15
+ end
16
+
17
+ it 'prints to STDOUT' do
18
+ expect { described_class.puts 'bar' }.to output("bar\n").to_stdout
19
+ end
20
+
21
+ it 'concatenates multiple arguments' do
22
+ expect { described_class.puts 'bar', 'baz' }.to output("bar\nbaz\n").to_stdout
23
+ end
24
+ end
25
+
26
+ describe '#errs' do
27
+ it 'is callable on the module' do
28
+ expect(described_class.respond_to?(:errs)).to be true
29
+ end
30
+
31
+ it 'is private on the instance' do
32
+ expect(printer.respond_to?(:errs)).to be false
33
+ end
34
+
35
+ it 'prints to STDERR' do
36
+ expect { described_class.errs 'bar' }.to output("bar\n").to_stderr
37
+ end
38
+
39
+ it 'concatenates multiple arguments' do
40
+ expect { described_class.errs 'bar', 'baz' }.to output("bar\nbaz\n").to_stderr
41
+ end
42
+ end
43
+
44
+ describe '#say' do
45
+ it 'is not callable on the module' do
46
+ expect(described_class.respond_to?(:say)).to be false
47
+ end
48
+
49
+ it 'is callable on the instance' do
50
+ expect(printer.respond_to?(:say)).to be true
51
+ end
52
+
53
+ it 'prints the message to STDOUT bolded with newline' do
54
+ expect { printer.say('X') }.to output("\e[1;39;49mX\e[0m\n").to_stdout
55
+ end
56
+
57
+ it 'accepts placeholders and replacement strings' do
58
+ expect { printer.say('Hello, %s.', 'world') }.to output("\e[1;39;49mHello, world.\e[0m\n").to_stdout
59
+ end
60
+ end
61
+
62
+ describe '#emote' do
63
+ it 'is not callable on the module' do
64
+ expect(described_class.respond_to?(:emote)).to be false
65
+ end
66
+
67
+ it 'is callable on the instance' do
68
+ expect(printer.respond_to?(:emote)).to be true
69
+ end
70
+
71
+ it 'prints the message to STDOUT with color and asterisks' do
72
+ expect { printer.emote('FOO') }.to output("\e[0;35;49m*FOO*\e[0m\n").to_stdout
73
+ end
74
+ end
75
+
76
+ describe '#alert' do
77
+ it 'is not callable on the module' do
78
+ expect(described_class.respond_to?(:alert)).to be false
79
+ end
80
+
81
+ it 'is callable on the instance' do
82
+ expect(printer.respond_to?(:alert)).to be true
83
+ end
84
+
85
+ it 'prints the message to STDERR with color and newline' do
86
+ expect { printer.alert('FOO') }.to output("\e[0;91;49mFOO\e[0m\n").to_stderr
87
+ end
88
+
89
+ it 'accepts placeholders and replacement strings' do
90
+ expect { printer.alert('Error: %s.', 'foo') }.to output("\e[0;91;49mError: foo.\e[0m\n").to_stderr
91
+ end
92
+ end
93
+
94
+ describe '#debug' do
95
+ it 'is not callable on the module' do
96
+ expect(described_class.respond_to?(:debug)).to be false
97
+ end
98
+
99
+ it 'is callable on the instance' do
100
+ expect(printer.respond_to?(:debug)).to be true
101
+ end
102
+
103
+ context 'in verbose instance' do
104
+ it 'prints the message to STDERR with newline' do
105
+ expect { verbose_printer.debug('FOO') }.to output("FOO\n").to_stderr
106
+ end
107
+
108
+ it 'accepts placeholders and replacement strings' do
109
+ expect { verbose_printer.debug('Hello, %s.', 'world') }.to output("Hello, world.\n").to_stderr
110
+ end
111
+ end
112
+
113
+ context 'in default instance' do
114
+ it 'does not print anything' do
115
+ expect { printer.debug('FOO') }.not_to output.to_stderr
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end