geet 0.26.0 → 0.27.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.
@@ -1,10 +1,23 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
3
4
  module Geet
4
5
  module Github
5
6
  class PR < AbstractIssue
7
+ extend T::Sig
8
+
9
+ sig { returns(T.nilable(String)) }
6
10
  attr_reader :node_id
7
11
 
12
+ sig {
13
+ override.params(
14
+ number: Integer,
15
+ api_interface: Geet::Github::ApiInterface,
16
+ title: String,
17
+ link: String,
18
+ node_id: T.nilable(String)
19
+ ).void
20
+ }
8
21
  def initialize(number, api_interface, title, link, node_id: nil)
9
22
  super(number, api_interface, title, link)
10
23
  @node_id = node_id
@@ -12,6 +25,16 @@ module Geet
12
25
 
13
26
  # See https://developer.github.com/v3/pulls/#create-a-pull-request
14
27
  #
28
+ sig {
29
+ params(
30
+ title: String,
31
+ description: String,
32
+ head: String,
33
+ api_interface: Geet::Github::ApiInterface,
34
+ base: String,
35
+ draft: T::Boolean
36
+ ).returns(Geet::Github::PR)
37
+ }
15
38
  def self.create(title, description, head, api_interface, base, draft: false)
16
39
  api_path = 'pulls'
17
40
 
@@ -20,19 +43,34 @@ module Geet
20
43
  head = "#{authenticated_user}:#{head}"
21
44
  end
22
45
 
23
- request_data = { title: title, body: description, head: head, base: base, draft: draft }
46
+ request_data = { title:, body: description, head:, base:, draft: }
24
47
 
25
- response = api_interface.send_request(api_path, data: request_data)
48
+ response = T.cast(api_interface.send_request(api_path, data: request_data), T::Hash[String, T.untyped])
26
49
 
27
- number, title, link = response.fetch_values('number', 'title', 'html_url')
28
- node_id = response['node_id']
50
+ number = T.cast(response.fetch('number'), Integer)
51
+ title = T.cast(response.fetch('title'), String)
52
+ link = T.cast(response.fetch('html_url'), String)
53
+ node_id = T.cast(response['node_id'], T.nilable(String))
29
54
 
30
55
  new(number, api_interface, title, link, node_id:)
31
56
  end
32
57
 
33
58
  # See https://developer.github.com/v3/pulls/#list-pull-requests
34
59
  #
35
- def self.list(api_interface, milestone: nil, assignee: nil, owner: nil, head: nil)
60
+ sig {
61
+ override.params(
62
+ api_interface: Geet::Github::ApiInterface,
63
+ milestone: T.nilable(Geet::Github::Milestone),
64
+ assignee: T.nilable(Geet::Github::User),
65
+ owner: T.nilable(String),
66
+ head: T.nilable(String),
67
+ type_filter:
68
+ T.nilable(
69
+ T.proc.params(issue_data: T::Hash[String, T.untyped]).returns(T::Boolean)
70
+ )
71
+ ).returns(T::Array[Geet::Github::PR])
72
+ }
73
+ def self.list(api_interface, milestone: nil, assignee: nil, owner: nil, head: nil, &type_filter)
36
74
  check_list_params!(milestone, assignee, head)
37
75
 
38
76
  if head
@@ -44,7 +82,10 @@ module Geet
44
82
  # For upstream pulls, the owner is the authenticated user, otherwise, the repository owner.
45
83
  #
46
84
  response = if api_interface.upstream?
47
- unfiltered_response = api_interface.send_request(api_path, multipage: true)
85
+ unfiltered_response = T.cast(
86
+ api_interface.send_request(api_path, multipage: true),
87
+ T::Array[T::Hash[String, T.untyped]]
88
+ )
48
89
 
49
90
  # VERY weird. From the docs, it's not clear if the user/org is required in the `head` parameter,
50
91
  # but:
@@ -54,29 +95,44 @@ module Geet
54
95
  #
55
96
  # For this reason, we can't use that param, and have to filter manually.
56
97
  #
57
- unfiltered_response.select { |pr_data| pr_data.fetch('head').fetch('label') == "#{owner}:#{head}" }
98
+ unfiltered_response.select do |pr_data|
99
+ pr_head = T.cast(pr_data.fetch('head'), T::Hash[String, T.untyped])
100
+ label = T.cast(pr_head.fetch('label'), String)
101
+
102
+ label == "#{owner}:#{head}"
103
+ end
58
104
  else
