/
zenhub.go
175 lines (153 loc) · 4.5 KB
/
zenhub.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package zenhub
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"time"
"github.com/44smkn/zenhub_exporter/pkg/model"
)
const (
ZenhubRootEndpoint = "https://api.zenhub.com"
ZenhubWorkspacesEndpoint = "/p2/repositories/%s/workspaces"
ZenhubBoardEndpoint = "/p2/workspaces/%s/repositories/%s/board"
)
func decodeBody(resp *http.Response, out interface{}) error {
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
return decoder.Decode(out)
}
//go:generate mockgen -destination=../../mocks/zenhub/zenhub.go -package=mocks_zenhub github.com/44smkn/zenhub_exporter/pkg/zenhub Client
type Client interface {
FetchWorkspaceIssues(ctx context.Context) ([]model.Issue, error)
}
func NewClient(token, repoID, workspaceName string, timeout time.Duration) Client {
parsedURL, _ := url.ParseRequestURI(ZenhubRootEndpoint)
httpClient := &http.Client{
Timeout: timeout,
}
return &defaultClient{
apiToken: token,
repoID: repoID,
workspaceName: workspaceName,
URL: parsedURL,
httpClient: httpClient,
}
}
var _ Client = &defaultClient{}
type defaultClient struct {
apiToken string
repoID string
workspaceName string
URL *url.URL
httpClient *http.Client
}
func (c *defaultClient) newRequest(ctx context.Context, method, spath string, body io.Reader) (*http.Request, error) {
u := *c.URL
u.Path = path.Join(c.URL.Path, spath)
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("X-Authentication-Token", c.apiToken)
req.Header.Add("Content-Type", "application/json")
return req, nil
}
func (c *defaultClient) FetchWorkspaceIssues(ctx context.Context) ([]model.Issue, error) {
workspace, err := c.fetchWorkspaces(ctx)
if err != nil {
return nil, err
}
issues := make([]model.Issue, 0, 200)
for _, repoID := range workspace.Repositories {
uri := fmt.Sprintf(ZenhubBoardEndpoint, workspace.ID, strconv.Itoa(repoID))
req, err := c.newRequest(ctx, http.MethodGet, uri, nil)
if err != nil {
return nil, err
}
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
board := &ZenHubBoard{}
if err := decodeBody(res, board); err != nil {
return nil, err
}
for _, pipeline := range board.Pipelines {
if pipeline.Issues == nil {
continue
}
for _, is := range pipeline.Issues {
var estimate *int
if is.Estimate != nil {
estimate = &is.Estimate.Value
}
issue := model.Issue{
RepoID: strconv.Itoa(repoID),
WorkspaceName: c.workspaceName,
IssueNumber: strconv.Itoa(is.IssueNumber),
Estimate: estimate,
IsEpic: is.IsEpic,
PipelineName: pipeline.Name,
}
issues = append(issues, issue)
}
}
}
return issues, nil
}
func (c *defaultClient) fetchWorkspaces(ctx context.Context) (*ZenHubWorkspace, error) {
uri := fmt.Sprintf(ZenhubWorkspacesEndpoint, c.repoID)
req, err := c.newRequest(ctx, http.MethodGet, uri, nil)
if err != nil {
return nil, err
}
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, errors.New(fmt.Sprintf("Requesting to %s is failed. Status: %s", uri, res.Status))
}
workspaces := make([]ZenHubWorkspace, 0, 10)
if err := decodeBody(res, &workspaces); err != nil {
return nil, err
}
for _, w := range workspaces {
if w.Name == c.workspaceName {
return &w, nil
}
}
return nil, errors.New(fmt.Sprintf("Not contains specified workspace: %s", c.workspaceName))
}
// ZenHubBoard is the object representing board in zenhub world.
// It is bound with Get board data API Response of zenhub.
// See: https://github.com/ZenHubIO/API#get-a-zenhub-board-for-a-repository
type ZenHubBoard struct {
Pipelines []struct {
ID string `json:"id"`
Name string `json:"name"`
Issues []struct {
IssueNumber int `json:"issue_number"`
Estimate *struct {
Value int `json:"value"`
} `json:"estimate,omitempty"`
Position int `json:"position,omitempty"`
IsEpic bool `json:"is_epic"`
} `json:"issues"`
} `json:"pipelines"`
}
// ZenHubWorkspace is the object representing workspace in zenhub world.
// It is bound with Get workspace data API Response of zenhub.
// See: https://github.com/ZenHubIO/API#get-zenhub-workspaces-for-a-repository
type ZenHubWorkspace struct {
Name string `json:"name"`
Description string `json:"description"`
ID string `json:"id"`
Repositories []int `json:"repositories"`
}