X Tutup
package api import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "regexp" "strings" "github.com/cli/cli/v2/internal/ghinstance" "github.com/henvic/httpretty" "github.com/shurcooL/graphql" ) // ClientOption represents an argument to NewClient type ClientOption = func(http.RoundTripper) http.RoundTripper // NewHTTPClient initializes an http.Client func NewHTTPClient(opts ...ClientOption) *http.Client { tr := http.DefaultTransport for _, opt := range opts { tr = opt(tr) } return &http.Client{Transport: tr} } // NewClient initializes a Client func NewClient(opts ...ClientOption) *Client { client := &Client{http: NewHTTPClient(opts...)} return client } // NewClientFromHTTP takes in an http.Client instance func NewClientFromHTTP(httpClient *http.Client) *Client { client := &Client{http: httpClient} return client } // AddHeader turns a RoundTripper into one that adds a request header func AddHeader(name, value string) ClientOption { return func(tr http.RoundTripper) http.RoundTripper { return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { if req.Header.Get(name) == "" { req.Header.Add(name, value) } return tr.RoundTrip(req) }} } } // AddHeaderFunc is an AddHeader that gets the string value from a function func AddHeaderFunc(name string, getValue func(*http.Request) (string, error)) ClientOption { return func(tr http.RoundTripper) http.RoundTripper { return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { if req.Header.Get(name) != "" { return tr.RoundTrip(req) } value, err := getValue(req) if err != nil { return nil, err } if value != "" { req.Header.Add(name, value) } return tr.RoundTrip(req) }} } } // VerboseLog enables request/response logging within a RoundTripper func VerboseLog(out io.Writer, logTraffic bool, colorize bool) ClientOption { logger := &httpretty.Logger{ Time: true, TLS: false, Colors: colorize, RequestHeader: logTraffic, RequestBody: logTraffic, ResponseHeader: logTraffic, ResponseBody: logTraffic, Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, MaxResponseBody: 10000, } logger.SetOutput(out) logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { return !inspectableMIMEType(h.Get("Content-Type")), nil }) return logger.RoundTripper } // ReplaceTripper substitutes the underlying RoundTripper with a custom one func ReplaceTripper(tr http.RoundTripper) ClientOption { return func(http.RoundTripper) http.RoundTripper { return tr } } type funcTripper struct { roundTrip func(*http.Request) (*http.Response, error) } func (tr funcTripper) RoundTrip(req *http.Request) (*http.Response, error) { return tr.roundTrip(req) } // Client facilitates making HTTP requests to the GitHub API type Client struct { http *http.Client } func (c *Client) HTTP() *http.Client { return c.http } type graphQLResponse struct { Data interface{} Errors []GraphQLError } // GraphQLError is a single error returned in a GraphQL response type GraphQLError struct { Type string Message string // Path []interface // mixed strings and numbers } // GraphQLErrorResponse contains errors returned in a GraphQL response type GraphQLErrorResponse struct { Errors []GraphQLError } func (gr GraphQLErrorResponse) Error() string { errorMessages := make([]string, 0, len(gr.Errors)) for _, e := range gr.Errors { errorMessages = append(errorMessages, e.Message) } return fmt.Sprintf("GraphQL error: %s", strings.Join(errorMessages, "\n")) } // HTTPError is an error returned by a failed API call type HTTPError struct { StatusCode int RequestURL *url.URL Message string OAuthScopes string Errors []HTTPErrorItem } type HTTPErrorItem struct { Message string Resource string Field string Code string } func (err HTTPError) Error() string { if msgs := strings.SplitN(err.Message, "\n", 2); len(msgs) > 1 { return fmt.Sprintf("HTTP %d: %s (%s)\n%s", err.StatusCode, msgs[0], err.RequestURL, msgs[1]) } else if err.Message != "" { return fmt.Sprintf("HTTP %d: %s (%s)", err.StatusCode, err.Message, err.RequestURL) } return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL) } // GraphQL performs a GraphQL request and parses the response func (c Client) GraphQL(hostname string, query string, variables map[string]interface{}, data interface{}) error { reqBody, err := json.Marshal(map[string]interface{}{"query": query, "variables": variables}) if err != nil { return err } req, err := http.NewRequest("POST", ghinstance.GraphQLEndpoint(hostname), bytes.NewBuffer(reqBody)) if err != nil { return err } req.Header.Set("Content-Type", "application/json; charset=utf-8") resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() return handleResponse(resp, data) } func graphQLClient(h *http.Client, hostname string) *graphql.Client { return graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), h) } // REST performs a REST request and parses the response. func (c Client) REST(hostname string, method string, p string, body io.Reader, data interface{}) error { req, err := http.NewRequest(method, restURL(hostname, p), body) if err != nil { return err } req.Header.Set("Content-Type", "application/json; charset=utf-8") resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() success := resp.StatusCode >= 200 && resp.StatusCode < 300 if !success { return HandleHTTPError(resp) } if resp.StatusCode == http.StatusNoContent { return nil } b, err := ioutil.ReadAll(resp.Body) if err != nil { return err } err = json.Unmarshal(b, &data) if err != nil { return err } return nil } func restURL(hostname string, pathOrURL string) string { if strings.HasPrefix(pathOrURL, "https://") || strings.HasPrefix(pathOrURL, "http://") { return pathOrURL } return ghinstance.RESTPrefix(hostname) + pathOrURL } func handleResponse(resp *http.Response, data interface{}) error { success := resp.StatusCode >= 200 && resp.StatusCode < 300 if !success { return HandleHTTPError(resp) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } gr := &graphQLResponse{Data: data} err = json.Unmarshal(body, &gr) if err != nil { return err } if len(gr.Errors) > 0 { return &GraphQLErrorResponse{Errors: gr.Errors} } return nil } func HandleHTTPError(resp *http.Response) error { httpError := HTTPError{ StatusCode: resp.StatusCode, RequestURL: resp.Request.URL, OAuthScopes: resp.Header.Get("X-Oauth-Scopes"), } if !jsonTypeRE.MatchString(resp.Header.Get("Content-Type")) { httpError.Message = resp.Status return httpError } body, err := ioutil.ReadAll(resp.Body) if err != nil { httpError.Message = err.Error() return httpError } var parsedBody struct { Message string `json:"message"` Errors []json.RawMessage } if err := json.Unmarshal(body, &parsedBody); err != nil { return httpError } var messages []string if parsedBody.Message != "" { messages = append(messages, parsedBody.Message) } for _, raw := range parsedBody.Errors { switch raw[0] { case '"': var errString string _ = json.Unmarshal(raw, &errString) messages = append(messages, errString) httpError.Errors = append(httpError.Errors, HTTPErrorItem{Message: errString}) case '{': var errInfo HTTPErrorItem _ = json.Unmarshal(raw, &errInfo) msg := errInfo.Message if errInfo.Code != "" && errInfo.Code != "custom" { msg = fmt.Sprintf("%s.%s %s", errInfo.Resource, errInfo.Field, errorCodeToMessage(errInfo.Code)) } if msg != "" { messages = append(messages, msg) } httpError.Errors = append(httpError.Errors, errInfo) } } httpError.Message = strings.Join(messages, "\n") return httpError } func errorCodeToMessage(code string) string { // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors switch code { case "missing", "missing_field": return "is missing" case "invalid", "unprocessable": return "is invalid" case "already_exists": return "already exists" default: return code } } var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`) func inspectableMIMEType(t string) bool { return strings.HasPrefix(t, "text/") || jsonTypeRE.MatchString(t) }
X Tutup