package client import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httputil" "strings" ) const ( gandiEndpoint = "https://api.gandi.net/v5/" ) // Gandi is the handle used to interact with the Gandi API type Gandi struct { apikey string endpoint string sharingID string debug bool dryRun bool } // New instantiates a new Gandi client func New(apikey string, sharingID string, debug bool, dryRun bool) *Gandi { return &Gandi{apikey: apikey, endpoint: gandiEndpoint, sharingID: sharingID, debug: debug, dryRun: dryRun} } // SetEndpoint sets the URL to the endpoint. It takes a string defining the subpath under https://api.gandi.net/v5/ func (g *Gandi) SetEndpoint(endpoint string) { g.endpoint = gandiEndpoint + endpoint } // Get issues a GET request. It takes a subpath rooted in the endpoint. Response data is written to the recipient. // Returns the response headers and any error func (g *Gandi) Get(path string, params, recipient interface{}) (http.Header, error) { return g.askGandi(http.MethodGet, path, params, recipient) } // Post issues a POST request. It takes a subpath rooted in the endpoint. Response data is written to the recipient. // Returns the response headers and any error func (g *Gandi) Post(path string, params, recipient interface{}) (http.Header, error) { return g.askGandi(http.MethodPost, path, params, recipient) } // Patch issues a PATCH request. It takes a subpath rooted in the endpoint. Response data is written to the recipient. // Returns the response headers and any error func (g *Gandi) Patch(path string, params, recipient interface{}) (http.Header, error) { return g.askGandi(http.MethodPatch, path, params, recipient) } // Delete issues a DELETE request. It takes a subpath rooted in the endpoint. Response data is written to the recipient. // Returns the response headers and any error func (g *Gandi) Delete(path string, params, recipient interface{}) (http.Header, error) { return g.askGandi(http.MethodDelete, path, params, recipient) } // Put issues a PUT request. It takes a subpath rooted in the endpoint. Response data is written to the recipient. // Returns the response headers and any error func (g *Gandi) Put(path string, params, recipient interface{}) (http.Header, error) { return g.askGandi(http.MethodPut, path, params, recipient) } func (g *Gandi) askGandi(method, path string, params, recipient interface{}) (http.Header, error) { resp, err := g.doAskGandi(method, path, params, nil) if err != nil { return nil, err } defer resp.Body.Close() decoder := json.NewDecoder(resp.Body) decoder.Decode(recipient) return resp.Header, nil } // GetBytes issues a GET request but does not attempt to parse any response into JSON. // It returns the response headers, a byteslice of the response, and any error func (g *Gandi) GetBytes(path string, params interface{}) (http.Header, []byte, error) { headers := [][2]string{ {"Accept", "text/plain"}, } resp, err := g.doAskGandi(http.MethodGet, path, params, headers) if err != nil { return nil, nil, err } defer resp.Body.Close() content, err := ioutil.ReadAll(resp.Body) return resp.Header, content, err } func (g *Gandi) doAskGandi(method, path string, p interface{}, extraHeaders [][2]string) (*http.Response, error) { var ( err error req *http.Request ) params, err := json.Marshal(p) if err != nil { return nil, err } client := &http.Client{} suffix := "" if len(g.sharingID) != 0 { suffix += "?sharing_id=" + g.sharingID } if params != nil && string(params) != "null" { req, err = http.NewRequest(method, g.endpoint+path+suffix, bytes.NewReader(params)) } else { req, err = http.NewRequest(method, g.endpoint+path+suffix, nil) } if err != nil { return nil, err } req.Header.Add("Authorization", "Apikey "+g.apikey) req.Header.Add("Content-Type", "application/json") if g.dryRun { req.Header.Add("Dry-Run", "1") } for _, header := range extraHeaders { req.Header.Add(header[0], header[1]) } if g.debug { dump, _ := httputil.DumpRequestOut(req, true) fmt.Println("=======================================\nREQUEST:") fmt.Println(string(dump)) } resp, err := client.Do(req) if err != nil { return resp, err } if g.debug { dump, _ := httputil.DumpResponse(resp, true) fmt.Println("=======================================\nREQUEST:") fmt.Println(string(dump)) } if resp.StatusCode < 200 || resp.StatusCode > 299 { defer resp.Body.Close() var message StandardResponse defer resp.Body.Close() decoder := json.NewDecoder(resp.Body) decoder.Decode(&message) if message.Message != "" { err = fmt.Errorf("%d: %s", resp.StatusCode, message.Message) } else if len(message.Errors) > 0 { var errors []string for _, oneError := range message.Errors { errors = append(errors, fmt.Sprintf("%s: %s", oneError.Name, oneError.Description)) } err = fmt.Errorf(strings.Join(errors, ", ")) } else { err = fmt.Errorf("%d", resp.StatusCode) } } return resp, err } // StandardResponse is a standard response type StandardResponse struct { Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` UUID string `json:"uuid,omitempty"` Object string `json:"object,omitempty"` Cause string `json:"cause,omitempty"` Status string `json:"status,omitempty"` Errors []StandardError `json:"errors,omitempty"` } // StandardError is embedded in a standard error type StandardError struct { Location string `json:"location"` Name string `json:"name"` Description string `json:"description"` }