client.go (3748B)
1 package main 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "net/http" 10 "net/url" 11 "path" 12 "strconv" 13 "time" 14 ) 15 16 const GitlabHosted string = "https://gitlab.com/api/v4" 17 18 type gError struct { 19 Message any 20 } 21 22 func (e gError) Error() string { 23 switch v := e.Message.(type) { 24 case string: 25 return v 26 } 27 return "unknown" 28 } 29 30 type Issue struct { 31 ID int `json:"iid"` 32 Title string `json:"title"` 33 Created time.Time `json:"created_at"` 34 Updated time.Time `json:"updated_at"` 35 Closed time.Time `json:"closed_at"` 36 State string `json:"state"` 37 Author struct { 38 ID int `json:"id"` 39 Username string `json:"username"` 40 } 41 Description string `json:"description"` 42 Labels []string `json:"labels"` 43 URL string `json:"web_url"` 44 } 45 46 type Client struct { 47 *http.Client 48 BaseURL string 49 Token string 50 } 51 52 func (c *Client) Issues(project string, search map[string]string) ([]Issue, error) { 53 p := path.Join("projects", url.PathEscape(project), "issues") 54 resp, err := c.get(p) 55 if err != nil { 56 return nil, err 57 } 58 defer resp.Body.Close() 59 var issues []Issue 60 if err := json.NewDecoder(resp.Body).Decode(&issues); err != nil { 61 return nil, fmt.Errorf("decode response: %w", err) 62 } 63 return issues, nil 64 } 65 66 func (c *Client) Issue(project string, id int) (*Issue, error) { 67 p := path.Join("projects", url.PathEscape(project), "issues", strconv.Itoa(id)) 68 resp, err := c.get(p) 69 if err != nil { 70 return nil, err 71 } 72 var issue Issue 73 if err := json.NewDecoder(resp.Body).Decode(&issue); err != nil { 74 return nil, fmt.Errorf("decode response: %w", err) 75 } 76 return &issue, nil 77 } 78 79 func (c *Client) Create(project, title, desc string) (*Issue, error) { 80 m := map[string]string{ 81 "title": title, 82 "description": desc, 83 } 84 b, err := json.Marshal(m) 85 if err != nil { 86 return nil, err 87 } 88 p := path.Join("projects", url.PathEscape(project), "issues") 89 resp, err := c.post(p, "application/json", bytes.NewReader(b)) 90 if err != nil { 91 return nil, err 92 } 93 var issue Issue 94 if err := json.NewDecoder(resp.Body).Decode(&issue); err != nil { 95 return nil, fmt.Errorf("decode created issue: %w", err) 96 } 97 return &issue, nil 98 } 99 100 func (c *Client) get(path string) (*http.Response, error) { 101 if c.BaseURL == "" { 102 c.BaseURL = GitlabHosted 103 } 104 u := fmt.Sprintf("%s/%s", c.BaseURL, path) 105 req, err := http.NewRequest(http.MethodGet, u, nil) 106 if err != nil { 107 return nil, err 108 } 109 return c.do(req) 110 } 111 112 func (c *Client) post(path, contentType string, body io.Reader) (*http.Response, error) { 113 if c.BaseURL == "" { 114 c.BaseURL = GitlabHosted 115 } 116 u := fmt.Sprintf("%s/%s", c.BaseURL, path) 117 req, err := http.NewRequest(http.MethodPost, u, body) 118 if err != nil { 119 return nil, err 120 } 121 req.Header.Set("Content-Type", contentType) 122 resp, err := c.do(req) 123 if err != nil { 124 return nil, err 125 } 126 if resp.StatusCode != http.StatusCreated { 127 return nil, fmt.Errorf("unexpected status %s", resp.Status) 128 } 129 return resp, nil 130 } 131 132 func (c *Client) do(req *http.Request) (*http.Response, error) { 133 if c.Client == nil { 134 c.Client = http.DefaultClient 135 } 136 if c.BaseURL == "" { 137 c.BaseURL = "https://gitlab.com/api/v4" 138 } 139 140 if c.Token != "" { 141 req.Header.Set("Authorization", "Bearer "+c.Token) 142 } 143 144 req.Header.Set("Accept", "application/json") 145 if *debug { 146 log.Println(req.Method, req.URL) 147 } 148 resp, err := c.Do(req) 149 if err != nil { 150 return nil, err 151 } 152 if resp.StatusCode >= http.StatusBadRequest { 153 var e gError 154 if err := json.NewDecoder(resp.Body).Decode(&e); err != nil { 155 resp.Body.Close() 156 return nil, fmt.Errorf("%s %s: %s: decode error message: %w", req.Method, req.URL, resp.Status, err) 157 } 158 resp.Body.Close() 159 return nil, fmt.Errorf("%s %s: %s: %w", req.Method, req.URL, resp.Status, e) 160 } 161 return resp, err 162 }