smart_todo 1.6.0 → 1.8.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: c17d1471d893e5a764dda2fa9adcfc73049b5c5a45d6678feca40104879065fc
4
- data.tar.gz: 85411f13bc189f060372e90a72fc1a545cd0030b6f67cf0fd75f1ff97dbeb755
3
+ metadata.gz: a7cac40289c5fb2fdf4bb143ee7cca9b1c74ce41c7e1fa426f7efc17364a2683
4
+ data.tar.gz: c93946f4131f64068e9c249c760829a33f3e88ae669f322bb00ea0647f68eb20
5
5
  SHA512:
6
- metadata.gz: bfe190ce26c9b3e65aea86f3d23e40a400c14edcac8c465b361341ac00a7ab7d59498c9c3edda92d7e180659cad96240148bce26dc3a1db77a92ee2e03575204
7
- data.tar.gz: f1c0386fbb5346a74c3f9c0c3b6228a9af29a810241793a5f44db5cbe7d7873c24e9dda987b65aff0d4984193be3652bec472431a48407badb2eb7610da506d6
6
+ metadata.gz: a6a84fd8bd731f7b31fe0033be57a50ba018af724ae29cfa16a4c9a0a96766e6335ee1c1145976002519aff1f69d6a28a3bf220963e42f55c28b051f02c85d80
7
+ data.tar.gz: 562151dd8c4fc2b8d55d72c004a701ef2c10f01d3e84f632b1a945ee969469e7dffd5473c7f109548ca105175b1a893f1e039a9ee091b3cb8034af7e47116082
@@ -12,7 +12,7 @@ jobs:
12
12
  name: Ruby ${{ matrix.version }}
13
13
  strategy:
14
14
  matrix:
15
- version: [3.0, 3.1, 3.2]
15
+ version: [3.0, 3.1, 3.2, 3.3]
16
16
 
17
17
  steps:
18
18
  - uses: actions/checkout@v2
@@ -8,10 +8,9 @@ jobs:
8
8
 
9
9
  steps:
10
10
  - uses: actions/checkout@v2
11
- - name: Set up Ruby 3.0
11
+ - name: Set up Ruby
12
12
  uses: ruby/setup-ruby@v1
13
13
  with:
14
- ruby-version: 3.0
15
14
  bundler-cache: true
16
15
  - name: Install gems
17
16
  run: |
data/.rubocop.yml CHANGED
@@ -2,7 +2,6 @@ inherit_gem:
2
2
  rubocop-shopify: rubocop.yml
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 3.0
6
5
  NewCops: disable
7
6
  SuggestExtensions: false
8
7
  Exclude:
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.0
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- smart_todo (1.6.0)
5
- rexml
4
+ smart_todo (1.8.0)
5
+ prism (~> 0.15)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
@@ -13,34 +13,37 @@ GEM
13
13
  crack (0.4.5)
14
14
  rexml
15
15
  hashdiff (1.0.1)
16
- json (2.6.3)
16
+ json (2.7.1)
17
+ language_server-protocol (3.17.0.3)
17
18
  minitest (5.14.4)
18
- parallel (1.23.0)
19
- parser (3.2.2.3)
19
+ parallel (1.24.0)
20
+ parser (3.3.0.5)
20
21
  ast (~> 2.4.1)
21
22
  racc
23
+ prism (0.17.0)
22
24
  public_suffix (4.0.6)
23
- racc (1.7.0)
25
+ racc (1.7.3)
24
26
  rainbow (3.1.1)
25
27
  rake (13.0.6)
26
- regexp_parser (2.8.1)
27
- rexml (3.2.5)
28
- rubocop (1.52.1)
28
+ regexp_parser (2.9.0)
29
+ rexml (3.2.6)
30
+ rubocop (1.62.1)
29
31
  json (~> 2.3)
32
+ language_server-protocol (>= 3.17.0)
30
33
  parallel (~> 1.10)
31
- parser (>= 3.2.2.3)
34
+ parser (>= 3.3.0.2)
32
35
  rainbow (>= 2.2.2, < 4.0)
33
36
  regexp_parser (>= 1.8, < 3.0)