59
105
  request_params = { head: "#{owner}:#{head}" }
60
106
 
61
- api_interface.send_request(api_path, params: request_params, multipage: true)
107
+ T.cast(
108
+ api_interface.send_request(api_path, params: request_params, multipage: true),
109
+ T::Array[T::Hash[String, T.untyped]]
110
+ )
62
111
  end
63
112
 
64
113
  response.map do |pr_data|
65
- number = pr_data.fetch('number')
66
- title = pr_data.fetch('title')
67
- link = pr_data.fetch('html_url')
114
+ number = T.cast(pr_data.fetch('number'), Integer)
115
+ title = T.cast(pr_data.fetch('title'), String)
116
+ link = T.cast(pr_data.fetch('html_url'), String)
68
117
 
69
118
  new(number, api_interface, title, link)
70
119
  end
71
120
  else
72
- super(api_interface, milestone: milestone, assignee: assignee) do |issue_data|
121
+ result = super(api_interface, milestone:, assignee:) do |issue_data|
73
122
  issue_data.key?('pull_request')
74
123
  end
124
+
125
+ T.cast(result, T::Array[Geet::Github::PR])
75
126
  end
76
127
  end
77
128
 
78
129
  # See https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button
79
130
  #
131
+ sig {
132
+ params(
133
+ merge_method: T.nilable(String)
134
+ ).void
135
+ }
80
136
  def merge(merge_method: nil)
81
137
  api_path = "pulls/#{number}/merge"
82
138
  request_data = { merge_method: } if merge_method
@@ -84,9 +140,14 @@ module Geet
84
140
  @api_interface.send_request(api_path, http_method: :put, data: request_data)
85
141
  end
86
142
 
143
+ sig {
144
+ params(
145
+ reviewers: T::Array[String]
146
+ ).void
147
+ }
87
148
  def request_review(reviewers)
88
149
  api_path = "pulls/#{number}/requested_reviewers"
89
- request_data = { reviewers: reviewers }
150
+ request_data = { reviewers: }
90
151
 
91
152
  @api_interface.send_request(api_path, data: request_data)
92
153
  end
@@ -96,6 +157,7 @@ module Geet
96
157
  # (see method comment below for the priority).
97
158
  # See https://docs.github.com/en/graphql/reference/mutations#enablepullrequestautomerge
98
159
  #
160
+ sig { returns(String) }
99
161
  def enable_automerge
100
162
  merge_method = fetch_available_merge_method
101
163
 
@@ -116,35 +178,48 @@ module Geet
116
178
  variables = { pullRequestId: @node_id, mergeMethod: merge_method }
117
179
 
118
180
  @api_interface.send_graphql_request(query, variables:)
181
+
182
+ merge_method
119
183
  end
120
184
 
121
185
  private
122
186
 
123
187
  # Query the repository to find the first available merge method.
124
- # Priority: MERGE > SQUASH > REBASE.
188
+ # Priority:
189
+ # - If there is one commit and squash merge is supported: SQUASH
190
+ # - Otherwise, if merge commit is supported: MERGE
191
+ # - Otherwise: SQUASH > REBASE
125
192
  #
193
+ sig { returns(String) }
126
194
  def fetch_available_merge_method
127
195
  query = <<~GRAPHQL
