slack-ruby-bot 0.10.1 → 0.10.2

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/CHANGELOG.md +5 -0
  4. data/README.md +131 -2
  5. data/TUTORIAL.md +3 -1
  6. data/examples/inventory/Gemfile +5 -0
  7. data/examples/inventory/Procfile +1 -0
  8. data/examples/inventory/inventorybot.rb +248 -0
  9. data/lib/slack-ruby-bot.rb +1 -0
  10. data/lib/slack-ruby-bot/commands/base.rb +1 -1
  11. data/lib/slack-ruby-bot/mvc.rb +1 -0
  12. data/lib/slack-ruby-bot/mvc/controller/base.rb +111 -0
  13. data/lib/slack-ruby-bot/mvc/model/base.rb +25 -0
  14. data/lib/slack-ruby-bot/mvc/mvc.rb +5 -0
  15. data/lib/slack-ruby-bot/mvc/view/base.rb +28 -0
  16. data/lib/slack-ruby-bot/version.rb +1 -1
  17. data/spec/slack-ruby-bot/app_spec.rb +0 -2
  18. data/spec/slack-ruby-bot/client_spec.rb +0 -2
  19. data/spec/slack-ruby-bot/commands/about_spec.rb +0 -2
  20. data/spec/slack-ruby-bot/commands/aliases_spec.rb +0 -2
  21. data/spec/slack-ruby-bot/commands/bot_message_spec.rb +0 -2
  22. data/spec/slack-ruby-bot/commands/bot_spec.rb +0 -2
  23. data/spec/slack-ruby-bot/commands/commands_command_classes_spec.rb +7 -2
  24. data/spec/slack-ruby-bot/commands/commands_precedence_spec.rb +0 -2
  25. data/spec/slack-ruby-bot/commands/commands_regexp_escape_spec.rb +0 -2
  26. data/spec/slack-ruby-bot/commands/commands_spaces_spec.rb +0 -2
  27. data/spec/slack-ruby-bot/commands/commands_spec.rb +0 -2
  28. data/spec/slack-ruby-bot/commands/commands_with_block_spec.rb +0 -2
  29. data/spec/slack-ruby-bot/commands/commands_with_expression_spec.rb +0 -2
  30. data/spec/slack-ruby-bot/commands/direct_messages_spec.rb +0 -2
  31. data/spec/slack-ruby-bot/commands/empty_text_spec.rb +0 -2
  32. data/spec/slack-ruby-bot/commands/help/attrs_spec.rb +0 -2
  33. data/spec/slack-ruby-bot/commands/help_spec.rb +0 -2
  34. data/spec/slack-ruby-bot/commands/hi_spec.rb +0 -2
  35. data/spec/slack-ruby-bot/commands/match_spec.rb +0 -2
  36. data/spec/slack-ruby-bot/commands/message_loop_spec.rb +0 -2
  37. data/spec/slack-ruby-bot/commands/nil_message_spec.rb +0 -2
  38. data/spec/slack-ruby-bot/commands/not_implemented_spec.rb +0 -2
  39. data/spec/slack-ruby-bot/commands/operators_spec.rb +0 -2
  40. data/spec/slack-ruby-bot/commands/operators_with_block_spec.rb +0 -2
  41. data/spec/slack-ruby-bot/commands/scan_spec.rb +0 -2
  42. data/spec/slack-ruby-bot/commands/send_gif_spec.rb +0 -2
  43. data/spec/slack-ruby-bot/commands/send_message_spec.rb +0 -2
  44. data/spec/slack-ruby-bot/commands/send_message_with_gif_spec.rb +0 -2
  45. data/spec/slack-ruby-bot/commands/unknown_spec.rb +0 -2
  46. data/spec/slack-ruby-bot/config_spec.rb +0 -2
  47. data/spec/slack-ruby-bot/hooks/hook_support_spec.rb +0 -2
  48. data/spec/slack-ruby-bot/hooks/message_spec.rb +0 -2
  49. data/spec/slack-ruby-bot/hooks/set_spec.rb +0 -2
  50. data/spec/slack-ruby-bot/mvc/controller/controller_to_command_spec.rb +97 -0
  51. data/spec/slack-ruby-bot/rspec/respond_with_error_spec.rb +0 -2
  52. data/spec/slack-ruby-bot/server_spec.rb +0 -2
  53. data/spec/slack-ruby-bot/support/commands_helper_spec.rb +0 -2
  54. data/spec/slack-ruby-bot/support/loggable_spec.rb +0 -2
  55. data/spec/slack-ruby-bot/version_spec.rb +0 -2
  56. metadata +13 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 599b19f9020a13b61a9cda2f4bd570e4121a4208
4
- data.tar.gz: 885f72dffbb46d911fb7368309713393a684d35f
3
+ metadata.gz: 15ac681a340b2b17bc5a4148c0b55dd7295fc1f2
4
+ data.tar.gz: 04d0018c8acf732715f5992af917a69fcaa6d337
5
5
  SHA512:
6
- metadata.gz: 2f98b14588981310cafa3b95aa12b167551bdd32c7c0a78f77689d6d7ded74a10088f0b65a60a777f186f280a5e2ad1fd309e3166987d4f9c0e4fef83059a313
7
- data.tar.gz: 25c471d74bf5b09f4ebe5de10f74add3decf68c226fa8e57506a58a282c7736441604431bac0773afc08a169e36b4c0c0c848643d255fe5ed13e7bb245e7b2fb
6
+ metadata.gz: 9f04ac47c686f1acd671d691826e7192d24da2b73c30fcdfda5a553f0906fa37439b5d4b4c2690148fd8fee7dd58cb9c6b2074e141b4d9670dbe5ceaa05e2e75
7
+ data.tar.gz: 9e65685bc89a791ac36000bfd955daef01ef230f1010e31ff71b2133d82ee241c6f81dd6660ff81488d7d20a5e0b3f6eab5b79ba22323e5a3794e7e93b22349f
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
+ --require spec_helper
@@ -1,3 +1,8 @@
1
+ ### 0.10.2 (06/03/2017)
2
+
3
+ * [#137](https://github.com/slack-ruby/slack-ruby-bot/pull/137): Add Model-View-Controller classes to allow for more explicit control over how `command`s are designed - [@chuckremes](https://github.com/chuckremes).
4
+ * [#130](https://github.com/slack-ruby/slack-ruby-bot/issues/130): Added test dependencies in TUTORIAL.md - [@jbristow](https://github.com/jbristow).
5
+
1
6
  ### 0.10.1 (2/12/2017)
2
7
 
3
8
  * [#113](https://github.com/slack-ruby/slack-ruby-bot/issues/113): Fixed commands in subclassed `SlackRubyBot::Bot` - [@dblock](https://github.com/dblock).
data/README.md CHANGED
@@ -19,7 +19,7 @@ If you are not familiar with Slack bots or Slack API concepts, you might want to
19
19
 
20
20
  ## Stable Release
21
21
 
22
- You're reading the documentation for the **stable** release of slack-ruby-bot, 0.10.1. See [CHANGELOG](CHANGELOG.md) for a history of changes and [UPGRADING](UPGRADING.md) for how to upgrade to more recent versions.
22
+ You're reading the documentation for the **next** release of slack-ruby-bot. Please see the documentation for the [last stable release, v0.10.1](https://github.com/slack-ruby/slack-ruby-bot/tree/v0.10.1) unless you're integrating with HEAD. See [CHANGELOG](CHANGELOG.md) for a history of changes and [UPGRADING](UPGRADING.md) for how to upgrade to more recent versions.
23
23
 
24
24
  ## Usage
25
25
 
@@ -64,7 +64,9 @@ The following examples of bots based on slack-ruby-bot are listed in growing ord
64
64
  * [slack-mathbot](https://github.com/dblock/slack-mathbot): Slack integration with math.
65
65
  * [slack-google-bot](https://github.com/dblock/slack-google-bot): A Slack bot that searches Google, including CSE.
66
66
  * [slack-aws](https://github.com/dblock/slack-aws): Slack integration with Amazon Web Services.
67
+ * [slack-deploy-bot](https://github.com/accessd/slack-deploy-bot): A Slack bot that helps you to deploy your apps.
67
68
  * [slack-gamebot](https://github.com/dblock/slack-gamebot): A game bot service for ping pong, chess, etc, hosted at [playplay.io](http://playplay.io).
69
+ * [slack-victorbot](https://github.com/uShip/victorbot): A Slack bot to talk to the Victorops service.
68
70
 
69
71
  ### Commands and Operators
70
72
 
@@ -293,7 +295,7 @@ module MyBot
293
295
  end
294
296
  ```
295
297
 
296
- Hooks can also be written as blocks inside the `SlackBotRuby::Server` class, for example
298
+ Hooks can also be written as blocks inside the `SlackRubyBot::Server` class, for example
297
299
 
298
300
  ```ruby
299
301
  module MyBot
@@ -344,6 +346,133 @@ end
344
346
 
345
347
  For an example of advanced integration that supports multiple teams, see [slack-gamebot](https://github.com/dblock/slack-gamebot) and [playplay.io](http://playplay.io) that is built on top of it.
346
348
 
349
+ ### Model-View-Controller Design
350
+
351
+ The `command` method is essentially a controller method that receives input from the outside and acts upon it. Complex behaviors could lead to a long and difficult-to-understand `command` block. A complex `command` block is a candidate for separation into classes conforming to the Model-View-Controller pattern popularized by Rails.
352
+
353
+ The library provides three helpful base classes named `SlackRubyBot::MVC::Model::Base`, `SlackRubyBot::MVC::View::Base`, and `SlackRubyBot::MVC::Controller::Base`.
354
+
355
+ Testing a `command` block is difficult. As separate classes, the Model/View/Controller's behavior can be tested via `rspec` or a similar tool.
356
+
357
+ #### Controller
358
+
359
+ The Controller is the focal point of the bot behavior. Typically the code that would go into the `command` block will now go into an instance method in a Controller subclass. The instance method name should match the command name exactly (case sensitive).
360
+
361
+ As an example, these two classes are functionally equivalent.
362
+
363
+ Consider the following `Agent` class which is the simplest default approach to take.
364
+
365
+ ```ruby
366
+ class Agent < SlackRubyBot::Bot
367
+ command 'sayhello' do |client, data, match|
368
+ client.say(channel: data.channel, text: "Received command #{match[:command]} with args #{match[:expression]}")
369
+ end
370
+ end
371
+
372
+ Using the MVC functionality, we would create a controller instead to encapsulate this function.
373
+ ```ruby
374
+ class MyController < SlackRubyBot::MVC::Controller::Base
375
+ def sayhello
376
+ client.say(channel: data.channel, text: "Received command #{match[:command]} with args #{match[:expression]}")
377
+ end
378
+ end
379
+ MyController.new(MyModel.new, MyView.new)
380
+ ```
381
+ Note in the above example that the Controller instance method `sayhello` does not receive any arguments. When the instance method is called, the Controller class sets up some accessor methods to provide the normal `client`, `data`, and `match` objects. These are the same objects passed to the `command` block.
382
+
383
+ However, the Controller anticipates that the model and view objects should contain business logic that will also operate on the `client`, `data`, and `match` objects. The controller provides access to the model and view via the `model` and `view` accessor methods. The [inventory example](examples/inventory/inventorybot.rb) provides a full example of a Model, View, and Controller working together.
384
+
385
+ A Controller may need helper methods for certain work. To prevent the helper method from creating a route that the bot will respond to directly, the instance method name should begin with an underscore (e.g. `_my_helper_method`). When building the bot routes, these methods will be skipped.
386
+
387
+ Lastly, the Controller class includes `ActiveSupport::Callbacks` which allows for full flexibility in creating `before`, `after`, and `around` hooks for all methods. Again, see the [inventory example](examples/inventory/inventorybot.rb) for more information.
388
+
389
+ #### Model
390
+
391
+ A complex bot may need to read or write data from a database or other network resource. Setting up and tearing down these connections can be costly, so the model can do it once upon instantiation.
392
+
393
+ The Model also includes `ActiveSupport::Callbacks`.
394
+
395
+ ```ruby
396
+ class MyModel < SlackRubyBot::MVC::Model::Base
397
+ define_callbacks :sanitize
398
+ set_callback :sanitize, :around, :sanitize_resource
399
+ attr_accessor :_resource
400
+
401
+ def initialize
402
+ @db = setup_database_connection
403
+ end
404
+
405
+ def read(resource)
406
+ self._resource = resource
407
+ run_callbacks :sanitize do
408
+ @db.select(:column1 => resource)
409
+ # ... do some expensive work
410
+ end
411
+ end
412
+
413
+ private
414
+
415
+ def sanitize_resource
416
+ self._resource.downcase
417
+ result = yield
418
+ puts "After read, result is #{result.inspect}"
419
+ end
420
+ end
421
+ ```
422
+
423
+ Like Controllers, the Model is automatically loaded with the latest version of the `client`, `data`, and `match` objects each time the controller method is called. Therefore the model will always have access to the latest objects when doing its work. It will typically only use the `data` and `match` objects.
424
+
425
+ Model methods are not matched to routes, so there is no restriction on how to name methods as there is in Controllers.
426
+
427
+ #### View
428
+
429
+ A typical bot just writes to a channel or uses the web client to react/unreact to a message. More complex bots will probably require more complex behaviors. These should be stored in a `SlackRubyBot::MVC::View::Base` subclass.
430
+
431
+ ```ruby
432
+ class MyView < SlackRubyBot::MVC::View::Base
433
+ define_callbacks :logit
434
+ set_callbacks :logit, :around, :audit_trail
435
+
436
+ def initialize
437
+ @mailer = setup_mailer
438
+ @ftp = setup_ftp_handler
439
+ end
440
+
441
+ def email_admin(message)
442
+ run_callbacks :logit do
443
+ @mailer.send(:administrator, message)
444
+ end
445
+ end
446
+
447
+ def react_thumbsup
448
+ client.web_client.reactions_add(
449
+ name: :thumbs_up,
450
+ channel: data.channel,
451
+ timestamp: data.ts,
452
+ as_user: true)
453
+ end
454
+
455
+ def react_thumbsdown
456
+ client.web_client.reactions_remove(
457
+ name: :thumbs_up,
458
+ channel: data.channel,
459
+ timestamp: data.ts,
460
+ as_user: true)
461
+ end
462
+
463
+ private
464
+
465
+ def audit_trail
466
+ Logger.audit("Sending email at [#{Time.now}]")
467
+ yield
468
+ Logger.audit("Email sent by [#{Time.now}]")
469
+ end
470
+ end
471
+ ```
472
+ Again, the View will have access to the most up to date `client`, `data`, and `match` objects. It will typically only use the `client` and `data` objects.
473
+
474
+ View methods are not matched to routes, so there is no restriction on how to name methods as there is in Controllers.
475
+
347
476
  ### RSpec Shared Behaviors
348
477
 
349
478
  Slack-ruby-bot ships with a number of shared RSpec behaviors that can be used in your RSpec tests.
@@ -13,7 +13,7 @@ A typical production Slack bot is a combination of a vanilla web server and a we
13
13
  Create a `Gemfile` that uses [slack-ruby-bot](https://github.com/slack-ruby/slack-ruby-bot), [sinatra](https://github.com/sinatra/sinatra) (a web framework) and [puma](https://github.com/puma/puma) (a web server). For development we'll also use [foreman](https://github.com/theforeman/foreman) and write tests with [rspec](https://github.com/rspec/rspec).
14
14
 
15
15
  ```ruby
16
- source 'http://rubygems.org'
16
+ source 'https://rubygems.org'
17
17
 
18
18
  gem 'slack-ruby-bot'
19
19
  gem 'puma'
@@ -29,6 +29,8 @@ end
29
29
  group :test do
30
30
  gem 'rspec'
31
31
  gem 'rack-test'
32
+ gem 'vcr'
33
+ gem 'webmock'
32
34
  end
33
35
  ```
34
36
 
@@ -0,0 +1,5 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'slack-ruby-bot', path: '../..'
4
+ gem 'celluloid-io'
5
+ gem 'sqlite3'
@@ -0,0 +1 @@
1
+ console: bundle exec ruby inventorybot.rb
@@ -0,0 +1,248 @@
1
+ require 'slack-ruby-bot'
2
+ require 'sqlite3'
3
+
4
+ # Demonstrate the usage of the Model, View, and Controller
5
+ # classes to build an inventory bot.
6
+
7
+ SlackRubyBot::Client.logger.level = Logger::WARN
8
+
9
+ # The Controller takes the place of the `command` block. It has access to the
10
+ # `client`, `data`, and `match` arguments passed to the `command` block. Each
11
+ # instance method generates a route for the bot to match against.
12
+ #
13
+ # The controller instance accesses the model and view via the `model` and
14
+ # `view` accessor methods.
15
+ #
16
+ # ActiveSupport::Callbacks support is included so every method can have
17
+ # before, after, or around hooks.
18
+ #
19
+ # Helper methods should begin with an underscore (e.g. _row) so that
20
+ # `command` routes are not auto-generated for the method.
21
+ #
22
+ class InventoryController < SlackRubyBot::MVC::Controller::Base
23
+ define_callbacks :react, :notify
24
+ set_callback :react, :around, :around_reaction
25
+ set_callback :notify, :after, :notify_admin
26
+ attr_accessor :_row
27
+
28
+ def create
29
+ model.add_item(match[:expression])
30
+ end
31
+
32
+ def read
33
+ run_callbacks :react do
34
+ row = model.read_item(match[:expression])
35
+ view.say(channel: data.channel, text: row.inspect)
36
+ end
37
+ end
38
+
39
+ def update
40
+ run_callbacks :notify do
41
+ self._row = model.update_item(match[:expression])
42
+ end
43
+ end
44
+
45
+ def delete
46
+ result = model.delete_item(match[:expression])
47
+ if result
48
+ view.delete_succeeded
49
+ else
50
+ view.delete_failed
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def notify_admin
57
+ row = _row.first
58
+ return if row[:quantity].to_i.zero?
59
+ view.email_admin("Inventory item #{row[:name]} needs to be refilled.")
60
+ view.say(channel: data.channel, text: "Administrator notified via email to refill #{row[:name]}.")
61
+ end
62
+
63
+ def around_reaction
64
+ view.react_wait
65
+ view.say(channel: data.channel, text: 'Please wait while long-running action completes...')
66
+ sleep 2.0
67
+ yield
68
+ view.say(channel: data.channel, text: 'Action has completed!')
69
+ view.unreact_wait
70
+ end
71
+ end
72
+
73
+ # The Model contains all business logic.
74
+ #
75
+ # ActiveSupport::Callbacks support is included for before, after,
76
+ # or around hooks.
77
+ #
78
+ # The Model has access to the `client`, `data`, and `match` objects.
79
+ #
80
+ class InventoryModel < SlackRubyBot::MVC::Model::Base
81
+ define_callbacks :fixup
82
+ set_callback :fixup, :before, :normalize_data
83
+ attr_accessor :_line
84
+
85
+ def initialize
86
+ @db = SQLite3::Database.new ':memory'
87
+ @db.results_as_hash = true
88
+ @db.execute 'CREATE TABLE IF NOT EXISTS Inventory(Id INTEGER PRIMARY KEY,
89
+ Name TEXT, Quantity INT, Price INT)'
90
+
91
+ s = @db.prepare 'SELECT * FROM Inventory'
92
+ results = s.execute
93
+ count = 0
94
+ count += 1 while results.next
95
+ return if count < 4
96
+ add_item "'Audi',3,52642"
97
+ add_item "'Mercedes',1,57127"
98
+ add_item "'Skoda',5,9000"
99
+ add_item "'Volvo',1,29000"
100
+ end
101
+
102
+ def add_item(line)
103
+ self._line = line # make line accessible to callback
104
+ run_callbacks :fixup do
105
+ name, quantity, price = parse(_line)
106
+ row = @db.prepare('SELECT MAX(Id) FROM Inventory').execute
107
+ max_id = row.next_hash['MAX(Id)']
108
+ @db.execute "INSERT INTO Inventory VALUES(#{max_id + 1},'#{name}',#{quantity.to_i},#{price.to_i})"
109
+ end
110
+ end
111
+
112
+ def read_item(line)
113
+ self._line = line
114
+ run_callbacks :fixup do
115
+ name, _other = parse(_line)
116
+ statement = if name == '*'
117
+ @db.prepare 'SELECT * FROM Inventory'
118
+ else
119
+ @db.prepare("SELECT * FROM Inventory WHERE Name='#{name}'")
120
+ end
121
+
122
+ results = statement.execute
123
+ a = []
124
+ results.each do |row|
125
+ a << { id: row['Id'], name: row['Name'], quantity: row['Quantity'], price: row['Price'] }
126
+ end
127
+ a
128
+ end
129
+ end
130
+
131
+ def update_item(line)
132
+ self._line = line
133
+ run_callbacks :fixup do
134
+ name, quantity, price = parse(_line)
135
+ statement = if price
136
+ @db.prepare "UPDATE Inventory SET Quantity=#{quantity}, Price=#{price} WHERE Name='#{name}'"
137
+ else
138
+ @db.prepare "UPDATE Inventory SET Quantity=#{quantity} WHERE Name='#{name}'"
139
+ end
140
+ statement.execute
141
+ read_item(_line)
142
+ end
143
+ end
144
+
145
+ def delete_item(line)
146
+ self._line = line
147
+ run_callbacks :fixup do
148
+ name, _other = parse(_line)
149
+ before_count = row_count
150
+ statement = @db.prepare "DELETE FROM Inventory WHERE Name='#{name}'"
151
+ statement.execute
152
+ before_count != row_count
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ def row_count
159
+ statement = @db.prepare 'SELECT COUNT(*) FROM Inventory'
160
+ result = statement.execute
161
+ result.next_hash['COUNT(*)']
162
+ end
163
+
164
+ def parse(line)
165
+ line.split(',')
166
+ end
167
+
168
+ def normalize_data
169
+ name, quantity, price = parse(_line)
170
+ self._line = [name.capitalize, quantity, price].join(',')
171
+ end
172
+ end
173
+
174
+ # All interactivity logic should live in this class.
175
+ #
176
+ # ActiveSupport::Callbacks support is included for before, after,
177
+ # or around hooks.
178
+ #
179
+ # The Model has access to the `client`, `data`, and `match` objects.
180
+ #
181
+ class InventoryView < SlackRubyBot::MVC::View::Base
182
+ def react_wait
183
+ client.web_client.reactions_add(
184
+ name: :hourglass_flowing_sand,
185
+ channel: data.channel,
186
+ timestamp: data.ts,
187
+ as_user: true
188
+ )
189
+ end
190
+
191
+ def unreact_wait
192
+ client.web_client.reactions_remove(
193
+ name: :hourglass_flowing_sand,
194
+ channel: data.channel,
195
+ timestamp: data.ts,
196
+ as_user: true
197
+ )
198
+ end
199
+
200
+ def email_admin(message)
201
+ # send email to administrator with +message+
202
+ # ...
203
+ puts "Sent email to administrator: #{message}"
204
+ end
205
+
206
+ def delete_succeeded
207
+ say(channel: data.channel, text: 'Item was successfully deleted.')
208
+ end
209
+
210
+ def delete_failed
211
+ say(channel: data.channel, text: 'Item failed to be deleted.')
212
+ end
213
+ end
214
+
215
+ class InventoryBot < SlackRubyBot::Bot
216
+ help do
217
+ title 'Inventory Bot'
218
+ desc 'This bot lets you create, read, update, and delete items from an inventory.'
219
+
220
+ command 'create' do
221
+ desc 'Add an item to the inventory.'
222
+ end
223
+
224
+ command 'read' do
225
+ desc 'Get inventory status for an item.'
226
+ end
227
+
228
+ command 'update' do
229
+ desc 'Update inventory levels for an item.'
230
+ end
231
+
232
+ command 'delete' do
233
+ desc 'Remove an item from inventory.'
234
+ end
235
+ end
236
+
237
+ # Instantiate the Model, View, and Controller objects within the +Bot+ subclass
238
+ # or within a SlackRubyBot::Commands::Base subclass.
239
+ #
240
+ model = InventoryModel.new
241
+ view = InventoryView.new
242
+ @controller = InventoryController.new(model, view)
243
+ @controller.class.command_class.routes.each do |route|
244
+ STDERR.puts route.inspect
245
+ end
246
+ end
247
+
248
+ InventoryBot.run
@@ -25,3 +25,4 @@ require 'slack-ruby-bot/client'
25
25
  require 'slack-ruby-bot/server'
26
26
  require 'slack-ruby-bot/app'
27
27
  require 'slack-ruby-bot/bot'
28
+ require 'slack-ruby-bot/mvc'
@@ -36,7 +36,7 @@ module SlackRubyBot
36
36
  end
37
37
 
38
38
  def default_command_name
39
- name && name.split(':').last.downcase
39
+ name ? name.split(':').last.downcase : object_id.to_s
40
40
  end
41
41
 
42
42
  def operator(*values, &block)
@@ -0,0 +1 @@
1
+ require 'slack-ruby-bot/mvc/mvc'
@@ -0,0 +1,111 @@
1
+ module SlackRubyBot
2
+ module MVC
3
+ module Controller
4
+ class Base
5
+ include ActiveSupport::Callbacks
6
+
7
+ class << self
8
+ attr_reader :abstract
9
+ alias abstract? abstract
10
+
11
+ def controllers
12
+ Base.instance_variable_get(:@controllers)
13
+ end
14
+
15
+ def command_class
16
+ Base.instance_variable_get(:@command_class)
17
+ end
18
+
19
+ def reset!
20
+ # Remove any earlier anonymous classes from prior calls so we don't leak them
21
+ Commands::Base.command_classes.delete(Controller::Base.command_class) if Base.command_class
22
+
23
+ # Create anonymous class to hold our #commands; required by SlackRubyBot::Commands::Base
24
+ Base.instance_variable_set(:@command_class, Class.new(SlackRubyBot::Commands::Base))
25
+ Base.instance_variable_set(:@controllers, [])
26
+ end
27
+
28
+ # Define a controller as abstract. See internal_methods for more
29
+ # details.
30
+ def abstract!
31
+ @abstract = true
32
+ end
33
+
34
+ def inherited(klass) # :nodoc:
35
+ # Define the abstract ivar on subclasses so that we don't get
36
+ # uninitialized ivar warnings
37
+ unless klass.instance_variable_defined?(:@abstract)
38
+ klass.instance_variable_set(:@abstract, false)
39
+ end
40
+ super
41
+ end
42
+
43
+ def register_controller(controller)
44
+ # Only used to keep a reference around so the instance object doesn't get garbage collected
45
+ Base.instance_variable_set(:@controllers, []) unless controllers
46
+ controller_ary = Base.instance_variable_get(:@controllers)
47
+ controller_ary << controller
48
+ klass = controller.class
49
+
50
+ methods = (klass.public_instance_methods(true) -
51
+ # Except for public instance methods of Base and its ancestors
52
+ internal_methods(klass) +
53
+ # Be sure to include shadowed public instance methods of this class
54
+ klass.public_instance_methods(false)).uniq.map(&:to_s)
55
+
56
+ methods.each do |meth_name|
57
+ next if meth_name[0] == '_'
58
+
59
+ # sprinkle a little syntactic sugar on top of existing `command` infrastructure
60
+ command_class.class_eval do
61
+ command meth_name.to_s do |client, data, match|
62
+ controller.use_args(client, data, match)
63
+ controller.call_command
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # A list of all internal methods for a controller. This finds the first
70
+ # abstract superclass of a controller, and gets a list of all public
71
+ # instance methods on that abstract class. Public instance methods of
72
+ # a controller would normally be considered action methods, so methods
73
+ # declared on abstract classes are being removed.
74
+ # (Controller::Base is defined as abstract)
75
+ def internal_methods(controller)
76
+ controller = controller.superclass until controller.abstract?
77
+ controller.public_instance_methods(true)
78
+ end
79
+ end
80
+
81
+ abstract!
82
+ reset!
83
+
84
+ attr_reader :model, :view, :client, :data, :match
85
+
86
+ def initialize(model, view)
87
+ @model = model
88
+ @view = view
89
+ self.class.register_controller(self)
90
+ end
91
+
92
+ # Hand off the latest updated objects to the +model+ and +view+ and
93
+ # update our +client+, +data+, and +match+ accessors.
94
+ def use_args(client, data, match)
95
+ @client = client
96
+ @data = data
97
+ @match = match
98
+ model.use_args(client, data, match)
99
+ view.use_args(client, data, match)
100
+ end
101
+
102
+ # Determine the command issued and call the corresponding instance method
103
+ def call_command
104
+ verb = match.captures[match.names.index('command')]
105
+ verb = verb.downcase if verb
106
+ public_send(verb)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,25 @@
1
+ module SlackRubyBot
2
+ module MVC
3
+ module Model
4
+ class Base
5
+ include ActiveSupport::Callbacks
6
+
7
+ class << self
8
+ def inherited(klass) # :nodoc:
9
+ super
10
+ end
11
+ end
12
+
13
+ attr_reader :client, :data, :match
14
+
15
+ # Hand off the latest updated objects to the +model+ and +view+ and
16
+ # update our +client+, +data+, and +match+ accessors.
17
+ def use_args(client, data, match)
18
+ @client = client
19
+ @data = data
20
+ @match = match
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ require_relative '../mvc/controller/base'
2
+
3
+ require_relative '../mvc/model/base'
4
+
5
+ require_relative '../mvc/view/base'
@@ -0,0 +1,28 @@
1
+ module SlackRubyBot
2
+ module MVC
3
+ module View
4
+ class Base
5
+ include ActiveSupport::Callbacks
6
+ extend Forwardable
7
+
8
+ class << self
9
+ def inherited(klass) # :nodoc:
10
+ super
11
+ end
12
+ end
13
+
14
+ attr_reader :client, :data, :match
15
+
16
+ def_delegators :@client, :say
17
+
18
+ # Hand off the latest updated objects to the +model+ and +view+ and
19
+ # update our +client+, +data+, and +match+ accessors.
20
+ def use_args(client, data, match)
21
+ @client = client
22
+ @data = data
23
+ @match = match
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module SlackRubyBot
2
- VERSION = '0.10.1'.freeze
2
+ VERSION = '0.10.2'.freeze
3
3
  end
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::App do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Client do
4
2
  describe '#send_gifs?' do
5
3
  context 'without giphy is false', unless: ENV.key?('WITH_GIPHY') do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Default do
4
2
  it 'lowercase' do
5
3
  expect(message: SlackRubyBot.config.user).to respond_with_slack_message(SlackRubyBot::ABOUT)
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot do
4
2
  def client
5
3
  SlackRubyBot::Client.new aliases: %w(:emoji: alias каспаров)
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Help do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Bot do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Bot) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Base do
4
2
  let! :command1 do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -35,5 +33,12 @@ describe SlackRubyBot::Commands::Base do
35
33
  command_classes = SlackRubyBot::Commands::Base.command_classes
36
34
  expect(command_classes.find_index(command1)).to be < command_classes.find_index(command2)
37
35
  end
36
+
37
+ it 'uses the object_id of an anonymous class as the default command name' do
38
+ command_classes = SlackRubyBot::Commands::Base.command_classes
39
+ anon_class = Class.new(SlackRubyBot::Commands::Base)
40
+ expect(command_classes).to include anon_class
41
+ expect(command_classes.find { |obj| obj == anon_class }.object_id.to_s).to eq anon_class.default_command_name
42
+ end
38
43
  end
39
44
  end
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Help::Attrs do
4
2
  let(:help_attrs) { described_class.new('WeatherBot') }
5
3
 
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Help do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Hi do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::App do
4
2
  def app
5
3
  SlackRubyBot::App.new
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Base do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands, if: ENV.key?('WITH_GIPHY') do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands, if: ENV.key?('WITH_GIPHY') do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Commands::Unknown do
4
2
  it 'invalid command' do
5
3
  expect(message: "#{SlackRubyBot.config.user} foobar").to respond_with_slack_message("Sorry <@user>, I don't understand that command!")
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Config do
4
2
  describe '.send_gifs?' do
5
3
  after { ENV.delete 'SLACK_RUBY_BOT_SEND_GIFS' }
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Hooks::HookSupport do
4
2
  subject do
5
3
  Class.new do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Hooks::Message do
4
2
  let(:message_hook) { described_class.new }
5
3
 
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Hooks::Set do
4
2
  let(:client) { Slack::RealTime::Client.new }
5
3
 
@@ -0,0 +1,97 @@
1
+ describe SlackRubyBot::MVC::Controller::Base, 'initialization' do
2
+ let(:model) { double('model') }
3
+ let(:view) { double('view') }
4
+ let(:controller) do
5
+ Class.new(SlackRubyBot::MVC::Controller::Base) do
6
+ def quxo() end
7
+ end
8
+ end
9
+
10
+ after(:each) { controller.reset! }
11
+
12
+ it 'sets :model accessor to passed in model' do
13
+ expect(controller.new(model, view).model).to eq(model)
14
+ end
15
+
16
+ it 'sets :view accessor to passed in view' do
17
+ expect(controller.new(model, view).view).to eq(view)
18
+ end
19
+
20
+ it 'saves the controller instance to the class' do
21
+ instance = controller.new(model, view)
22
+ expect(SlackRubyBot::MVC::Controller::Base.controllers).to include(instance)
23
+ SlackRubyBot::MVC::Controller::Base.controllers.clear
24
+ end
25
+
26
+ it 'creates a command route on SlackRubyBot::Commands::Base' do
27
+ controller.reset!
28
+ route_count = 0
29
+ controller.new(model, view)
30
+ expect(controller.command_class.routes.size).to eq(route_count + 1)
31
+ end
32
+
33
+ it 'does NOT create a command route on SlackRubyBot::Commands::Base when method starts with a single "_"' do
34
+ controller.reset!
35
+ route_count = 0
36
+ controller.class_eval do
37
+ def _no_route_to_me() end
38
+ end
39
+ controller.new(model, view)
40
+ expect(controller.command_class.routes.size).to eq(route_count + 1) # instead of +2
41
+ end
42
+ end
43
+
44
+ describe SlackRubyBot::MVC::Controller::Base, 'setup' do
45
+ let(:controller) do
46
+ Class.new(SlackRubyBot::MVC::Controller::Base) do
47
+ def quxo
48
+ client.say(channel: data.channel, text: "#{match[:command]}: #{match[:expression]}")
49
+ end
50
+ end
51
+ end
52
+
53
+ after(:each) { controller.reset! }
54
+
55
+ it 'passes client, data, and match args to the model' do
56
+ model = SlackRubyBot::MVC::Model::Base.new
57
+ view = SlackRubyBot::MVC::View::Base.new
58
+ controller.new(model, view)
59
+ expect(message: " #{SlackRubyBot.config.user} quxo red").to respond_with_slack_message('quxo: red')
60
+ expect(model.client).to be_kind_of(SlackRubyBot::Client)
61
+ expect(model.data).to be_kind_of(Hash)
62
+ expect(model.match).to be_kind_of(MatchData)
63
+ end
64
+
65
+ it 'passes client, data, and match args to the view' do
66
+ model = SlackRubyBot::MVC::Model::Base.new
67
+ view = SlackRubyBot::MVC::View::Base.new
68
+ controller.new(model, view)
69
+ expect(message: " #{SlackRubyBot.config.user} quxo red").to respond_with_slack_message('quxo: red')
70
+ expect(view.client).to be_kind_of(SlackRubyBot::Client)
71
+ expect(view.data).to be_kind_of(Hash)
72
+ expect(view.match).to be_kind_of(MatchData)
73
+ end
74
+ end
75
+
76
+ describe SlackRubyBot::MVC::Controller::Base, 'execution' do
77
+ let(:controller) do
78
+ Class.new(SlackRubyBot::MVC::Controller::Base) do
79
+ attr_accessor :__flag
80
+ def quxo
81
+ @__flag = true
82
+ client.say(channel: data.channel, text: "#{match[:command]}: #{match[:expression]}")
83
+ end
84
+ end
85
+ end
86
+
87
+ after(:each) { controller.reset! }
88
+
89
+ it 'runs the controller method' do
90
+ model = SlackRubyBot::MVC::Model::Base.new
91
+ view = SlackRubyBot::MVC::View::Base.new
92
+ instance = controller.new(model, view)
93
+ instance.__flag = false
94
+ expect(message: " #{SlackRubyBot.config.user} quxo red").to respond_with_slack_message('quxo: red')
95
+ expect(instance.__flag).to be_truthy
96
+ end
97
+ end
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe RSpec do
4
2
  let! :command do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Server do
4
2
  let(:logger) { subject.send :logger }
5
3
  let(:client) { Slack::RealTime::Client.new }
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::CommandsHelper do
4
2
  let(:bot_class) { Testing::WeatherBot }
5
3
  let(:command_class) { Testing::HelloCommand }
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot::Loggable do
4
2
  let! :class_with_logger do
5
3
  Class.new(SlackRubyBot::Commands::Base) do
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  describe SlackRubyBot do
4
2
  it 'has a version' do
5
3
  expect(SlackRubyBot::VERSION).to_not be nil
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slack-ruby-bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Doubrovkine
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-12 00:00:00.000000000 Z
11
+ date: 2017-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashie
@@ -144,6 +144,9 @@ files:
144
144
  - Rakefile
145
145
  - TUTORIAL.md
146
146
  - UPGRADING.md
147
+ - examples/inventory/Gemfile
148
+ - examples/inventory/Procfile
149
+ - examples/inventory/inventorybot.rb
147
150
  - examples/market/Gemfile
148
151
  - examples/market/Procfile
149
152
  - examples/market/marketbot.rb
@@ -175,6 +178,11 @@ files:
175
178
  - lib/slack-ruby-bot/hooks/hook_support.rb
176
179
  - lib/slack-ruby-bot/hooks/message.rb
177
180
  - lib/slack-ruby-bot/hooks/set.rb
181
+ - lib/slack-ruby-bot/mvc.rb
182
+ - lib/slack-ruby-bot/mvc/controller/base.rb
183
+ - lib/slack-ruby-bot/mvc/model/base.rb
184
+ - lib/slack-ruby-bot/mvc/mvc.rb
185
+ - lib/slack-ruby-bot/mvc/view/base.rb
178
186
  - lib/slack-ruby-bot/rspec.rb
179
187
  - lib/slack-ruby-bot/rspec/support/bots_for_tests.rb
180
188
  - lib/slack-ruby-bot/rspec/support/fixtures/slack/migration_in_progress.yml
@@ -231,6 +239,7 @@ files:
231
239
  - spec/slack-ruby-bot/hooks/hook_support_spec.rb
232
240
  - spec/slack-ruby-bot/hooks/message_spec.rb
233
241
  - spec/slack-ruby-bot/hooks/set_spec.rb
242
+ - spec/slack-ruby-bot/mvc/controller/controller_to_command_spec.rb
234
243
  - spec/slack-ruby-bot/rspec/respond_with_error_spec.rb
235
244
  - spec/slack-ruby-bot/server_spec.rb
236
245
  - spec/slack-ruby-bot/support/commands_helper_spec.rb
@@ -257,7 +266,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
257
266
  version: 1.3.6
258
267
  requirements: []
259
268
  rubyforge_project:
260
- rubygems_version: 2.5.1
269
+ rubygems_version: 2.6.11
261
270
  signing_key:
262
271
  specification_version: 4
263
272
  summary: The easiest way to write a Slack bot in Ruby.
@@ -295,6 +304,7 @@ test_files:
295
304
  - spec/slack-ruby-bot/hooks/hook_support_spec.rb
296
305
  - spec/slack-ruby-bot/hooks/message_spec.rb
297
306
  - spec/slack-ruby-bot/hooks/set_spec.rb
307
+ - spec/slack-ruby-bot/mvc/controller/controller_to_command_spec.rb
298
308
  - spec/slack-ruby-bot/rspec/respond_with_error_spec.rb
299
309
  - spec/slack-ruby-bot/server_spec.rb
300
310
  - spec/slack-ruby-bot/support/commands_helper_spec.rb