34
37
  rexml (>= 3.2.5, < 4.0)
35
- rubocop-ast (>= 1.28.0, < 2.0)
38
+ rubocop-ast (>= 1.31.1, < 2.0)
36
39
  ruby-progressbar (~> 1.7)
37
40
  unicode-display_width (>= 2.4.0, < 3.0)
38
- rubocop-ast (1.29.0)
39
- parser (>= 3.2.1.0)
41
+ rubocop-ast (1.31.2)
42
+ parser (>= 3.3.0.4)
40
43
  rubocop-shopify (2.14.0)
41
44
  rubocop (~> 1.51)
42
45
  ruby-progressbar (1.13.0)
43
- unicode-display_width (2.4.2)
46
+ unicode-display_width (2.5.0)
44
47
  webmock (3.11.2)
45
48
  addressable (>= 2.3.6)
46
49
  crack (>= 0.3.2)
@@ -58,4 +61,4 @@ DEPENDENCIES
58
61
  webmock
59
62
 
60
63
  BUNDLED WITH
61
- 2.2.25
64
+ 2.5.7
data/bin/profile ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "smart_todo"
6
+
7
+ class NullDispatcher < SmartTodo::Dispatchers::Base
8
+ class << self
9
+ def validate_options!(_); end
10
+ end
11
+
12
+ def dispatch
13
+ end
14
+ end
15
+
16
+ exit SmartTodo::CLI.new(NullDispatcher).run
data/dev.yml CHANGED
@@ -4,7 +4,7 @@ type:
4
4
  - ruby
5
5
 
6
6
  up:
7
- - ruby: 3.0.2
7
+ - ruby
8
8
  - bundler
9
9
 
10
10
  console:
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optionparser"
4
+ require "etc"
4
5
 
5
6
  module SmartTodo
6
7
  # This class is the entrypoint of the SmartTodo library and is responsible
7
8
  # to retrieve the command line options as well as iterating over each files/directories
8
9
  # to run the +CommentParser+ on.
9
10
  class CLI
10
- def initialize
11
+ def initialize(dispatcher = nil)
11
12
  @options = {}
12
13
  @errors = []
14
+ @dispatcher = dispatcher
13
15
  end
14
16
 
15
17
  # @param args [Array<String>]
@@ -19,15 +21,18 @@ module SmartTodo
19
21
 
20
22
  paths << "." if paths.empty?
21
23
 
24
+ comment_parser = CommentParser.new
22
25
  paths.each do |path|
23
- normalize_path(path).each do |file|
24
- parse_file(file)
26
+ normalize_path(path).each do |filepath|
27
+ comment_parser.parse_file(filepath)
25
28
 
26
29
  $stdout.print(".")
27
30
  $stdout.flush
28
31
  end
29
32
  end
30
33
 
34
+ process_dispatches(process_todos(comment_parser.todos))
35
+
31
36
  if @errors.empty?
32
37
  0
33
38
  else
@@ -79,14 +84,17 @@ module SmartTodo
79
84
  end
80
85
  end
81
86
 
82
- # @param file [String] a path to a file
83
- def parse_file(file)
84
- Parser::CommentParser.new(File.read(file, encoding: "UTF-8")).parse.each do |todo_node|
87
+ def process_todos(todos)
88
+ events = Events.new
89
+ dispatches = []
90
+
91
+ todos.each do |todo|
85
92
  event_message = nil
86
- event_met = todo_node.metadata.events.find do |event|
87
- event_message = Events.public_send(event.method_name, *event.arguments)
93
+ event_met = todo.events.find do |event|
94
+ event_message = events.public_send(event.method_name, *event.arguments)
88
95
  rescue => e
89
- message = "Error while parsing #{file} on event `#{event.method_name}` with arguments #{event.arguments}: " \
96
+ message = "Error while parsing #{todo.filepath} on event `#{event.method_name}` " \
97
+ "with arguments #{event.arguments.map(&:inspect)}: " \
90
98
  "#{e.message}"
91
99
 
92
100
  @errors << message
@@ -94,10 +102,36 @@ module SmartTodo
94
102
  nil
