ruboty 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG.md +73 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +67 -0
  8. data/Rakefile +5 -0
  9. data/bin/ruboty +6 -0
  10. data/images/screenshot.png +0 -0
  11. data/lib/ruboty/action.rb +48 -0
  12. data/lib/ruboty/actions/base.rb +11 -0
  13. data/lib/ruboty/actions/help.rb +27 -0
  14. data/lib/ruboty/actions/ping.rb +19 -0
  15. data/lib/ruboty/actions/whoami.rb +9 -0
  16. data/lib/ruboty/adapter_builder.rb +23 -0
  17. data/lib/ruboty/adapters/base.rb +21 -0
  18. data/lib/ruboty/adapters/shell.rb +82 -0
  19. data/lib/ruboty/brains/base.rb +25 -0
  20. data/lib/ruboty/brains/memory.rb +9 -0
  21. data/lib/ruboty/command_builder.rb +30 -0
  22. data/lib/ruboty/commands/base.rb +11 -0
  23. data/lib/ruboty/commands/generate.rb +33 -0
  24. data/lib/ruboty/commands/run.rb +9 -0
  25. data/lib/ruboty/env/missing_required_key_error.rb +6 -0
  26. data/lib/ruboty/env/validatable.rb +33 -0
  27. data/lib/ruboty/env/validation_error.rb +6 -0
  28. data/lib/ruboty/env.rb +33 -0
  29. data/lib/ruboty/handlers/base.rb +37 -0
  30. data/lib/ruboty/handlers/help.rb +11 -0
  31. data/lib/ruboty/handlers/ping.rb +11 -0
  32. data/lib/ruboty/handlers/whoami.rb +15 -0
  33. data/lib/ruboty/logger.rb +10 -0
  34. data/lib/ruboty/message.rb +47 -0
  35. data/lib/ruboty/robot.rb +76 -0
  36. data/lib/ruboty/version.rb +3 -0
  37. data/lib/ruboty.rb +58 -0
  38. data/ruboty.gemspec +29 -0
  39. data/spec/ruboty/adapter_builder_spec.rb +36 -0
  40. data/spec/ruboty/adapters/shell_spec.rb +56 -0
  41. data/spec/ruboty/commands/generate_spec.rb +39 -0
  42. data/spec/ruboty/commands/run_spec.rb +22 -0
  43. data/spec/ruboty/env/validatable_spec.rb +48 -0
  44. data/spec/ruboty/handlers/base_spec.rb +28 -0
  45. data/spec/ruboty/handlers/help_spec.rb +41 -0
  46. data/spec/ruboty/handlers/ping_spec.rb +40 -0
  47. data/spec/ruboty/handlers/whoami_spec.rb +36 -0
  48. data/spec/ruboty/robot_spec.rb +38 -0
  49. data/spec/spec_helper.rb +16 -0
  50. data/templates/Gemfile +3 -0
  51. metadata +246 -0
