smart_todo 1.0.2 → 1.1.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.
- 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
|