95
103
  end
96
104
 
97
- @errors.concat(todo_node.metadata.errors)
98
-
99
- dispatcher.new(event_message, todo_node, file, @options).dispatch if event_met
105
+ @errors.concat(todo.errors)
106
+ dispatches << [event_message, todo] if event_met
100
107
  end
108
+
109
+ dispatches
110
+ end
111
+
112
+ def process_dispatches(dispatches)
113
+ queue = Queue.new
114
+ dispatches.each { |dispatch| queue << dispatch }
115
+
116
+ thread_count = Etc.nprocessors
117
+ thread_count.times { queue << nil }
118
+
119
+ threads =
120
+ thread_count.times.map do
121
+ Thread.new do
122
+ Thread.current.abort_on_exception = true
123
+
124
+ loop do
125
+ dispatch = queue.pop
126
+ break if dispatch.nil?
127
+
128
+ (event_message, todo) = dispatch
129
+ dispatcher.new(event_message, todo, todo.filepath, @options).dispatch
130
+ end
131
+ end
132
+ end
133
+
134
+ threads.each(&:join)
101
135
  end
102
136
  end
103
137
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartTodo
4
+ class CommentParser
5
+ attr_reader :todos
6
+
7
+ def initialize
8
+ @todos = []
9
+ end
10
+
11
+ if Prism.respond_to?(:parse_comments)
12
+ def parse(source, filepath = "-e")
13
+ parse_comments(Prism.parse_comments(source), filepath)
14
+ end
15
+
16
+ def parse_file(filepath)
17
+ parse_comments(Prism.parse_file_comments(filepath), filepath)
18
+ end
19
+ else
20
+ def parse(source, filepath = "-e")
21
+ parse_comments(Prism.parse(source, filepath).comments, filepath)
22
+ end
23
+
24
+ def parse_file(filepath)
25
+ parse_comments(Prism.parse_file(filepath).comments, filepath)
26
+ end
27
+ end
28
+
29
+ class << self
30
+ def parse(source)
31
+ parser = new
32
+ parser.parse(source)
33
+ parser.todos
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ if defined?(Prism::InlineComment)
40
+ def inline?(comment)
41
+ comment.is_a?(Prism::InlineComment)
42
+ end
43
+ else
44
+ def inline?(comment)
45
+ comment.type == :inline
46
+ end
47
+ end
48
+
49
+ def parse_comments(comments, filepath)
50
+ current_todo = nil
51
+
52
+ comments.each do |comment|
53
+ next unless inline?(comment)
54
+
55
+ source = comment.location.slice
56
+
57
+ if source.match?(/^#\sTODO\(/)
58
+ todos << current_todo if current_todo
59
+ current_todo = Todo.new(source, filepath)
60
+ elsif current_todo && (indent = source[/^#(\s*)/, 1].length) && (indent - current_todo.indent == 2)
61
+ current_todo << "#{source[(indent + 1)..]}\n"
62
+ else
63
+ todos << current_todo if current_todo
64
+ current_todo = nil
65
+ end
66
+ end
67
+
68
+ todos << current_todo if current_todo
69
+ end
70
+ end
71
+ end
@@ -40,7 +40,7 @@ module SmartTodo
40
40
  @todo_node = todo_node
41
41
  @options = options
42
42
  @file = file
43
- @assignees = @todo_node.metadata.assignees
43
+ @assignees = @todo_node.assignees
44
44
  end
45
45
 
46
46
  # This method gets called when a TODO reminder is expired and needs to be delivered.
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ gem("bundler")
4
+ require "bundler"
5
+ require "net/http"
6
+ require "time"
7
+ require "json"
8
+
3
9
  module SmartTodo
4
10
  # This module contains all the methods accessible for SmartTodo comments.
5
11
  # It is meant to be reopened by the host application in order to define
@@ -10,7 +16,7 @@ module SmartTodo
10
16
  #
11
17
  # @example Adding a custom event
12
18
  # module SmartTodo
13
- # module Events
19
+ # class Events
14
20
  # def trello_card_close(card)
15
21
  # ...
16
22
  # end
@@ -18,33 +24,87 @@ module SmartTodo
18
24
  # end
19
25
  #
20
26
  # TODO(on: trello_card_close(381), to: 'john@example.com')
21
- module Events
22
- extend self
27
+ #
28
+ class Events
29
+ def initialize(now: nil, spec_set: nil, current_ruby_version: nil)
30
+ @now = now
31
+ @spec_set = spec_set
32
+ @rubygems_client = nil
33
+ @github_client = nil
34
+ @current_ruby_version = current_ruby_version
35
+ end
23
36
 
24
37
  # Check if the +date+ is in the past
25
38
  #
26
- # @param date [String] a correctly formatted date
39
+ # @param on_date [String] a string parsable by Time.parse
27
40
  # @return [false, String]
28
- def date(date)
29
- Date.met?(date)
41
+ def date(on_date)
42
+ if now >= Time.parse(on_date)
43
+ "We are past the *#{on_date}* due date and " \
44
+ "your TODO is now ready to be addressed."
45
+ else
46
+ false
47
+ end
30
48
  end
31
49
 
32
50
  # Check if a new version of +gem_name+ was released with the +requirements+ expected
33
51
  #
52
+ # @example Expecting a specific version
53
+ # gem_release('rails', '6.0')
54
+ #
55
+ # @example Expecting a version in the 5.x.x series
56
+ # gem_release('rails', '> 5.2', '< 6')
57
+ #
34
58
  # @param gem_name [String]
35
59
  # @param requirements [Array<String>] a list of version specifiers
36
60
  # @return [false, String]
37
61
  def gem_release(gem_name, *requirements)
38
- GemRelease.new(gem_name, requirements).met?
62
+ response = rubygems_client.get("/api/v1/versions/#{gem_name}.json")
63
+
64
+ if response.code_type < Net::HTTPClientError
65
+ "The gem *#{gem_name}* doesn't seem to exist, I can't determine if " \
66
+ "your TODO is ready to be addressed."
67
+ else
68
+ requirement = Gem::Requirement.new(requirements)
69
+ version = JSON.parse(response.body).find { |gem| requirement.satisfied_by?(Gem::Version.new(gem["number"])) }
70
+
71
+ if version
72
+ "The gem *#{gem_name}* was released to version *#{version["number"]}* and " \
73
+ "your TODO is now ready to be addressed."
74
+ else
75
+ false
76
+ end
77
+ end
39
78
  end
40
79
 
41
80
  # Check if +gem_name+ was bumped to the +requirements+ expected
42
81
  #
82
+ # @example Expecting a specific version
83
+ # gem_bump('rails', '6.0')
84
+ #
85
+ # @example Expecting a version in the 5.x.x series
86
+ # gem_bump('rails', '> 5.2', '< 6')
87
+ #
43
88
  # @param gem_name [String]
44
89
  # @param requirements [Array<String>] a list of version specifiers
45
90
  # @return [false, String]
46
91
  def gem_bump(gem_name, *requirements)
47
- GemBump.new(gem_name, requirements).met?
92
+ specs = spec_set[gem_name]
93
+
94
+ if specs.empty?
95
+ "The gem *#{gem_name}* is not in your dependencies, I can't determine if " \
96
+ "your TODO is ready to be addressed."
97
+ else
98
+ requirement = Gem::Requirement.new(requirements)
99
+ version = specs.first.version
100
+
101
+ if requirement.satisfied_by?(version)
102
+ "The gem *#{gem_name}* was updated to version *#{version}* and " \
103
+ "your TODO is now ready to be addressed."
104
+ else
105
+ false
106
+ end
107
+ end
48
108
  end
49
109
 
50
110
  # Check if the issue +issue_number+ is closed
@@ -54,7 +114,22 @@ module SmartTodo
54
114
  # @param issue_number [String, Integer]
55
115
  # @return [false, String]
56
116
  def issue_close(organization, repo, issue_number)
57
- IssueClose.new(organization, repo, issue_number, type: "issues").met?
117
+ headers = github_headers(organization, repo)
118
+ response = github_client.get("/repos/#{organization}/#{repo}/issues/#{issue_number}", headers)
119
+
120
+ if response.code_type < Net::HTTPClientError
121
+ <<~EOM
122
+ I can't retrieve the information from the issue *#{issue_number}* in the *#{organization}/#{repo}* repository.
123
+
124
+ If the repository is a private one, make sure to export the `#{GITHUB_TOKEN}`
125
+ environment variable with a correct GitHub token.
126
+ EOM
127
+ elsif JSON.parse(response.body)["state"] == "closed"
128
+ "The issue https://github.com/#{organization}/#{repo}/issues/#{issue_number} is now closed, " \
129
+ "your TODO is ready to be addressed."
130
+ else
131
+ false
132
+ end
58
133
  end
59
134
 
60
135
  # Check if the pull request +pr_number+ is closed
@@ -64,7 +139,22 @@ module SmartTodo
64
139
  # @param pr_number [String, Integer]
65
140
  # @return [false, String]
66
141
  def pull_request_close(organization, repo, pr_number)
67
- IssueClose.new(organization, repo, pr_number, type: "pulls").met?
142
+ headers = github_headers(organization, repo)
143
+ response = github_client.get("/repos/#{organization}/#{repo}/pulls/#{pr_number}", headers)
144
+
145
+ if response.code_type < Net::HTTPClientError
146
+ <<~EOM
147
+ I can't retrieve the information from the PR *#{pr_number}* in the *#{organization}/#{repo}* repository.
148
+
149
+ If the repository is a private one, make sure to export the `#{GITHUB_TOKEN}`
150
+ environment variable with a correct GitHub token.
151
+ EOM
152
+ elsif JSON.parse(response.body)["state"] == "closed"
153
+ "The pull request https://github.com/#{organization}/#{repo}/pull/#{pr_number} is now closed, " \
154
+ "your TODO is ready to be addressed."
155
+ else
156
+ false
157
+ end
68
158
  end
69
159
 
70
160
  # Check if the installed ruby version meets requirements.
@@ -72,7 +162,61 @@ module SmartTodo
72
162
  # @param requirements [Array<String>] a list of version specifiers
73
163
  # @return [false, String]
74
164
  def ruby_version(*requirements)
75
- RubyVersion.new(requirements).met?
165
+ requirement = Gem::Requirement.new(requirements)
166
+
167
+ if requirement.satisfied_by?(current_ruby_version)
168
+ "The currently installed version of Ruby #{current_ruby_version} is #{requirement}."
169
+ else
170
+ false
171
+ end
172
+ end
173
+
174
+ private
175
+
176
+ def now
177
+ @now ||= Time.now
178
+ end
179
+
180
+ def spec_set
181
+ @spec_set ||= Bundler.load.specs
182
+ end
183
+
184
+ def rubygems_client
185
+ @rubygems_client ||= HttpClientBuilder.build("rubygems.org")
186
+ end
187
+
188
+ def github_client
189
+ @github_client ||= HttpClientBuilder.build("api.github.com")
190
+ end
191
+
192
+ def github_headers(organization, repo)
193
+ headers = { "Accept" => "application/vnd.github.v3+json" }
194
+
195
+ token = github_authorization_token(organization, repo)
196
+ headers["Authorization"] = "token #{token}" if token
197
+
198
+ headers
199
+ end
200
+
201
+ GITHUB_TOKEN = "SMART_TODO_GITHUB_TOKEN"
202
+
203
+ # @return [String, nil]
204
+ def github_authorization_token(organization, repo)
205
+ organization_name = organization.upcase.gsub(/[^A-Z0-9]/, "_")
206
+ repo_name = repo.upcase.gsub(/[^A-Z0-9]/, "_")
207
+
208
+ [
209
+ "#{GITHUB_TOKEN}__#{organization_name}__#{repo_name}",
210
+ "#{GITHUB_TOKEN}__#{organization_name}",
211
+ GITHUB_TOKEN,
212
+ ].find do |key|
213
+ token = ENV[key]
214
+ break token unless token.nil? || token.empty?
215
+ end
216
+ end
217
+
218
+ def current_ruby_version
219
+ @current_ruby_version ||= Gem::Version.new(RUBY_VERSION)
76
220
  end
77
221
  end
78
222
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module SmartTodo
6
+ # @api private
7
+ class HttpClientBuilder
8
+ class << self
9
+ def build(endpoint)
10
+ Net::HTTP.new(endpoint, Net::HTTP.https_default_port).tap do |client|
11
+ client.use_ssl = true
12
+ client.read_timeout = 30
13
+ client.ssl_timeout = 15
14
+ client.max_retries = 2
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -26,11 +26,7 @@ module SmartTodo
26
26
  # @param slack_token [String]
27
27
  def initialize(slack_token)
28
28
  @slack_token = slack_token
29
- @client = Net::HTTP.new("slack.com", Net::HTTP.https_default_port).tap do |client|
30
- client.use_ssl = true
31
- client.read_timeout = 30
32
- client.ssl_timeout = 15
33
- end
29
+ @client = HttpClientBuilder.build("slack.com")
34
30
  end
35
31
 
36
32
  # Retrieve the Slack ID of a user from his email
@@ -43,9 +39,9 @@ module SmartTodo
43
39
  #
44
40
  # @see https://api.slack.com/methods/users.lookupByEmail
45
41
  def lookup_user_by_email(email)
46
- headers = { "Content-Type" => "application/x-www-form-urlencoded" }
47
-
48
- request(:get, "/api/users.lookupByEmail?email=#{CGI.escape(email)}", nil, headers)
42
+ headers = default_headers.merge("Content-Type" => "application/x-www-form-urlencoded")
43
+ request = Net::HTTP::Get.new("/api/users.lookupByEmail?email=#{CGI.escape(email)}", headers)
44
+ dispatch(request)
49
45
  end
50
46
 
51
47
  # Send a message to a Slack channel or to a user
@@ -59,7 +55,10 @@ module SmartTodo
59
55
  #
60
56
  # @see https://api.slack.com/methods/chat.postMessage
61
57
  def post_message(channel, text)
62
- request(:post, "/api/chat.postMessage", JSON.dump(channel: channel, text: text))
58
+ headers = default_headers
59
+ request = Net::HTTP::Post.new("/api/chat.postMessage", headers)
60
+ request.body = JSON.dump(channel: channel, text: text)
61
+ dispatch(request)
63
62
  end
64
63
 
65
64
  private
@@ -71,27 +70,19 @@ module SmartTodo
71
70
  #
72
71
  # @raise [Net::HTTPError] in case the request to Slack failed
73
72
  # @raise [SlackClient::Error] in case Slack returns a { ok: false } in the body
74
- def request(method, endpoint, data = nil, headers = {})
75
- response = case method
76
- when :post, :patch
77
- @client.public_send(method, endpoint, data, default_headers.merge(headers))
78
- else
79
- @client.public_send(method, endpoint, default_headers.merge(headers))
80
- end
73
+ def dispatch(request, max_attempts = 5)
74
+ response = @client.request(request)
75
+ attempts = 1
81
76
 
82
- slack_response!(response)
83
- end
77
+ while response.is_a?(Net::HTTPTooManyRequests) && attempts < max_attempts
78
+ sleep([Integer(response["Retry-After"]), 600].min)
79
+ response = @client.request(request)
80
+ attempts += 1
81
+ end
84
82
 
85
- # Check if the response to Slack was a 200 and the Slack API request was successful
86
- #
87
- # @param response [Net::HTTPResponse] a net Net::HTTPResponse subclass
88
- # (Net::HTTPOK, Net::HTTPNotFound ...)
89
- # @return [Hash]
90
- #
91
- # @raise [Net::HTTPError] in case the request to Slack failed
92
- # @raise [SlackClient::Error] in case Slack returns a { ok: false } in the body
93
- def slack_response!(response)
94
- raise(Net::HTTPError.new("Request to slack failed", response)) unless response.code_type < Net::HTTPSuccess
83
+ unless response.code_type < Net::HTTPSuccess
84
+ raise(Net::HTTPError.new("Request to slack failed", response))
85
+ end
95
86
 
96
87
  body = JSON.parse(response.body)
97
88