@@ -0,0 +1,37 @@
1
+ module Ruboty
2
+ module Handlers
3
+ class Base
4
+ class << self
5
+ include Mem
6
+
7
+ def inherited(child)
8
+ Ruboty.handlers << child
9
+ end
10
+
11
+ def on(pattern, options = {})
12
+ actions << Action.new(pattern, options)
13
+ end
14
+
15
+ def actions
16
+ []
17
+ end
18
+ memoize :actions
19
+ end
20
+
21
+ include Env::Validatable
22
+
23
+ attr_reader :robot
24
+
25
+ def initialize(robot)
26
+ @robot = robot
27
+ validate!
28
+ end
29
+
30
+ def call(message)
31
+ self.class.actions.each do |action|
32
+ action.call(self, message)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,11 @@
1
+ module Ruboty
2
+ module Handlers
3
+ class Help < Base
4
+ on /help( me)?\z/i, name: "help", description: "Show this help message"
5
+
6
+ def help(message)
7
+ Ruboty::Actions::Help.new(message).call
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Ruboty
2
+ module Handlers
3
+ class Ping < Base
4
+ on /ping\z/i, name: "ping", description: "Return PONG to PING"
5
+
6
+ def ping(message)
7
+ Ruboty::Actions::Ping.new(message).call
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module Ruboty
2
+ module Handlers
3
+ class Whoami < Base
4
+ on(
5
+ /who am i\?/i,
6
+ name: "whoami",
7
+ description: "Answer who you are",
8
+ )
9
+
10
+ def whoami(message)
11
+ Ruboty::Actions::Whoami.new(message).call
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ require "logger"
2
+
3
+ module Ruboty
4
+ class Logger < Logger
5
+ def initialize(*)
6
+ super
7
+ @formatter = ->(severity, time, name, message) { "#{message}\n" }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,47 @@
1
+ module Ruboty
2
+ class Message
3
+ attr_reader(
4
+ :match_data,
5
+ :original,
6
+ )
7
+
8
+ def initialize(original)
9
+ @original = original
10
+ end
11
+
12
+ def body
13
+ @original[:body]
14
+ end
15
+
16
+ def from
17
+ @original[:from]
18
+ end
19
+
20
+ # @note Some chat service such as XMPP has 2 types of address, address and username
21
+ # @return [String] User identifier in the Chat service
22
+ def from_name
23
+ @original[:from_name] || from
24
+ end
25
+
26
+ def robot
27
+ @original[:robot]
28
+ end
29
+
30
+ def to
31
+ @original[:to]
32
+ end
33
+
34
+ def match(pattern)
35
+ @match_data = pattern.match(body)
36
+ end
37
+
38
+ def [](index)
39
+ match_data[index]
40
+ end
41
+
42
+ def reply(body, options = {})
43
+ attributes = { body: body, from: to, to: from, original: original }.merge(options)
44
+ robot.say(attributes)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,76 @@
1
+ module Ruboty
2
+ class Robot
3
+ DEFAULT_ROBOT_NAME = "ruboty"
4
+
5
+ include Mem
6
+
7
+ delegate :say, to: :adapter
8
+
9
+ attr_reader :options
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+ end
14
+
15
+ def run
16
+ dotenv
17
+ bundle
18
+ setup
19
+ remember
20
+ handle
21
+ adapt
22
+ end
23
+
24
+ def receive(attributes)
25
+ message = Message.new(attributes.merge(robot: self))
26
+ handlers.each do |handler|
27
+ handler.call(message)
28
+ end
29
+ end
30
+
31
+ def name
32
+ ENV["ROBOT_NAME"] || DEFAULT_ROBOT_NAME
33
+ end
34
+
35
+ def brain
36
+ Brains::Base.find_class.new
37
+ end
38
+ memoize :brain
39
+
40
+ private
41
+
42
+ def adapt
43
+ adapter.run
44
+ end
45
+
46
+ def adapter
47
+ AdapterBuilder.new(self).build
48
+ end
49
+ memoize :adapter
50
+
51
+ def bundle
52
+ Bundler.require
53
+ end
54
+
55
+ def dotenv
56
+ Dotenv.load if options[:dotenv]
57
+ end
58
+
59
+ def setup
60
+ load(options[:load]) if options[:load]
61
+ end
62
+
63
+ def handlers
64
+ Ruboty.handlers.map {|handler_class| handler_class.new(self) }
65
+ end
66
+ memoize :handlers
67
+
68
+ def remember
69
+ brain
70
+ end
71
+
72
+ def handle
73
+ handlers
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module Ruboty
2
+ VERSION = "1.0.0"
3
+ end
data/lib/ruboty.rb ADDED
@@ -0,0 +1,58 @@
1
+ require "active_support/concern"
2
+ require "active_support/core_ext/module/delegation"
3
+ require "active_support/core_ext/string/inflections"
4
+ require "bundler"
5
+ require "dotenv"
6
+ require "mem"
7
+ require "slop"
8
+
9
+ module Ruboty
10
+ class << self
11
+ include Mem
12
+
13
+ def logger
14
+ @logger ||= Ruboty::Logger.new($stdout)
15
+ end
16
+
17
+ def die(message)
18
+ logger.error("Error: #{message}")
19
+ exit(1)
20
+ end
21
+
22
+ def handlers
23
+ []
24
+ end
25
+ memoize :handlers
26
+
27
+ def actions
28
+ handlers.map(&:actions).flatten.sort_by(&:all?)
29
+ end
30
+ end
31
+ end
32
+
33
+ require "ruboty/action"
34
+ require "ruboty/actions/base"
35
+ require "ruboty/actions/help"
36
+ require "ruboty/actions/ping"
37
+ require "ruboty/actions/whoami"
38
+ require "ruboty/adapter_builder"
39
+ require "ruboty/env"
40
+ require "ruboty/env/missing_required_key_error"
41
+ require "ruboty/env/validatable"
42
+ require "ruboty/env/validation_error"
43
+ require "ruboty/adapters/base"
44
+ require "ruboty/adapters/shell"
45
+ require "ruboty/brains/base"
46
+ require "ruboty/brains/memory"
47
+ require "ruboty/command_builder"
48
+ require "ruboty/commands/base"
49
+ require "ruboty/commands/generate"
50
+ require "ruboty/commands/run"
51
+ require "ruboty/handlers/base"
52
+ require "ruboty/handlers/help"
53
+ require "ruboty/handlers/ping"
54
+ require "ruboty/handlers/whoami"
55
+ require "ruboty/logger"
56
+ require "ruboty/message"
57
+ require "ruboty/robot"
58
+ require "ruboty/version"
data/ruboty.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "ruboty/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ruboty"
7
+ spec.version = Ruboty::VERSION
8
+ spec.authors = ["Ryo Nakamura"]
9
+ spec.email = ["r7kamura@gmail.com"]
10
+ spec.summary = "Ruby + Bot = Ruboty"
11
+ spec.homepage = "https://github.com/r7kamura/ruboty"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files -z`.split("\x0")
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_dependency "activesupport"
20
+ spec.add_dependency "bundler"
21
+ spec.add_dependency "dotenv"
22
+ spec.add_dependency "mem"
23
+ spec.add_dependency "slop"
24
+ spec.add_development_dependency "codeclimate-test-reporter", ">= 0.3.0"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec", "2.14.1"
28
+ spec.add_development_dependency "simplecov"
29
+ end
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::AdapterBuilder do
4
+ let(:builder) do
5
+ described_class.new(robot)
6
+ end
7
+
8
+ let(:robot) do
9
+ Ruboty::Robot.new
10
+ end
11
+
12
+ describe "#build" do
13
+ context "with no other adapter class definition" do
14
+ it "returns a Ruboty::Adapters::Shell as a default adapter" do
15
+ builder.build.should be_a Ruboty::Adapters::Shell
16
+ end
17
+ end
18
+
19
+ context "with another adapter class definition" do
20
+ after do
21
+ Ruboty::AdapterBuilder.adapter_classes.pop
22
+ end
23
+
24
+ let!(:another_adapter_class) do
25
+ Class.new(Ruboty::Adapters::Base) do
26
+ def run
27
+ end
28
+ end
29
+ end
30
+
31
+ it "returns an instance of that adapter class" do
32
+ builder.build.should be_a another_adapter_class
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,56 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::Adapters::Shell do
4
+ before do
5
+ Ruboty.logger.stub(:info)
6
+ end
7
+
8
+ let(:adapter) do
9
+ described_class.new(robot)
10
+ end
11
+
12
+ let(:robot) do
13
+ Ruboty::Robot.new
14
+ end
15
+
16
+ describe "#run" do
17
+ context "with `exit`" do
18
+ it "stops" do
19
+ Readline.stub(readline: "exit")
20
+ adapter.should_receive(:stop).and_call_original
21
+ adapter.run
22
+ end
23
+ end
24
+
25
+ context "with `quit`" do
26
+ it "stops" do
27
+ Readline.stub(readline: "exit")
28
+ adapter.should_receive(:stop).and_call_original
29
+ adapter.run
30
+ end
31
+ end
32
+
33
+ context "with Inturrupt from console" do
34
+ it "stops" do
35
+ Readline.stub(:readline).and_raise(Interrupt)
36
+ adapter.should_receive(:stop).and_call_original
37
+ adapter.run
38
+ end
39
+ end
40
+
41
+ context "without `exit` nor `quit`" do
42
+ it "passes given message to robot" do
43
+ Readline.stub(:readline).and_return("a", "exit")
44
+ robot.should_receive(:receive).with(body: "a", source: described_class::SOURCE)
45
+ adapter.run
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "#say" do
51
+ it "shows given message body on stdout" do
52
+ Ruboty.logger.should_receive(:info).with("a")
53
+ adapter.say(body: "a")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,39 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::Commands::Generate do
4
+ describe "#call" do
5
+ after do
6
+ FileUtils.rmtree("./ruboty/")
7
+ end
8
+
9
+ let(:command) do
10
+ Ruboty::CommandBuilder.new(arguments).build
11
+ end
12
+
13
+ let(:call) do
14
+ command.call
15
+ end
16
+
17
+ let(:arguments) do
18
+ ["--generate"]
19
+ end
20
+
21
+ context "with normal condition" do
22
+ it "generates ./ruboty/ directory from our templates" do
23
+ call
24
+ File.exists?("./ruboty/").should == true
25
+ end
26
+ end
27
+
28
+ context "when ./ruboty/ directory already exists" do
29
+ before do
30
+ FileUtils.mkdir("./ruboty/")
31
+ end
32
+
33
+ it "exits process with dying message" do
34
+ Ruboty.logger.should_receive(:error).with("Error: ./ruboty/ already exists.")
35
+ expect { call }.to raise_error(SystemExit)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::Commands::Run do
4
+ describe "#call" do
5
+ let(:command) do
6
+ Ruboty::CommandBuilder.new(arguments).build
7
+ end
8
+
9
+ let(:call) do
10
+ command.call
11
+ end
12
+
13
+ let(:arguments) do
14
+ []
15
+ end
16
+
17
+ it "creates an adapter and calls .run to it" do
18
+ Ruboty::Adapters::Shell.any_instance.should_receive(:run)
19
+ call
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,48 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::Env::Validatable do
4
+ let(:validatable_class) do
5
+ Class.new do
6
+ include Ruboty::Env::Validatable
7
+
8
+ env :A, "description of A"
9
+ env :B, "description of B", optional: true
10
+ end
11
+ end
12
+
13
+ let(:instance) do
14
+ validatable_class.new
15
+ end
16
+
17
+ describe "#validate" do
18
+ context "without required ENV" do
19
+ it "raises Ruboty::Env::ValidationError" do
20
+ expect { instance.validate }.to raise_error(Ruboty::Env::ValidationError)
21
+ end
22
+ end
23
+
24
+ context "without optional ENV" do
25
+ before do
26
+ ENV["A"] = "dummy"
27
+ end
28
+
29
+ after do
30
+ ENV["A"] = nil
31
+ end
32
+
33
+ it "does nothing" do
34
+ expect { instance.validate }.not_to raise_error
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "#validate!" do
40
+ context "without required ENV" do
41
+ it "dies with usage as erorr message" do
42
+ Ruboty.logger.should_receive(:error).with(/description of A/)
43
+ Ruboty.should_receive(:exit)
44
+ instance.validate!
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,28 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::Handlers::Base do
4
+ after do
5
+ Ruboty.handlers.pop
6
+ end
7
+
8
+ let(:robot) do
9
+ Ruboty::Robot.new
10
+ end
11
+
12
+ let!(:handler_class) do
13
+ Class.new(described_class) do
14
+ on(/(\d+) \+ (\d+)/, name: "addition", all: true)
15
+
16
+ def addition(message)
17
+ robot.say(message[1].to_i + message[2].to_i)
18
+ end
19
+ end
20
+ end
21
+
22
+ describe ".on" do
23
+ it "registers an action to the handler" do
24
+ robot.should_receive(:say).with(2)
25
+ robot.receive(body: "1 + 1")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::Handlers::Help do
4
+ let(:robot) do
5
+ Ruboty::Robot.new
6
+ end
7
+
8
+ describe "#help" do
9
+ let(:from) do
10
+ "alice"
11
+ end
12
+
13
+ let(:to) do
14
+ "#general"
15
+ end
16
+
17
+ let(:body) do
18
+ <<-EOS.strip_heredoc.strip
19
+ ruboty /help( me)?\\z/i - Show this help message
20
+ ruboty /ping\\z/i - Return PONG to PING
21
+ ruboty /who am i\\?/i - Answer who you are
22
+ EOS
23
+ end
24
+
25
+ it "responds to `@ruboty help` and says each handler's usage" do
26
+ robot.should_receive(:say).with(
27
+ body: body,
28
+ code: true,
29
+ from: to,
30
+ to: from,
31
+ original: {
32
+ body: "@ruboty help",
33
+ from: from,
34
+ robot: robot,
35
+ to: to,
36
+ },
37
+ )
38
+ robot.receive(body: "@ruboty help", from: from, to: to)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::Handlers::Ping do
4
+ let(:robot) do
5
+ Ruboty::Robot.new
6
+ end
7
+
8
+ describe "#ping" do
9
+ let(:from) do
10
+ "alice"
11
+ end
12
+
13
+ let(:to) do
14
+ "#general"
15
+ end
16
+
17
+ let(:said) do
18
+ "@ruboty ping"
19
+ end
20
+
21
+ let(:replied) do
22
+ "pong"
23
+ end
24
+
25
+ it "returns PONG to PING" do
26
+ robot.should_receive(:say).with(
27
+ body: replied,
28
+ from: to,
29
+ to: from,
30
+ original: {
31
+ body: said,
32
+ from: from,
33
+ robot: robot,
34
+ to: to,
35
+ },
36
+ )
37
+ robot.receive(body: said, from: from, to: to)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::Handlers::Whoami do
4
+ let(:robot) do
5
+ Ruboty::Robot.new
6
+ end
7
+
8
+ describe "#ping" do
9
+ let(:from) do
10
+ "alice"
11
+ end
12
+
13
+ let(:to) do
14
+ "#general"
15
+ end
16
+
17
+ let(:said) do
18
+ "@ruboty Who am I?"
19
+ end
20
+
21
+ it "returns PONG to PING" do
22
+ robot.should_receive(:say).with(
23
+ body: from,
24
+ from: to,
25
+ to: from,
26
+ original: {
27
+ body: said,
28
+ from: from,
29
+ robot: robot,
30
+ to: to,
31
+ },
32
+ )
33
+ robot.receive(body: said, from: from, to: to)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ describe Ruboty::Robot do
4
+ let(:instance) do
5
+ described_class.new(options)
6
+ end
7
+
8
+ let(:options) do
9
+ {}
10
+ end
11
+
12
+ describe "#brain" do
13
+ context "without any Brain class" do
14
+ it "returns a Ruboty::Brains::Memory" do
15
+ instance.brain.should be_a Ruboty::Brains::Memory
16
+ end
17
+
18
+ it "can be used as a Hash object" do
19
+ instance.brain.data["a"] = 1
20
+ instance.brain.data["a"].should == 1
21
+ end
22
+ end
23
+
24
+ context "with another Brain class" do
25
+ after do
26
+ Ruboty::Brains::Base.brain_classes.pop
27
+ end
28
+
29
+ let!(:another_brain_class) do
30
+ Class.new(Ruboty::Brains::Base)
31
+ end
32
+
33
+ it "returns its instance as a Brain" do
34
+ instance.brain.should be_a another_brain_class
35
+ end
36
+ end
37
+ end
38
+ end