smart_todo 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/Gemfile.lock +1 -1
- data/exe/smart_todo +2 -4
- data/lib/smart_todo/cli.rb +12 -6
- data/lib/smart_todo/dispatchers/base.rb +92 -0
- data/lib/smart_todo/dispatchers/output.rb +15 -0
- data/lib/smart_todo/dispatchers/slack.rb +61 -0
- data/lib/smart_todo/version.rb +1 -1
- data/lib/smart_todo.rb +6 -1
- data/lib/smart_todo_cop.rb +3 -1
- metadata +5 -3
- data/lib/smart_todo/dispatcher.rb +0 -99
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a61dfef2e6101321b34f154e8143441b3f28502709b26cb2db504fe83251ba1a
|
4
|
+
data.tar.gz: 3f12eef19063ed2f8585b56be3b483f1312dc44a7c96cc93931c8ae89a4ac7c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25d629887ac69968359d5bd26a15736adebad37c881eeec2c5b05432be1a05fca0d69cf9c978c638b7033e9deffcc2f24111fee307359325f70c07d6905d6b04
|
7
|
+
data.tar.gz: 7a5472d3ad8d69187ccc5d46975b1f3ac0368886259c1f42d2303c4a0e8cb6f4de34a33f08b12d205ddde13f8ba5184a423eba95dece54335f0e7ed405861a15
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [1.1.0] - 2019-09-06
|
10
|
+
### Fixed
|
11
|
+
- Fixed the SmartTodo cop to add an offense in case a SmartTodo has a wrong event.
|
12
|
+
```ruby
|
13
|
+
# Bad
|
14
|
+
#
|
15
|
+
# TODO(on '2019-08-08')
|
16
|
+
```
|
17
|
+
|
18
|
+
### Added
|
19
|
+
- SmartTodo will now use the fallback channel in case a todo has a channel
|
20
|
+
assignee that doesn't exist.
|
21
|
+
- Added a new `Output` dispatcher which will just output the expired event.
|
22
|
+
By default SmartTodo will now output expired todo in the terminal instead
|
23
|
+
of not running at all.
|
24
|
+
|
25
|
+
Users should now pass a `--dispatcher` to the CLI to let SmartTodo through
|
26
|
+
which dispatcher the message should be send.
|
27
|
+
|
28
|
+
```sh
|
29
|
+
bin/smart_todo --dispatcher 'slack'
|
30
|
+
```
|
31
|
+
|
32
|
+
For backward compatibility reasons, the dispacher used will be Slack, in
|
33
|
+
case you have the `ENABLE_SMART_TODO` environment set. This will be removed
|
34
|
+
in the next major version.
|
35
|
+
|
9
36
|
## [1.0.2] - 2019-08-09
|
10
37
|
### Fixed
|
11
38
|
- Fixed the SmartTodo cop to add an offense in case a SmartTodo has no assignee.
|
data/Gemfile.lock
CHANGED
data/exe/smart_todo
CHANGED
@@ -5,10 +5,8 @@ $LOAD_PATH.unshift("#{__dir__}/../lib")
|
|
5
5
|
|
6
6
|
require 'smart_todo'
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
exit(0)
|
8
|
+
if ENV['ENABLE_SMART_TODO'] && !ARGV.include?('--dispatcher')
|
9
|
+
ARGV << '--dispatcher' << 'slack'
|
12
10
|
end
|
13
11
|
|
14
12
|
SmartTodo::CLI.new.run
|
data/lib/smart_todo/cli.rb
CHANGED
@@ -27,13 +27,11 @@ module SmartTodo
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
# @raise [ArgumentError]
|
31
|
-
#
|
30
|
+
# @raise [ArgumentError] In case an option needed by a dispatcher wasn't provided.
|
31
|
+
#
|
32
32
|
# @return [void]
|
33
33
|
def validate_options!
|
34
|
-
@options
|
35
|
-
|
36
|
-
@options.fetch(:fallback_channel) { raise(ArgumentError, 'Missing :fallback_channel') }
|
34
|
+
dispatcher.validate_options!(@options)
|
37
35
|
end
|
38
36
|
|
39
37
|
# @return [OptionParser] an instance of OptionParser
|
@@ -46,9 +44,17 @@ module SmartTodo
|
|
46
44
|
opts.on('--fallback_channel CHANNEL') do |channel|
|
47
45
|
@options[:fallback_channel] = channel
|
48
46
|
end
|
47
|
+
opts.on('--dispatcher DISPATCHER') do |dispatcher|
|
48
|
+
@options[:dispatcher] = dispatcher
|
49
|
+
end
|
49
50
|
end
|
50
51
|
end
|
51
52
|
|
53
|
+
# @return [Class] a Dispatchers::Base subclass
|
54
|
+
def dispatcher
|
55
|
+
@dispatcher ||= Dispatchers::Base.class_for(@options[:dispatcher])
|
56
|
+
end
|
57
|
+
|
52
58
|
# @param path [String] a path to a file or directory
|
53
59
|
# @return [Array<String>] all the directories the parser should run on
|
54
60
|
def normalize_path(path)
|
@@ -67,7 +73,7 @@ module SmartTodo
|
|
67
73
|
event_message = Events.public_send(event.method_name, *event.arguments)
|
68
74
|
end
|
69
75
|
|
70
|
-
|
76
|
+
dispatcher.new(event_message, todo_node, file, @options).dispatch if event_met
|
71
77
|
end
|
72
78
|
end
|
73
79
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartTodo
|
4
|
+
module Dispatchers
|
5
|
+
class Base
|
6
|
+
# Factory pattern to retrive the right dispatcher class.
|
7
|
+
#
|
8
|
+
# @param dispatcher [String]
|
9
|
+
#
|
10
|
+
# @return [Class]
|
11
|
+
def self.class_for(dispatcher)
|
12
|
+
case dispatcher
|
13
|
+
when "slack"
|
14
|
+
Slack
|
15
|
+
when nil, 'output'
|
16
|
+
Output
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Subclasses should define what options from the CLI they need in order
|
21
|
+
# to properly deliver the message. For instance the Slack dispatcher
|
22
|
+
# requires an API key.
|
23
|
+
#
|
24
|
+
# @param _options [Hash]
|
25
|
+
#
|
26
|
+
# @return void
|
27
|
+
def self.validate_options!(_options)
|
28
|
+
raise(NotImplemetedError, 'subclass responsability')
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param event_message [String] the success message associated
|
32
|
+
# a specific event
|
33
|
+
# @param todo_node [SmartTodo::Parser::TodoNode]
|
34
|
+
# @param file [String] the file containing the TODO
|
35
|
+
# @param options [Hash]
|
36
|
+
def initialize(event_message, todo_node, file, options)
|
37
|
+
@event_message = event_message
|
38
|
+
@todo_node = todo_node
|
39
|
+
@options = options
|
40
|
+
@file = file
|
41
|
+
@assignee = @todo_node.metadata.assignee
|
42
|
+
end
|
43
|
+
|
44
|
+
# This method gets called when a TODO reminder is expired and needs to be delivered.
|
45
|
+
# Dispatchers should implement this method to deliver the message where they need.
|
46
|
+
#
|
47
|
+
# @return void
|
48
|
+
def dispatch
|
49
|
+
raise(NotImplemetedError, 'subclass responsability')
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Prepare the content of the message to send to the TODO assignee
|
55
|
+
#
|
56
|
+
# @param user [Hash] contain information about a user
|
57
|
+
# @return [String]
|
58
|
+
def slack_message(user)
|
59
|
+
header = if user.key?('fallback')
|
60
|
+
unexisting_user
|
61
|
+
else
|
62
|
+
existing_user
|
63
|
+
end
|
64
|
+
|
65
|
+
<<~EOM
|
66
|
+
#{header}
|
67
|
+
|
68
|
+
You have an assigned TODO in the `#{@file}` file.
|
69
|
+
#{@event_message}
|
70
|
+
|
71
|
+
Here is the associated comment on your TODO:
|
72
|
+
|
73
|
+
```
|
74
|
+
#{@todo_node.comment.strip}
|
75
|
+
```
|
76
|
+
EOM
|
77
|
+
end
|
78
|
+
|
79
|
+
# Message in case a TODO's assignee doesn't exist in the Slack organization
|
80
|
+
#
|
81
|
+
# @return [String]
|
82
|
+
def unexisting_user
|
83
|
+
"Hello :wave:,\n\n`#{@assignee}` had an assigned TODO but this user or channel doesn't exist on Slack anymore."
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param user [Hash]
|
87
|
+
def existing_user
|
88
|
+
"Hello :wave:,"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartTodo
|
4
|
+
module Dispatchers
|
5
|
+
# A simple dispatcher that will output the reminder.
|
6
|
+
class Output < Base
|
7
|
+
def self.validate_options!(_); end
|
8
|
+
|
9
|
+
# @return void
|
10
|
+
def dispatch
|
11
|
+
puts slack_message({})
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartTodo
|
4
|
+
module Dispatchers
|
5
|
+
# Dispatcher that sends TODO reminders on Slack. Assignees can be either individual
|
6
|
+
# (using the associated slack email address) or a channel.
|
7
|
+
class Slack < Base
|
8
|
+
def self.validate_options!(options)
|
9
|
+
options[:slack_token] ||= ENV.fetch('SMART_TODO_SLACK_TOKEN') { raise(ArgumentError, 'Missing :slack_token') }
|
10
|
+
|
11
|
+
options.fetch(:fallback_channel) { raise(ArgumentError, 'Missing :fallback_channel') }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Make a Slack API call to dispatch the message to the user or channel
|
15
|
+
#
|
16
|
+
# @raise [SlackClient::Error] in case the Slack API returns an error
|
17
|
+
# other than `users_not_found`
|
18
|
+
#
|
19
|
+
# @return [Hash] the Slack response
|
20
|
+
def dispatch
|
21
|
+
user = slack_user_or_channel
|
22
|
+
|
23
|
+
client.post_message(user.dig('user', 'id'), slack_message(user))
|
24
|
+
rescue SlackClient::Error => error
|
25
|
+
if %w(users_not_found channel_not_found).include?(error.error_code)
|
26
|
+
user = { 'user' => { 'id' => @options[:fallback_channel] }, 'fallback' => true }
|
27
|
+
else
|
28
|
+
raise(error)
|
29
|
+
end
|
30
|
+
|
31
|
+
client.post_message(user.dig('user', 'id'), slack_message(user))
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Returns a formatted hash containing either the user id of a slack user or
|
37
|
+
# the channel the message should be sent to.
|
38
|
+
#
|
39
|
+
# @return [Hash] a suited hash containing the user ID for a given individual or a slack channel
|
40
|
+
def slack_user_or_channel
|
41
|
+
if email?
|
42
|
+
client.lookup_user_by_email(@assignee)
|
43
|
+
else
|
44
|
+
{ 'user' => { 'id' => @assignee } }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [SlackClient] an instance of SlackClient
|
49
|
+
def client
|
50
|
+
@client ||= SlackClient.new(@options[:slack_token])
|
51
|
+
end
|
52
|
+
|
53
|
+
# Check if the TODO's assignee is a specific user or a channel
|
54
|
+
#
|
55
|
+
# @return [true, false]
|
56
|
+
def email?
|
57
|
+
@assignee.include?("@")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/smart_todo/version.rb
CHANGED
data/lib/smart_todo.rb
CHANGED
@@ -6,7 +6,6 @@ require "smart_todo/events"
|
|
6
6
|
module SmartTodo
|
7
7
|
autoload :SlackClient, 'smart_todo/slack_client'
|
8
8
|
autoload :CLI, 'smart_todo/cli'
|
9
|
-
autoload :Dispatcher, 'smart_todo/dispatcher'
|
10
9
|
|
11
10
|
module Parser
|
12
11
|
autoload :CommentParser, 'smart_todo/parser/comment_parser'
|
@@ -19,4 +18,10 @@ module SmartTodo
|
|
19
18
|
autoload :GemRelease, 'smart_todo/events/gem_release'
|
20
19
|
autoload :IssueClose, 'smart_todo/events/issue_close'
|
21
20
|
end
|
21
|
+
|
22
|
+
module Dispatchers
|
23
|
+
autoload :Base, 'smart_todo/dispatchers/base'
|
24
|
+
autoload :Slack, 'smart_todo/dispatchers/slack'
|
25
|
+
autoload :Output, 'smart_todo/dispatchers/output'
|
26
|
+
end
|
22
27
|
end
|
data/lib/smart_todo_cop.rb
CHANGED
@@ -29,7 +29,9 @@ module RuboCop
|
|
29
29
|
def smart_todo?(comment)
|
30
30
|
metadata = ::SmartTodo::Parser::MetadataParser.parse(comment.gsub(/^#/, ''))
|
31
31
|
|
32
|
-
metadata.events.any? &&
|
32
|
+
metadata.events.any? &&
|
33
|
+
metadata.events.all? { |event| event.is_a?(::SmartTodo::Parser::MethodNode) } &&
|
34
|
+
metadata.assignee
|
33
35
|
end
|
34
36
|
end
|
35
37
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_todo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-09-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -109,7 +109,9 @@ files:
|
|
109
109
|
- exe/smart_todo
|
110
110
|
- lib/smart_todo.rb
|
111
111
|
- lib/smart_todo/cli.rb
|
112
|
-
- lib/smart_todo/
|
112
|
+
- lib/smart_todo/dispatchers/base.rb
|
113
|
+
- lib/smart_todo/dispatchers/output.rb
|
114
|
+
- lib/smart_todo/dispatchers/slack.rb
|
113
115
|
- lib/smart_todo/events.rb
|
114
116
|
- lib/smart_todo/events/date.rb
|
115
117
|
- lib/smart_todo/events/gem_release.rb
|
@@ -1,99 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SmartTodo
|
4
|
-
# The Dispatcher handles the logic to send the Slack message
|
5
|
-
# to the assignee once its TODO came to expiration.
|
6
|
-
class Dispatcher
|
7
|
-
# @param event_message [String] the success message associated
|
8
|
-
# a specific event
|
9
|
-
# @param todo_node [SmartTodo::Parser::TodoNode]
|
10
|
-
# @param file [String] the file containing the TODO
|
11
|
-
# @param options [Hash]
|
12
|
-
def initialize(event_message, todo_node, file, options)
|
13
|
-
@event_message = event_message
|
14
|
-
@todo_node = todo_node
|
15
|
-
@options = options
|
16
|
-
@file = file
|
17
|
-
@assignee = @todo_node.metadata.assignee
|
18
|
-
end
|
19
|
-
|
20
|
-
# Make a Slack API call to dispatch the message to the user or channel
|
21
|
-
#
|
22
|
-
# @return [Hash] the Slack response
|
23
|
-
def dispatch
|
24
|
-
user = if email?
|
25
|
-
retrieve_slack_user
|
26
|
-
else
|
27
|
-
{ 'user' => { 'id' => @assignee, 'profile' => { 'first_name' => 'Team' } } }
|
28
|
-
end
|
29
|
-
|
30
|
-
client.post_message(user.dig('user', 'id'), slack_message(user))
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
# Retrieve the unique identifier of a Slack user with his email address
|
36
|
-
#
|
37
|
-
# @return [Hash] the Slack response containing the user ID
|
38
|
-
# @raise [SlackClient::Error] in case the Slack API returns an error
|
39
|
-
# other than `users_not_found`
|
40
|
-
def retrieve_slack_user
|
41
|
-
client.lookup_user_by_email(@assignee)
|
42
|
-
rescue SlackClient::Error => error
|
43
|
-
if error.error_code == 'users_not_found'
|
44
|
-
{ 'user' => { 'id' => @options[:fallback_channel] }, 'fallback' => true }
|
45
|
-
else
|
46
|
-
raise(error)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Prepare the content of the message to send to the TODO assignee
|
51
|
-
#
|
52
|
-
# @param user [Hash] contain information about a user
|
53
|
-
# @return [String]
|
54
|
-
def slack_message(user)
|
55
|
-
header = if user.key?('fallback')
|
56
|
-
unexisting_user
|
57
|
-
else
|
58
|
-
existing_user(user)
|
59
|
-
end
|
60
|
-
|
61
|
-
<<~EOM
|
62
|
-
#{header}
|
63
|
-
|
64
|
-
You have an assigned TODO in the `#{@file}` file.
|
65
|
-
#{@event_message}
|
66
|
-
|
67
|
-
Here is the associated comment on your TODO:
|
68
|
-
|
69
|
-
```
|
70
|
-
#{@todo_node.comment.strip}
|
71
|
-
```
|
72
|
-
EOM
|
73
|
-
end
|
74
|
-
|
75
|
-
# Message in case a TODO's assignee doesn't exist in the Slack organization
|
76
|
-
#
|
77
|
-
# @return [String]
|
78
|
-
def unexisting_user
|
79
|
-
"Hello :wave:,\n\n`#{@assignee}` had an assigned TODO but this user doesn't exist on Slack anymore."
|
80
|
-
end
|
81
|
-
|
82
|
-
# @param user [Hash]
|
83
|
-
def existing_user(user)
|
84
|
-
"Hello #{user.dig('user', 'profile', 'first_name')} :wave:,"
|
85
|
-
end
|
86
|
-
|
87
|
-
# @return [SlackClient] an instance of SlackClient
|
88
|
-
def client
|
89
|
-
@client ||= SlackClient.new(@options[:slack_token])
|
90
|
-
end
|
91
|
-
|
92
|
-
# Check if the TODO's assignee is a specific user or a channel
|
93
|
-
#
|
94
|
-
# @return [true, false]
|
95
|
-
def email?
|
96
|
-
@assignee.include?("@")
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|