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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4a658f8acb049243c04ca03be1c3cf4eacd84a648955c6bc0625f9c497ffbe5
4
- data.tar.gz: 49258142b68fc748a054bb36a3c207b16bb010709a21e935a28fe4bdb379cf02
3
+ metadata.gz: a61dfef2e6101321b34f154e8143441b3f28502709b26cb2db504fe83251ba1a
4
+ data.tar.gz: 3f12eef19063ed2f8585b56be3b483f1312dc44a7c96cc93931c8ae89a4ac7c5
5
5
  SHA512:
6
- metadata.gz: da4d883a649e9c090eabd1415a778503d272ad36baf505688a126bc37e6b010f78a72e3da27ad0b28706ae015f48c41115e3644b68b3c590f1e69918d1b32b39
7
- data.tar.gz: 7ff862bbc35a507e220dd40aca07b1b7719e5e47270456652c4c4bf39a7cf9634ef0132379bb2f5e52038b9eafa6e5fab22f2f26948f172907577dde573f271a
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- smart_todo (1.0.2)
4
+ smart_todo (1.1.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/exe/smart_todo CHANGED
@@ -5,10 +5,8 @@ $LOAD_PATH.unshift("#{__dir__}/../lib")
5
5
 
6
6
  require 'smart_todo'
7
7
 
8
- unless ENV['ENABLE_SMART_TODO']
9
- puts 'Not running SmartTodo since the ENABLE_SMART_TODO ENV is not set'
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
@@ -27,13 +27,11 @@ module SmartTodo
27
27
  end
28
28
  end
29
29
 
30
- # @raise [ArgumentError] if the +slack_token+ or the +fallback_channel+
31
- # options are not passed to the command line
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[:slack_token] ||= ENV.fetch('SMART_TODO_SLACK_TOKEN') { raise(ArgumentError, 'Missing :slack_token') }
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
- Dispatcher.new(event_message, todo_node, file, @options).dispatch if event_met
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmartTodo
4
- VERSION = "1.0.2"
4
+ VERSION = "1.1.0"
5
5
  end
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
@@ -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? && metadata.assignee
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.2
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-08-09 00:00:00.000000000 Z
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/dispatcher.rb
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