ruboty 1.0.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.
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