//go:build ignore
package main
import (
"fmt"
"log"
"os"
"strings"
"text/template"
"time"
"github.com/pb33f/libopenapi"
validator "github.com/pb33f/libopenapi-validator"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// TemplateData represents information we'll inject into our code generation template.
type TemplateData struct {
// Resources is a map of predefined API resources.
Resources map[string]*Resource
// Timestamp represents when the code was generated.
Timestamp time.Time
// Title is the name of the API.
Title string
// Description is a description of the Fastly API.
Description string
}
// Resource is an individual API endpoint.
type Resource struct {
// Description is the description of the API resource.
Description string
// ExternalDocs is the Developer Hub API documentation page for the resource.
ExternalDocs string
// Endpoints is a list of endpoints available for the resource.
Endpoints []Endpoint
}
// Endpoint is an individual API endpoint for the resource.
type Endpoint struct {
// Path is the API endpoint.
Path string
// Params is the API path parameters.
Params []Param
// Servers is a list of API hosts.
Servers []*v3.Server
// Operations is a list of API operations (e.g. GET, POST, DELETE etc).
// Each operation is an object containing the details of the operation.
// This includes details of the request body, response body, metadata etc.
Operations map[string]*v3.Operation
}
// Param is an individual parameter (path or query)
type Param struct {
// Name is the name of the parameter.
Name string
// Description is the description of the parameter.
Description string
// In indicates whether the param is in the path or the query.
In string
// Required indicates if the param must be provided.
Required bool
// Type is the type of the parameter (e.g. string, integer etc).
// The generator needs to transform the type into a language specific format.
Type string
}
func main() {
schema, _ := os.ReadFile("fastly.yaml")
document, err := libopenapi.NewDocument(schema)
if err != nil {
panic(fmt.Sprintf("cannot create new document: %s", err))
}
highLevelValidator, validatorErrs := validator.NewValidator(document)
if len(validatorErrs) > 0 {
for _, e := range validatorErrs {
fmt.Printf("validatorErr: %+v\n", e)
}
return
}
valid, validationErrs := highLevelValidator.ValidateDocument()
if !valid {
for _, e := range validationErrs {
fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message)
fmt.Printf("Fix: %s\n\n", e.HowToFix)
}
}
porcelain, errors := document.BuildV3Model()
if len(errors) > 0 {
for i := range errors {
fmt.Printf("error: %e\n", errors[i])
}
panic(fmt.Sprintf("cannot create v3 model from document: %d errors reported", len(errors)))
}
d := TemplateData{
Resources: map[string]*Resource{},
Timestamp: time.Now(),
Title: porcelain.Model.Info.Title,
// FIXME: Rewrite Markdown syntax used in the description (e.g. hyperlinks).
Description: strings.ReplaceAll(porcelain.Model.Info.Description, "\n", " "),
}
// The following code locates the resource metadata associated with a path,
// and stores all relevant data into a Resource data type which can be looked
// up via a map key (i.e. the Data struct's Resources map field).
for path, data := range porcelain.Model.Paths.PathItems {
if path != "/service/{service_id}/version/{version_id}/backend/{backend_name}" {
continue
}
ops := data.GetOperations()
if len(ops) == 0 {
return
}
var resourceName string
for _, op := range ops {
if len(op.Tags) == 0 {
return
}
resourceName = op.Tags[0]
break
}
if resourceName == "" {
fmt.Printf("error: the resource path '%s' had no tag in any operation for us to identify the resource\n", path)
return
}
r := &Resource{}
if resource, ok := d.Resources[resourceName]; ok {
r = resource
}
if r.Description == "" && r.ExternalDocs == "" {
for _, metadata := range porcelain.Model.Tags {
if metadata.Name == resourceName {
r.Description = metadata.Description
r.ExternalDocs = metadata.ExternalDocs.URL
break
}
}
}
var params []Param
for _, param := range data.Parameters {
// TODO: Make the switch work for multiple languages (not just Go).
var t string
switch v := param.Schema.Schema().Type[0]; v {
case "integer":
t = "int"
default:
t = v
}
p := Param{
Name: param.Name,
Description: param.Description,
In: param.In,
Required: param.Required,
Type: t,
}
params = append(params, p)
}
r.Endpoints = append(r.Endpoints, Endpoint{
Path: path,
Params: params,
Servers: data.Servers,
// TODO: Generate response objects using defined schemas.
// That means we need to pass the schema for each operation.
// Which means passing a custom object, not `data.GetOperations()`.
// As we can't call a method (e.g. `data.Schema.Schema()`) within the template.
// See the below for loop (which should be deleted to avoid output).
Operations: data.GetOperations(),
})
d.Resources[resourceName] = r
for op, data := range data.GetOperations() {
fmt.Printf("op: %+v\n", op)
for code, resp := range data.Responses.Codes {
fmt.Printf("code: %+v\n", code)
for mime, data := range resp.Content {
fmt.Printf("%+v | %+v\n", mime, data.Schema.Schema())
}
}
}
}
// NOTE: We need to use a trick to include backticks in a raw string literal.
//
// e.g. `<TEXT>` would become ` + "`<TEXT>`" + `
//
// You stop the raw string literal backtick, then concatenate with normal
// string but the normal string happens to include backticks. Then you restart
// the raw string literal's backtick.
tmpl := template.Must(template.New("").Funcs(template.FuncMap{
"toCamelCase": func(s string) string {
words := strings.Split(s, "-")
if len(words) == 1 {
words = strings.Split(s, "_")
}
for i := 0; i < len(words); i++ {
words[i] = cases.Title(language.English).String(words[i])
}
return strings.Join(words, "")
},
"title": cases.Title(language.English).String,
}).Parse(`// Package fastly provides access to an API client for {{ .Title }}
//
// {{ .Description }}
package fastly
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// {{ .Timestamp }}
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/google/go-querystring/query"
"github.com/google/jsonapi"
"github.com/hashicorp/go-cleanhttp"
"github.com/mitchellh/mapstructure"
)
// API CLIENT LOGIC
// TODO: Move to a separate file for maintainability.
// APIKeyHeader is the name of the header that contains the Fastly API key.
const APIKeyHeader = "Fastly-Key"
// DefaultEndpoint is the default endpoint for Fastly. Since Fastly does not
// support an on-premise solution, this is likely to always be the default.
const DefaultEndpoint = "https://api.fastly.com"
// EndpointEnvVar is the name of an environment variable that can be used
// to change the URL of API requests.
const EndpointEnvVar = "FASTLY_API_URL"
// DebugEnvVar is the name of an environment variable that can be used to switch
// the API client into debug mode.
const DebugEnvVar = "FASTLY_DEBUG_MODE"
// ProjectVersion is the version of this library.
var ProjectVersion = "0.0.1"
// UserAgent is the user agent for this particular client.
var UserAgent = fmt.Sprintf("FastlyAPIClient/Go/%s", ProjectVersion)
// NewClient creates a new API client with the given key and the default API
// endpoint. Because Fastly allows some requests without an API key, this
// function will not error if the API token is not supplied. Attempts to make a
// request that requires an API key will return a 403 response.
func NewClient(key string) (*Client, error) {
endpoint, ok := os.LookupEnv(EndpointEnvVar)
if !ok {
endpoint = DefaultEndpoint
}
return NewClientForEndpoint(key, endpoint)
}
// NewClientForEndpoint creates a new API client with the given key and API
// endpoint. Because Fastly allows some requests without an API key, this
// function will not error if the API token is not supplied. Attempts to make a
// request that requires an API key will return a 403 response.
func NewClientForEndpoint(key string, endpoint string) (*Client, error) {
client := &Client{apiKey: key, Address: endpoint}
if endpoint, ok := os.LookupEnv(DebugEnvVar); ok && endpoint == "true" {
client.debugMode = true
}
return client.init()
}
// Client is the main entrypoint to the Fastly golang API library.
type Client struct {
// Address is the address of Fastly's API endpoint.
Address string
// HTTPClient is the HTTP client to use. If one is not provided, a default
// client will be used.
HTTPClient *http.Client
// apiKey is the Fastly API key to authenticate requests.
apiKey string
// debugMode enables HTTP request/response dumps.
debugMode bool
// remaining is last observed value of http header Fastly-RateLimit-Remaining
remaining int
// reset is last observed value of http header Fastly-RateLimit-Reset
reset int64
// updateLock forces serialization of calls that modify a service.
// Concurrent modifications have undefined semantics.
updateLock sync.Mutex
// url is the parsed URL from Address
url *url.URL
}
func (c *Client) init() (*Client, error) {
// Until we do a request, we don't know how many are left.
// Use the default limit as a first guess:
// https://developer.fastly.com/reference/api/#rate-limiting
c.remaining = 1000
u, err := url.Parse(c.Address)
if err != nil {
return nil, err
}
c.url = u
if c.HTTPClient == nil {
c.HTTPClient = cleanhttp.DefaultClient()
}
return c, nil
}
// RateLimitRemaining returns the number of non-read requests left before
// rate limiting causes a 429 Too Many Requests error.
func (c *Client) RateLimitRemaining() int {
return c.remaining
}
// RateLimitReset returns the next time the rate limiter's counter will be
// reset.
func (c *Client) RateLimitReset() time.Time {
return time.Unix(c.reset, 0)
}
// Get issues an HTTP GET request.
func (c *Client) Get(p string, ro *RequestOptions) (*http.Response, error) {
if ro == nil {
ro = new(RequestOptions)
}
ro.Parallel = true
return c.Request("GET", p, ro)
}
// Head issues an HTTP HEAD request.
func (c *Client) Head(p string, ro *RequestOptions) (*http.Response, error) {
if ro == nil {
ro = new(RequestOptions)
}
ro.Parallel = true
return c.Request("HEAD", p, ro)
}
// Patch issues an HTTP PATCH request.
func (c *Client) Patch(p string, ro *RequestOptions) (*http.Response, error) {
return c.Request("PATCH", p, ro)
}
// PatchForm issues an HTTP PUT request with the given interface form-encoded.
func (c *Client) PatchForm(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestForm("PATCH", p, i, ro)
}
// PatchJSON issues an HTTP PUT request with the given interface json-encoded.
func (c *Client) PatchJSON(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestJSON("PATCH", p, i, ro)
}
// PatchJSONAPI issues an HTTP PUT request with the given interface json-encoded.
func (c *Client) PatchJSONAPI(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestJSONAPI("PATCH", p, i, ro)
}
// Post issues an HTTP POST request.
func (c *Client) Post(p string, ro *RequestOptions) (*http.Response, error) {
return c.Request("POST", p, ro)
}
// PostForm issues an HTTP POST request with the given interface form-encoded.
func (c *Client) PostForm(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestForm("POST", p, i, ro)
}
// PostJSON issues an HTTP POST request with the given interface json-encoded.
func (c *Client) PostJSON(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestJSON("POST", p, i, ro)
}
// PostJSONAPI issues an HTTP POST request with the given interface json-encoded.
func (c *Client) PostJSONAPI(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestJSONAPI("POST", p, i, ro)
}
// PostJSONAPIBulk issues an HTTP POST request with the given interface json-encoded and bulk requests.
func (c *Client) PostJSONAPIBulk(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestJSONAPIBulk("POST", p, i, ro)
}
// Put issues an HTTP PUT request.
func (c *Client) Put(p string, ro *RequestOptions) (*http.Response, error) {
return c.Request("PUT", p, ro)
}
// PutForm issues an HTTP PUT request with the given interface form-encoded.
func (c *Client) PutForm(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestForm("PUT", p, i, ro)
}
// PutFormFile issues an HTTP PUT request (multipart/form-encoded) to put a file to an endpoint.
func (c *Client) PutFormFile(urlPath string, filePath string, fieldName string, ro *RequestOptions) (*http.Response, error) {
return c.RequestFormFile("PUT", urlPath, filePath, fieldName, ro)
}
// PutFormFileFromReader issues an HTTP PUT request (multipart/form-encoded) to put a file to an endpoint.
func (c *Client) PutFormFileFromReader(urlPath string, fileName string, fileBytes io.Reader, fieldName string, ro *RequestOptions) (*http.Response, error) {
return c.RequestFormFileFromReader("PUT", urlPath, fileName, fileBytes, fieldName, ro)
}
// PutJSON issues an HTTP PUT request with the given interface json-encoded.
func (c *Client) PutJSON(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestJSON("PUT", p, i, ro)
}
// PutJSONAPI issues an HTTP PUT request with the given interface json-encoded.
func (c *Client) PutJSONAPI(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestJSONAPI("PUT", p, i, ro)
}
// Delete issues an HTTP DELETE request.
func (c *Client) Delete(p string, ro *RequestOptions) (*http.Response, error) {
return c.Request("DELETE", p, ro)
}
// DeleteJSONAPI issues an HTTP DELETE request with the given interface json-encoded.
func (c *Client) DeleteJSONAPI(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestJSONAPI("DELETE", p, i, ro)
}
// DeleteJSONAPIBulk issues an HTTP DELETE request with the given interface json-encoded and bulk requests.
func (c *Client) DeleteJSONAPIBulk(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
return c.RequestJSONAPIBulk("DELETE", p, i, ro)
}
// RequestForm makes an HTTP request with the given interface being encoded as
// form data.
func (c *Client) RequestForm(verb, p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
if ro == nil {
ro = new(RequestOptions)
}
if ro.Headers == nil {
ro.Headers = make(map[string]string)
}
ro.Headers["Content-Type"] = "application/x-www-form-urlencoded"
v, err := query.Values(i)
if err != nil {
return nil, err
}
body := v.Encode()
if ro.HealthCheckHeaders {
body = parseHealthCheckHeaders(body)
}
ro.Body = strings.NewReader(body)
ro.BodyLength = int64(len(body))
return c.Request(verb, p, ro)
}
// RequestFormFile makes an HTTP request to upload a file to an endpoint.
func (c *Client) RequestFormFile(verb, urlPath string, filePath string, fieldName string, ro *RequestOptions) (*http.Response, error) {
file, err := os.Open(filepath.Clean(filePath))
if err != nil {
return nil, fmt.Errorf("error reading file: %v", err)
}
defer file.Close() // #nosec G307
return c.RequestFormFileFromReader(verb, urlPath, filepath.Base(filePath), file, fieldName, ro)
}
// RequestFormFileFromReader makes an HTTP request to upload a raw reader to an endpoint.
func (c *Client) RequestFormFileFromReader(verb, urlPath string, fileName string, fileBytes io.Reader, fieldName string, ro *RequestOptions) (*http.Response, error) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile(fieldName, fileName)
if err != nil {
return nil, fmt.Errorf("error creating multipart form: %v", err)
}
_, err = io.Copy(part, fileBytes)
if err != nil {
return nil, fmt.Errorf("error copying file to multipart form: %v", err)
}
err = writer.Close()
if err != nil {
return nil, fmt.Errorf("error closing multipart form: %v", err)
}
if ro == nil {
ro = new(RequestOptions)
}
if ro.Headers == nil {
ro.Headers = make(map[string]string)
}
ro.Headers["Content-Type"] = writer.FormDataContentType()
ro.Headers["Accept"] = "application/json"
ro.Body = &body
ro.BodyLength = int64(body.Len())
return c.Request(verb, urlPath, ro)
}
// RequestJSON constructs JSON HTTP request.
func (c *Client) RequestJSON(verb, p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
if ro == nil {
ro = new(RequestOptions)
}
if ro.Headers == nil {
ro.Headers = make(map[string]string)
}
ro.Headers["Content-Type"] = "application/json"
ro.Headers["Accept"] = "application/json"
body, err := json.Marshal(i)
if err != nil {
return nil, err
}
ro.Body = bytes.NewReader(body)
ro.BodyLength = int64(len(body))
return c.Request(verb, p, ro)
}
// RequestJSONAPI constructs JSON API HTTP request.
func (c *Client) RequestJSONAPI(verb, p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
if ro == nil {
ro = new(RequestOptions)
}
if ro.Headers == nil {
ro.Headers = make(map[string]string)
}
ro.Headers["Content-Type"] = jsonapi.MediaType
ro.Headers["Accept"] = jsonapi.MediaType
if i != nil {
var buf bytes.Buffer
if err := jsonapi.MarshalPayload(&buf, i); err != nil {
return nil, err
}
ro.Body = &buf
ro.BodyLength = int64(buf.Len())
}
return c.Request(verb, p, ro)
}
// RequestJSONAPIBulk constructs bulk JSON API HTTP request.
func (c *Client) RequestJSONAPIBulk(verb, p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
if ro == nil {
ro = new(RequestOptions)
}
if ro.Headers == nil {
ro.Headers = make(map[string]string)
}
ro.Headers["Content-Type"] = jsonapi.MediaType + "; ext=bulk"
ro.Headers["Accept"] = jsonapi.MediaType + "; ext=bulk"
var buf bytes.Buffer
if err := jsonapi.MarshalPayload(&buf, i); err != nil {
return nil, err
}
ro.Body = &buf
ro.BodyLength = int64(buf.Len())
return c.Request(verb, p, ro)
}
// Request makes an HTTP request against the HTTPClient using the given verb,
// Path, and request options.
func (c *Client) Request(verb, p string, ro *RequestOptions) (*http.Response, error) {
req, err := c.RawRequest(verb, p, ro)
if err != nil {
return nil, err
}
if ro == nil || !ro.Parallel {
c.updateLock.Lock()
defer c.updateLock.Unlock()
}
if c.debugMode {
dump, _ := httputil.DumpRequest(req, true)
fmt.Printf("http.Request (dump): %q\n", dump)
}
// nosemgrep: trailofbits.go.invalid-usage-of-modified-variable.invalid-usage-of-modified-variable
resp, err := checkResp(c.HTTPClient.Do(req))
if err != nil {
return resp, err
}
if c.debugMode {
dump, _ := httputil.DumpResponse(resp, true)
fmt.Printf("http.Response (dump): %q\n", dump)
}
if verb != "GET" && verb != "HEAD" {
remaining := resp.Header.Get("Fastly-RateLimit-Remaining")
if remaining != "" {
if val, err := strconv.Atoi(remaining); err == nil {
c.remaining = val
}
}
reset := resp.Header.Get("Fastly-RateLimit-Reset")
if reset != "" {
if val, err := strconv.ParseInt(reset, 10, 64); err == nil {
c.reset = val
}
}
}
return resp, nil
}
// RawRequest accepts a verb, URL, and RequestOptions struct and returns the
// constructed http.Request and any errors that occurred
func (c *Client) RawRequest(verb, p string, ro *RequestOptions) (*http.Request, error) {
// Ensure we have request options.
if ro == nil {
ro = new(RequestOptions)
}
// Append the path to the URL.
u := strings.TrimRight(c.url.String(), "/") + "/" + strings.TrimLeft(p, "/")
// Create the request object.
request, err := http.NewRequest(verb, u, ro.Body)
if err != nil {
return nil, err
}
params := make(url.Values)
for k, v := range ro.Params {
params.Add(k, v)
}
request.URL.RawQuery = params.Encode()
// Set the API key.
if len(c.apiKey) > 0 {
request.Header.Set(APIKeyHeader, c.apiKey)
}
// Set the User-Agent.
request.Header.Set("User-Agent", UserAgent)
// Add any custom headers.
for k, v := range ro.Headers {
request.Header.Add(k, v)
}
// Add Content-Length if we have it.
if ro.BodyLength > 0 {
request.ContentLength = ro.BodyLength
}
return request, nil
}
// RequestOptions is the list of options to pass to the request.
type RequestOptions struct {
// Body is an io.Reader object that will be streamed or uploaded with the
// Request.
Body io.Reader
// BodyLength is the final size of the Body.
BodyLength int64
// Headers is a map of key-value pairs that will be added to the Request.
Headers map[string]string
// HealthCheckHeaders indicates if there is any special parsing required to
// support the health check API endpoint (refer to client.RequestForm).
//
// TODO: Lookout for this when it comes to the future code-generated API
// client world, as this special case might get omitted accidentally.
HealthCheckHeaders bool
// Can this request run in parallel
Parallel bool
// Params is a map of key-value pairs that will be added to the Request.
Params map[string]string
}
// checkResp wraps an HTTP request from the default client and verifies that the
// request was successful. A non-200 request returns an error formatted to
// included any validation problems or otherwise.
func checkResp(resp *http.Response, err error) (*http.Response, error) {
// If the err is already there, there was an error higher up the chain, so
// just return that.
if err != nil {
return resp, err
}
switch resp.StatusCode {
case 200, 201, 202, 204, 205, 206:
return resp, nil
default:
return resp, NewHTTPError(resp)
}
}
// HTTPError is a custom error type that wraps an HTTP status code with some
// helper functions.
type HTTPError struct {
Errors []*ErrorObject ` + "`mapstructure:\"errors\"`" + `
// StatusCode is the HTTP status code (2xx-5xx).
StatusCode int
// RateLimitRemaining is the number of API requests remaining in the current
// rate limit window. A nil value indicates the API returned no value for
// the associated Fastly-RateLimit-Remaining response header.
RateLimitRemaining *int
// RateLimitReset is the time at which the current rate limit window resets,
// as a Unix timestamp. A nil value indicates the API returned no value for
// the associated Fastly-RateLimit-Reset response header.
RateLimitReset *int
}
// ErrorObject is a single error.
type ErrorObject struct {
Code string ` + "`mapstructure:\"code\"`" + `
Detail string ` + "`mapstructure:\"detail\"`" + `
ID string ` + "`mapstructure:\"id\"`" + `
Meta *map[string]interface{} ` + "`mapstructure:\"meta\"`" + `
Status string ` + "`mapstructure:\"status\"`" + `
Title string ` + "`mapstructure:\"title\"`" + `
}
// legacyError represents the older-style errors from Fastly. It is private
// because it is automatically converted to a jsonapi error.
type legacyError struct {
Detail string ` + "`mapstructure:\"detail\"`" + `
Message string ` + "`mapstructure:\"msg\"`" + `
Title string ` + "`mapstructure:\"title\"`" + `
}
// NewHTTPError creates a new HTTP error from the given code.
func NewHTTPError(resp *http.Response) *HTTPError {
var e HTTPError
e.StatusCode = resp.StatusCode
if v, err := strconv.Atoi(resp.Header.Get("Fastly-RateLimit-Remaining")); err == nil {
e.RateLimitRemaining = &v
}
if v, err := strconv.Atoi(resp.Header.Get("Fastly-RateLimit-Reset")); err == nil {
e.RateLimitReset = &v
}
if resp.Body == nil {
return &e
}
// Save a copy of the body as it's read/decoded.
// If decoding fails, it can then be used (via addDecodeErr)
// to create a generic error containing the body's read contents.
var bodyCp bytes.Buffer
body := io.TeeReader(resp.Body, &bodyCp)
addDecodeErr := func() {
// There are 2 errors at this point:
// 1. The response error.
// 2. The error decoding the response.
// The response error is still most relevant to users (just unable to be decoded).
// Provide the response's body verbatim as the error 'Detail' with the assumption
// that it may contain useful information, e.g. 'Bad Gateway'.
// The decode error could be conflated with the response error, so it is omitted.
e.Errors = append(e.Errors, &ErrorObject{
Title: "Undefined error",
Detail: bodyCp.String(),
})
}
switch resp.Header.Get("Content-Type") {
case jsonapi.MediaType:
// If this is a jsonapi response, decode it accordingly.
if err := decodeBodyMap(body, &e); err != nil {
addDecodeErr()
}
case "application/problem+json":
// Response is a "problem detail" as defined in RFC 7807.
var problemDetail struct {
Detail string ` + "`json:\"detail,omitempty\"`" + ` // A human-readable description of the specific error, aiming to help the user correct the problem
Status int ` + "`json:\"status\"`" + ` // HTTP status code
Title string ` + "`json:\"title,omitempty\"`" + ` // A short name for the error type, which remains constant from occurrence to occurrence
URL string ` + "`json:\"type,omitempty\"`" + ` // URL to a human-readable document describing this specific error condition
}
if err := json.NewDecoder(body).Decode(&problemDetail); err != nil {
addDecodeErr()
} else {
e.Errors = append(e.Errors, &ErrorObject{
Title: problemDetail.Title,
Detail: problemDetail.Detail,
Status: strconv.Itoa(problemDetail.Status),
})
}
default:
var lerr *legacyError
if err := decodeBodyMap(body, &lerr); err != nil {
addDecodeErr()
} else if lerr != nil {
msg := lerr.Message
if msg == "" && lerr.Title != "" {
msg = lerr.Title
}
e.Errors = append(e.Errors, &ErrorObject{
Title: msg,
Detail: lerr.Detail,
})
}
}
return &e
}
// Error implements the error interface and returns the string representing the
// error text that includes the status code and the corresponding status text.
func (e *HTTPError) Error() string {
var b bytes.Buffer
fmt.Fprintf(&b, "%d - %s:", e.StatusCode, http.StatusText(e.StatusCode))
for _, e := range e.Errors {
fmt.Fprintf(&b, "\n")
if e.ID != "" {
fmt.Fprintf(&b, "\n ID: %s", e.ID)
}
if e.Title != "" {
fmt.Fprintf(&b, "\n Title: %s", e.Title)
}
if e.Detail != "" {
fmt.Fprintf(&b, "\n Detail: %s", e.Detail)
}
if e.Code != "" {
fmt.Fprintf(&b, "\n Code: %s", e.Code)
}
if e.Meta != nil {
fmt.Fprintf(&b, "\n Meta: %v", *e.Meta)
}
}
if e.RateLimitRemaining != nil {
fmt.Fprintf(&b, "\n RateLimitRemaining: %v", *e.RateLimitRemaining)
}
if e.RateLimitReset != nil {
fmt.Fprintf(&b, "\n RateLimitReset: %v", *e.RateLimitReset)
}
return b.String()
}
// String implements the stringer interface and returns the string representing
// the string text that includes the status code and corresponding status text.
func (e *HTTPError) String() string {
return e.Error()
}
// IsNotFound returns true if the HTTP error code is a 404, false otherwise.
func (e *HTTPError) IsNotFound() bool {
return e.StatusCode == 404
}
// decodeBodyMap is used to decode an HTTP response body into a mapstructure struct.
func decodeBodyMap(body io.Reader, out interface{}) error {
var parsed interface{}
dec := json.NewDecoder(body)
if err := dec.Decode(&parsed); err != nil {
return err
}
return decodeMap(parsed, out)
}
// decodeMap decodes the ` + "`in`" + ` struct or map to a mapstructure tagged ` + "`out`" + `.
// It applies the decoder defaults used throughout go-fastly.
// Note that this uses opposite argument order from Go's copy().
func decodeMap(in interface{}, out interface{}) error {
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapToHTTPHeaderHookFunc(),
stringToTimeHookFunc(),
),
WeaklyTypedInput: true,
Result: out,
})
if err != nil {
return err
}
return decoder.Decode(in)
}
// mapToHTTPHeaderHookFunc returns a function that converts maps into an
// http.Header value.
func mapToHTTPHeaderHookFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{},
) (interface{}, error) {
if f.Kind() != reflect.Map {
return data, nil
}
if t != reflect.TypeOf(new(http.Header)) {
return data, nil
}
typed, ok := data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("cannot convert %T to http.Header", data)
}
n := map[string][]string{}
for k, v := range typed {
switch tv := v.(type) {
case string:
n[k] = []string{tv}
case []string:
n[k] = tv
case int, int8, int16, int32, int64:
n[k] = []string{fmt.Sprintf("%d", tv)}
case float32, float64:
n[k] = []string{fmt.Sprintf("%f", tv)}
default:
return nil, fmt.Errorf("cannot convert %T to http.Header", v)
}
}
return n, nil
}
}
// stringToTimeHookFunc returns a function that converts strings to a time.Time
// value.
func stringToTimeHookFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{},
) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(time.Now()) {
return data, nil
}
// Convert it by parsing
v, err := time.Parse(time.RFC3339, data.(string))
if err != nil {
// DictionaryInfo#get uses it's own special time format for now.
return time.Parse("2006-01-02 15:04:05", data.(string))
}
return v, err
}
}
// parseHealthCheckHeaders returns the serialised body with the custom health
// check headers appended.
//
// NOTE: The Google query library we use for parsing and encoding the provided
// struct values doesn't support the format headers=["Foo: Bar"] and so we
// have to manually construct this format.
func parseHealthCheckHeaders(s string) string {
headers := []string{}
result := []string{}
segs := strings.Split(s, "&")
for _, s := range segs {
if strings.HasPrefix(strings.ToLower(s), "headers=") {
v := strings.Split(s, "=")
if len(v) == 2 {
headers = append(headers, fmt.Sprintf("%q", strings.ReplaceAll(v[1], "%3A+", ":")))
}
} else {
result = append(result, s)
}
}
if len(headers) > 0 {
result = append(result, "headers=%5B"+strings.Join(headers, ",")+"%5D")
}
return strings.Join(result, "&")
}
// NewFieldError returns an error that formats as the given text.
func NewFieldError(kind string) *FieldError {
return &FieldError{
kind: kind,
}
}
// FieldError represents a custom error type for API data fields.
type FieldError struct {
kind string
message string
}
// Error fulfills the error interface.
//
// NOTE: some fields are optional but still need to present an error depending
// on the API they are associated with. For example, when updating a service
// the 'name' and 'comment' fields are both optional, but at least one of them
// needs to be provided for the API call to have any purpose (otherwise the API
// backend will just reject the call, thus being a waste of network resources).
//
// Because of this we allow modifying the error message to reflect whether the
// field was either missing or some other type of error occurred.
func (e *FieldError) Error() string {
if e.message != "" {
return fmt.Sprintf("problem with field '%s': %s", e.kind, e.message)
}
return fmt.Sprintf("missing required field '%s'", e.kind)
}
// Message prints the error message.
func (e *FieldError) Message(msg string) *FieldError {
e.message = msg
return e
}
// CODE-GENERATED LOGIC
{{ range $key, $resource := .Resources }}// RESOURCE:
//
// {{ title $key }}
//
// RESOURCE DESCRIPTION:
//
// {{ $resource.Description }}
//
// API DOCUMENTATION:
//
// {{ $resource.ExternalDocs }}
{{ range $index, $endpoint := $resource.Endpoints }}
{{ range $opName, $operation := $endpoint.Operations }}
{{ range $code, $resp := $operation.Responses.Codes }}
type {{ toCamelCase $operation.OperationId }}Resp{{ $code }}{{ $resp.Description }} struct {
{{ range $mime, $data := $resp.Content }}
// {{ $mime }} | {{ $data }}
{{ end }}
}
{{ end }}
type {{ toCamelCase $operation.OperationId }}Input struct {
{{ range $param := $endpoint.Params }}
// {{ toCamelCase $param.Name }}: {{ $param.Description }} {{ if eq $param.Required true }}(required){{ end }}
{{ toCamelCase $param.Name }} {{ $param.Type }}
{{ end }}
}
// {{ toCamelCase $operation.OperationId }}: {{ $operation.Description }}
func (c *Client) {{ toCamelCase $operation.OperationId }}(i *{{ toCamelCase $operation.OperationId }}Input) (*Backend, error) {
{{ range $param := $endpoint.Params }}
{{ if eq $param.Required true }}
if i.{{ toCamelCase $param.Name }} == {{ if eq $param.Type "string" }}""{{ else if eq $param.Type "int" }}0{{ end }} {
return nil, NewFieldError("{{ toCamelCase $param.Name }}")
}
{{ end }}
{{ end }}
path := "{{ $endpoint.Path }}"
// TODO: Figure out how to identify whether url.PathEscape(i.<Field>) is needed.
{{ range $param := $endpoint.Params }}
path = strings.Replace(path, fmt.Sprintf("{%s}", "{{ $param.Name }}"), i.{{ toCamelCase $param.Name }}, -1)
{{ end }}
// TODO: Figure out how to identify correct method to call.
// Will likely need TemplateData to provide a mapping.
// e.g. If the API is JSON-API then we'd need methods like PostJSONAPI etc.
resp, err := c.{{ title $opName }}(path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var b *Backend
if err := decodeBodyMap(resp.Body, &b); err != nil {
return nil, err
}
return b, nil
}
{{ end }}
{{ range $server := $endpoint.Servers }}
// {{ $server.URL }}
{{ end }}
{{ range $opName, $operation := $endpoint.Operations }}
// Name: {{ $opName }}
// Deprecated: {{ $operation.Deprecated }}
// Security: {{ $operation.Security }}
// Description: {{ $operation.Description }}
// OperationID: {{ toCamelCase $operation.OperationId }}
// RequestBody: {{ $operation.RequestBody }}
// Responses: {{ $operation.Responses }}
// Extensions: {{ $operation.Extensions }}
// Parameters: {{ $operation.Parameters }}
{{ end }}
{{ end }}
{{ end }}
`))
f, err := os.Create("client.go")
if err != nil {
log.Fatal(err)
}
defer f.Close()
tmpl.Execute(f, d)
}