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 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