128
- query($owner: String!, $name: String!) {
196
+ query($owner: String!, $name: String!, $number: Int!) {
129
197
  repository(owner: $owner, name: $name) {
130
198
  mergeCommitAllowed
131
199
  squashMergeAllowed
132
200
  rebaseMergeAllowed
201
+ pullRequest(number: $number) {
202
+ commits {
203
+ totalCount
204
+ }
205
+ }
133
206
  }
134
207
  }
135
208
  GRAPHQL
136
209
 
137
- owner, name = @api_interface.repository_path.split('/')
210
+ owner, name = T.must(@api_interface.repository_path).split('/')
138
211
 
139
- response = @api_interface.send_graphql_request(query, variables: {owner:, name:})
212
+ response = @api_interface.send_graphql_request(query, variables: {owner:, name:, number:})
140
213
  repo_data = response['repository'].transform_keys(&:to_sym)
214
+ commit_count = repo_data[:pullRequest]['commits']['totalCount']
141
215
 
142
- case repo_data
143
- in { mergeCommitAllowed: true }
216
+ if commit_count == 1 && repo_data[:squashMergeAllowed]
217
+ 'SQUASH'
218
+ elsif repo_data[:mergeCommitAllowed]
144
219
  'MERGE'
145
- in { squashMergeAllowed: true }
220
+ elsif repo_data[:squashMergeAllowed]
146
221
  'SQUASH'
147
- in { rebaseMergeAllowed: true }
222
+ elsif repo_data[:rebaseMergeAllowed]
148
223
  'REBASE'
149
224
  else
150
225
  raise 'No merge methods are allowed on this repository'
@@ -152,8 +227,17 @@ module Geet
152
227
  end
153
228
 
154
229
  class << self
230
+ extend T::Sig
231
+
155
232
  private
156
233
 
234
+ sig {
235
+ params(
236
+ milestone: T.nilable(Geet::Github::Milestone),
237
+ assignee: T.nilable(Geet::Github::User),
238
+ head: T.nilable(String)
239
+ ).void
240
+ }
157
241
  def check_list_params!(milestone, assignee, head)
158
242
  if (milestone || assignee) && head
159
243
  raise "Head can't be specified with milestone or assignee!"
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
3
4
  module Geet
4
5
  module Github
@@ -10,10 +11,19 @@ module Geet
10
11
  # duplication. All in all, for simplicity, the latter design is chosen, but is subject to redesign.
11
12
  #
12
13
  class RemoteRepository
14
+ extend T::Sig
15
+
13
16
  # Nil if the repository is not a fork.
14
17
  #
18
+ sig { returns(T.nilable(String)) }
15
19
  attr_reader :parent_path
16
20
 
21
+ sig {
22
+ params(
23
+ api_interface: Geet::Github::ApiInterface,
24
+ parent_path: T.nilable(String)
25
+ ).void
26
+ }
17
27
  def initialize(api_interface, parent_path: nil)
18
28
  @api_interface = api_interface
19
29
  @parent_path = parent_path
@@ -23,14 +33,20 @@ module Geet
23
33
  #
24
34
  # https://docs.github.com/en/rest/reference/repos#get-a-repository
25
35
  #
36
+ sig {
37
+ params(
38
+ api_interface: Geet::Github::ApiInterface
39
+ ).returns(Geet::Github::RemoteRepository)
40
+ }
26
41
  def self.find(api_interface)
27
42
  api_path = "/repos/#{api_interface.repository_path}"
28
43
 
29
- response = api_interface.send_request(api_path)
44
+ response = T.cast(api_interface.send_request(api_path), T::Hash[String, T.untyped])
30
45
 
31
- parent_path = response['parent']&.fetch("full_name")
46
+ parent_hash = T.cast(response['parent'], T.nilable(T::Hash[String, T.untyped]))
47
+ parent_path = T.cast(parent_hash&.fetch("full_name)"), T.nilable(String))
32
48
 
33
- self.new(api_interface, parent_path: parent_path)
49
+ new(api_interface, parent_path:)
34
50
  end
35
51
  end # module RemoteRepository
36
52
  end # module GitHub
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
3
4
  module Geet
4
5
  module Github
5
6
  class User
7
+ extend T::Sig
6
8
  include Geet::Shared::RepoPermissions
7
9
 
10
+ sig { returns(String) }
8
11
  attr_reader :username
9
12
 
13
+ sig {
14
+ params(
15
+ username: String,
16
+ api_interface: Geet::Github::ApiInterface
17
+ ).void
18
+ }
10
19
  def initialize(username, api_interface)
11
20
  @username = username
12
21
  @api_interface = api_interface
@@ -14,6 +23,11 @@ module Geet
14
23
 
15
24
  # See #repo_permission.
16
25
  #
26
+ sig {
27
+ params(
28
+ permission: String
29
+ ).returns(T::Boolean)
30
+ }
17
31
  def has_permission?(permission)
18
32
  user_permission = self.class.repo_permission(@api_interface)
19
33
 
@@ -22,6 +36,7 @@ module Geet
22
36
 
23
37
  # See https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
24
38
  #
39
+ sig { returns(T::Boolean) }
25
40
  def is_collaborator?
26
41
  api_path = "collaborators/#{@username}"
27
42
 
@@ -42,32 +57,50 @@ module Geet
42
57
 
43
58
  # See https://developer.github.com/v3/users/#get-the-authenticated-user
44
59
  #
45
- def self.authenticated(api_interface, **_)
60
+ sig {
61
+ params(
62
+ api_interface: Geet::Github::ApiInterface
63
+ ).returns(Geet::Github::User)
64
+ }
65
+ def self.authenticated(api_interface)
46
66
  api_path = '/user'
47
67
 
48
- response = api_interface.send_request(api_path)
68
+ response = T.cast(api_interface.send_request(api_path), T::Hash[String, T.untyped])
49
69
 
50
- new(response.fetch('login'), api_interface)
70
+ login = T.cast(response.fetch('login'), String)
71
+
72
+ new(login, api_interface)
51
73
  end
52
74
 
53
- # Returns an array of User instances
54
- #
55
- def self.list_collaborators(api_interface, **)
75
+ sig {
76
+ params(
77
+ api_interface: Geet::Github::ApiInterface
78
+ ).returns(T::Array[Geet::Github::User])
79
+ }
80
+ def self.list_collaborators(api_interface)
56
81
  api_path = 'collaborators'
57
- response = api_interface.send_request(api_path, multipage: true)
82
+ response = T.cast(api_interface.send_request(api_path, multipage: true), T::Array[T::Hash[String, T.untyped]])
58
83
 
59
- response.map { |user_entry| new(user_entry.fetch('login'), api_interface) }
84
+ response.map do |user_entry|
85
+ login = T.cast(user_entry.fetch('login'), String)
86
+ new(login, api_interface)
87
+ end
60
88
  end
61
89
 
62
90
  # See https://developer.github.com/v3/repos/collaborators/#review-a-users-permission-level
63
91
  #
92
+ sig {
93
+ params(
94
+ api_interface: Geet::Github::ApiInterface
95
+ ).returns(String)
96
+ }
64
97
  def self.repo_permission(api_interface)
65
98
  username = authenticated(api_interface).username
66
99
  api_path = "collaborators/#{username}/permission"
67
100
 
68
- response = api_interface.send_request(api_path)
101
+ response = T.cast(api_interface.send_request(api_path), T::Hash[String, T.untyped])
69
102
 
70
- permission = response.fetch('permission')
103
+ permission = T.cast(response.fetch('permission'), String)
71
104
 
72
105
  check_permission!(permission)
73
106
 
@@ -75,12 +108,15 @@ module Geet
75
108
  end
76
109
 
77
110
  class << self
111
+ extend T::Sig
112
+
78
113
  private
79
114
 
80
115
  # Future-proofing.
81
116
  #
117
+ sig { params(permission: String).void }
82
118
  def check_permission!(permission)
83
- raise "Unexpected permission #{permission.inspect}!" if !self::ALL_PERMISSIONS.include?(permission)
119
+ raise "Unexpected permission #{permission.inspect}!" if !Geet::Shared::RepoPermissions::ALL_PERMISSIONS.include?(permission)
84
120
  end
85
121
  end
86
122
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
3
4
  require 'cgi'
4
5
  require 'uri'
@@ -8,64 +9,86 @@ require 'json'
8
9
  module Geet
9
10
  module Gitlab
10
11
  class ApiInterface
12
+ extend T::Sig
13
+
11
14
  API_BASE_URL = 'https://gitlab.com/api/v4'
12
15
 
16
+ sig { returns(T.nilable(String)) }
13
17
  attr_reader :repository_path
14
18
 
15
19
  # repo_path: "path/namespace"; required for the current GitLab operations.
16
20
  # upstream: boolean; required for the current GitLab operations.
17
21
  #
22
+ sig {
23
+ params(
24
+ api_token: String,
25
+ repo_path: String,
26
+ upstream: T::Boolean
27
+ ).void
28
+ }
18
29
  def initialize(api_token, repo_path:, upstream:)
19
30
  @api_token = api_token
20
31
  @path_with_namespace = repo_path
21
32
  @upstream = upstream
33
+ @repository_path = T.let(nil, T.nilable(String))
22
34
  end
23
35
 
36
+ sig { returns(T::Boolean) }
24
37
  def upstream?
25
38
  @upstream
26
39
  end
27
40
 
41
+ sig {
42
+ params(
43
+ encoded: T::Boolean
44
+ ).returns(String)
45
+ }
28
46
  def path_with_namespace(encoded: false)
29
47
  encoded ? CGI.escape(@path_with_namespace) : @path_with_namespace
30
48
  end
31
49
 
32
50
  # Send a request.
33
51
  #
34
- # Returns the parsed response, or an Array, in case of multipage.
35
- # Where no body is present in the response, nil is returned.
36
- #
37
- # params:
38
- # :api_path: api path, will be appended to the API URL.
39
- # for root path, prepend a `/`:
40
- # - use `/gists` for `https://api.github.com/gists`
41
- # when owner/project/repos is included, don't prepend `/`:
42
- # - use `issues` for `https://api.github.com/myowner/myproject/repos/issues`
43
- # :params: (Hash)
44
- # :data: (Hash) if present, will generate a POST request, otherwise, a GET
45
- # :multipage: set true for paged Github responses (eg. issues); it will make the method
46
- # return an array, with the concatenated (parsed) responses
47
- # :http_method: symbol format of the method (:get, :patch, :post, :put and :delete)
48
- # :get and :post are automatically inferred by the present of :data; the other
49
- # cases must be specified.
50
- #
52
+ sig {
53
+ params(
54
+ # Appended to the API URL.
55
+ # for root path, prepend a `/`:
56
+ # - use `/gists` for `https://api.github.com/gists`
57
+ # when owner/project/repos is included, don't prepend `/`:
58
+ # - use `issues` for `https://api.github.com/myowner/myproject/repos/issues`
59
+ api_path: String,
60
+ params: T.nilable(T::Hash[Symbol, T.untyped]),
61
+ # If present, will generate a POST request, otherwise, a GET
62
+ data: T.nilable(T.any(T::Hash[Symbol, T.untyped], T::Array[T.untyped])),
63
+ # Set true for paged Github responses (eg. issues); it will make the method
64
+ multipage: T::Boolean,
65
+ # Method (:get, :patch, :post, :put and :delete)
66
+ # :get and :post are automatically inferred by the presence of :data; the other cases must be specified.
67
+ http_method: T.nilable(Symbol)
68
+ # Returns the parsed response, or an Array, in case of multipage.
69
+ # Where no body is present in the response, nil is returned.
70
+ ).returns(T.nilable(T.any(T::Hash[String, T.untyped], T::Array[T::Hash[String, T.untyped]])))
71
+ }
51
72
  def send_request(api_path, params: nil, data: nil, multipage: false, http_method: nil)
52
- address = api_url(api_path)
73
+ address = T.let(api_url(api_path), T.nilable(String))
53
74
  # filled only on :multipage
54
75
  parsed_responses = []
55
76
 
56
77
  loop do
57
- response = send_http_request(address, params: params, data: data, http_method: http_method)
78
+ response = send_http_request(T.must(address), params:, data:, http_method:)
58
79
 
59
- parsed_response = JSON.parse(response.body) if response.body
80
+ if response_body = response.body
81
+ parsed_response = JSON.parse(response_body)
82
+ end
60
83
 
61
84
  if error?(response)
62
- formatted_error = decode_and_format_error(parsed_response)
85
+ formatted_error = decode_and_format_error(T.cast(parsed_response, T::Hash[String, T.untyped]))
63
86
  raise(formatted_error)
64
87
  end
65
88
 
66
89
  return parsed_response if !multipage
67
90
 
68
- parsed_responses.concat(parsed_response)
91
+ parsed_responses.concat(T.cast(parsed_response, T::Array[T::Hash[String, T.untyped]]))
69
92
 
70
93
  address = link_next_page(response.to_hash)
71
94
 
@@ -79,10 +102,23 @@ module Geet
79
102
 
80
103
  private
81
104
 
105
+ sig {
106
+ params(
107
+ api_path: String
108
+ ).returns(String)
109
+ }
82
110
  def api_url(api_path)
83
111
  "#{API_BASE_URL}/#{api_path}"
84
112
  end
85
113
 
114
+ sig {
115
+ params(
116
+ address: String,
117
+ params: T.nilable(T::Hash[Symbol, T.untyped]),
118
+ data: T.nilable(T.any(T::Hash[Symbol, T.untyped], T::Array[T.untyped])),
119
+ http_method: T.nilable(Symbol)
120
+ ).returns(Net::HTTPResponse)
121
+ }
86
122
  def send_http_request(address, params: nil, data: nil, http_method: nil)
87
123
  uri = encode_uri(address, params)
88
124
  http_class = find_http_class(http_method, data)
@@ -97,16 +133,32 @@ module Geet
97
133
  end
98
134
  end
99
135
 
136
+ sig {
137
+ params(
138
+ address: String,
139
+ params: T.nilable(T::Hash[Symbol, T.untyped])
140
+ ).returns(URI::Generic)
141
+ }
100
142
  def encode_uri(address, params)
101
143
  address += '?' + URI.encode_www_form(params) if params
102
144
 
103
145
  URI(address)
104
146
  end
105
147
 
148
+ sig {
149
+ params(
150
+ response: Net::HTTPResponse
151
+ ).returns(T::Boolean)
152
+ }
106
153
  def error?(response)
107
154
  !response.code.start_with?('2')
108
155
  end
109
156
 
157
+ sig {
158
+ params(
159
+ parsed_response: T::Hash[String, T.untyped]
160
+ ).returns(String)
161
+ }
110
162
  def decode_and_format_error(parsed_response)
111
163
  if parsed_response.key?('error')
112
164
  parsed_response.fetch('error')
@@ -117,6 +169,11 @@ module Geet
117
169
  end
118
170
  end
119
171
 
172
+ sig {
173
+ params(
174
+ response_headers: T::Hash[String, T.untyped]
175
+ ).returns(T.nilable(String))
176
+ }
120
177
  def link_next_page(response_headers)
121
178
  # An array (or nil) is returned.
122
179
  link_header = Array(response_headers['link'])
@@ -126,6 +183,12 @@ module Geet
126
183
  link_header[0][/<(\S+)>; rel="next"/, 1]
127
184
  end
128
185
 
186
+ sig {
187
+ params(
188
+ http_method: T.nilable(Symbol),
189
+ data: T.nilable(Object)
190
+ ).returns(T.class_of(Net::HTTPRequest))
191
+ }
129
192
  def find_http_class(http_method, data)
130
193
  http_method ||= data ? :post : :get
131
194
 
@@ -1,10 +1,27 @@
1
1
  # frozen_string_literal: true
2
+ # typed: strict
2
3
 
3
4
  module Geet
4
5
  module Gitlab
5
6
  class Issue
6
- attr_reader :number, :title, :link
7
+ extend T::Sig
7
8
 
9
+ sig { returns(Integer) }
10
+ attr_reader :number
11
+
12
+ sig { returns(String) }
13
+ attr_reader :title
14
+
15
+ sig { returns(String) }
16
+ attr_reader :link
17
+
18
+ sig {
19
+ params(
20
+ number: Integer,
21
+ title: String,
22
+ link: String
23
+ ).void
24
+ }
8
25
  def initialize(number, title, link)
9
26
  @number = number
10
27
  @title = title
@@ -13,6 +30,13 @@ module Geet
13
30
 
14
31
  # See https://docs.gitlab.com/ee/api/issues.html#list-issues
15
32
  #
33
+ sig {
34
+ params(
35
+ api_interface: ApiInterface,
36
+ assignee: T.nilable(User),
37
+ milestone: T.nilable(Milestone)
38
+ ).returns(T::Array[Issue])
39
+ }
16
40
  def self.list(api_interface, assignee: nil, milestone: nil)
17
41
  api_path = "projects/#{api_interface.path_with_namespace(encoded: true)}/issues"
18
42
 
@@ -20,12 +44,15 @@ module Geet
20
44
  request_params[:assignee_id] = assignee.id if assignee
21
45
  request_params[:milestone] = milestone.title if milestone
22
46
 
23
- response = api_interface.send_request(api_path, params: request_params, multipage: true)
47
+ response = T.cast(
48
+ api_interface.send_request(api_path, params: request_params, multipage: true),
49
+ T::Array[T::Hash[String, T.untyped]]
50
+ )
24
51
 
25
52
  response.map do |issue_data, result|
26
- number = issue_data.fetch('iid')
27
- title = issue_data.fetch('title')
28
- link = issue_data.fetch('web_url')
53
+ number = T.cast(issue_data.fetch('iid'), Integer)
54
+ title = T.cast(issue_data.fetch('title'), String)
55
+ link = T.cast(issue_data.fetch('web_url'), String)
29
56
 
30
57
  new(number, title, link)
31
58
  end