Tags: #go
[!NOTE] As of Go 1.17
go run
works with a version.
So you can avoid the tools.go pattern with something like this (if necessary):
//go:generate go run golang.org/x/tools/cmd/stringer@v0.25.0 -type=Scope -linecomment
Refer to https://gist.github.com/Integralist/e19c9faee86e797125e6d95fe1188912 for more details.
Files required:
.
├── gen.go
├── generator
│ └── gen.go
Running go generate
will cause a main.go
file to be created in the root directory.
//go:generate go run generator/gen.go
package gen
//go:build ignore
// +build ignore
package main
import (
"log"
"os"
"text/template"
"time"
)
// Data represents information we'll inject into our code generation template.
type Data struct {
Timestamp time.Time
}
func main() {
tmpl := template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// {{ .Timestamp }}
package main
import (
"fmt"
)
func main() {
fmt.Println("this was generated code")
}
`))
f, err := os.Create("main.go")
if err != nil {
log.Fatal(err)
}
defer f.Close()
tmpl.Execute(f, Data{
Timestamp: time.Now(),
})
}
If you would like to have your code split up over multiple files then you can do that, but it will mean your final compiled binary will include code not actually utilized.
If we extend on the above example…
.
├── gen.go
├── generator
│ ├── gen.go
│ └── openapi
│ └── openapi.go
├── go.mod
By adding a go.mod
file to the root directory:
module testing-code-gen
go 1.16
It means we can put the code generator logic into multiple files within the /generator
directory and then reference it, like so:
// +build ignore
package main
import (
...
"testing-code-gen/generator/openapi"
)
...
func main() {
...
openapi.Parse() // prints "openapi parser executed"
}
The contents of openapi.go
is as follows:
package openapi
import "fmt"
func Parse() {
fmt.Println("openapi parser executed")
}
go.mod
from the root directory.go generate
treats the environment as if it’s the root of the directory.
./generator/openapi/...
) even when from inside nested directories..
├── README.md
├── client.go
├── fastly.yaml
├── generator
│ └── generator.go
├── go.mod
├── go.sum
└── main.go
2 directories, 7 files
The following code is what’s inside the generator/generator.go
file:
//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)
}
The above template code/logic produces the below code (refer to “Code Output”).
The outputted code is incomplete as I didn’t get a chance to finish the implementation.
The code generation logic also ouputs more whitespace, which is typical of code-generation as it’s a trade-off between template maintainability and code readability.
For the sake of reviewing the output below, I’ve manually removed unnecessary white space to make it easier to read + I’ve also removed the HTTP client logic †
† The HTTP client code takes up most of the space and isn’t dynamically generated, but copied from the go-fastly client library. I did this as the library is battle-tested and works with all the Fastly API request/response types (including JSON-API) so it made sense to use that rather than attempt to rewrite it from scratch.
I also wrote the code-generator to produce a single file for the output, where as for a real project you’d have it output to different files for maintainability.
The code generator logic was also constrained to producing output for a single resource path "/service/{service_id}/version/{version_id}/backend/{backend_name}"
.
What this example output demonstrates is…
DeleteBackend
, GetBackend
, UpdateBackend
etc)DeleteBackendInput
, GetBackendInput
, UpdateBackendInput
etc)Things outstanding:
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2023-06-21 10:03:11.390429 +0100 BST m=+0.299988084
//
// Everything below this line is generated from our OpenAPI schemas.
// Package fastly provides access to an API client for Fastly API
//
// Via the Fastly API you can perform any of the operations that are possible within the management console, including creating services, domains, and backends, configuring rules or uploading your own application code, as well as account operations such as user administration and billing reports. The API is organized into collections of endpoints that allow manipulation of objects related to Fastly services and accounts. For the most accurate and up-to-date API reference content, visit our [Developer Hub](https://developer.fastly.com/reference/api/)
package fastly
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 I'VE STRIPPED OUT SO YOU CAN SEE THE ACTUAL CODE-GENERATED LOGIC...
// RESOURCE:
//
// Backend
//
// RESOURCE DESCRIPTION:
//
// A backend (also sometimes called an origin server) is a server identified by IP address or hostname, from which Fastly will fetch your content. There can be multiple backends attached to a service, but each backend is specific to one service. By default, the first backend added to a service configuration will be used for all requests (provided it meets any [conditions](/reference/api/vcl-services/condition) attached to it). If multiple backends are defined for a service, the first one that has no attached conditions, or whose condition is satisfied for the current request, will be used, unless that behavior is modified using the `auto_loadbalance` field described below.
//
// API DOCUMENTATION:
//
// https://developer.fastly.com/reference/api/services/backend
type DeleteBackendResp200OK struct {
// INCOMPLETE: NOT FINISHED IMPLEMENTING RESPONSE GENERTION.
// application/json | {0x140063f59b0 <nil> map[body:0x1400217d5e0] map[] map[] 0x1400197da40}
}
type DeleteBackendInput struct {
// ServiceId: Alphanumeric string identifying the service. (required)
ServiceId string
// VersionId: Integer identifying a service version. (required)
VersionId int
// BackendName: The name of the backend. (required)
BackendName string
}
// DeleteBackend: Delete the backend for a particular service and version.
func (c *Client) DeleteBackend(i *DeleteBackendInput) (*Backend, error) {
if i.ServiceId == "" {
return nil, NewFieldError("ServiceId")
}
if i.VersionId == 0 {
return nil, NewFieldError("VersionId")
}
if i.BackendName == "" {
return nil, NewFieldError("BackendName")
}
// TODO: Figure out how to identify whether url.PathEscape(i.<Field>) is needed.
path := "/service/{service_id}/version/{version_id}/backend/{backend_name}"
path = strings.Replace(path, fmt.Sprintf("{%s}", "service_id"), i.ServiceId, -1)
path = strings.Replace(path, fmt.Sprintf("{%s}", "version_id"), i.VersionId, -1)
path = strings.Replace(path, fmt.Sprintf("{%s}", "backend_name"), i.BackendName, -1)
// 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.Delete(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
}
type GetBackendResp200OK struct {
// INCOMPLETE: NOT FINISHED IMPLEMENTING RESPONSE GENERTION.
// application/json | {0x140063f5350 <nil> map[body:0x1400217d4f0] map[] map[] 0x1400197d880}
}
type GetBackendInput struct {
// ServiceId: Alphanumeric string identifying the service. (required)
ServiceId string
// VersionId: Integer identifying a service version. (required)
VersionId int
// BackendName: The name of the backend. (required)
BackendName string
}
// GetBackend: Get the backend for a particular service and version.
func (c *Client) GetBackend(i *GetBackendInput) (*Backend, error) {
if i.ServiceId == "" {
return nil, NewFieldError("ServiceId")
}
if i.VersionId == 0 {
return nil, NewFieldError("VersionId")
}
if i.BackendName == "" {
return nil, NewFieldError("BackendName")
}
// TODO: Figure out how to identify whether url.PathEscape(i.<Field>) is needed.
path := "/service/{service_id}/version/{version_id}/backend/{backend_name}"
path = strings.Replace(path, fmt.Sprintf("{%s}", "service_id"), i.ServiceId, -1)
path = strings.Replace(path, fmt.Sprintf("{%s}", "version_id"), i.VersionId, -1)
path = strings.Replace(path, fmt.Sprintf("{%s}", "backend_name"), i.BackendName, -1)
// 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.Get(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
}
type UpdateBackendResp200OK struct {
// INCOMPLETE: NOT FINISHED IMPLEMENTING RESPONSE GENERTION.
// application/json | {0x140063f5740 <nil> map[body:0x1400217d590] map[] map[] 0x140005e3a40}
}
type UpdateBackendInput struct {
// ServiceId: Alphanumeric string identifying the service. (required)
ServiceId string
// VersionId: Integer identifying a service version. (required)
VersionId int
// BackendName: The name of the backend. (required)
BackendName string
}
// UpdateBackend: Update the backend for a particular service and version.
func (c *Client) UpdateBackend(i *UpdateBackendInput) (*Backend, error) {
if i.ServiceId == "" {
return nil, NewFieldError("ServiceId")
}
if i.VersionId == 0 {
return nil, NewFieldError("VersionId")
}
if i.BackendName == "" {
return nil, NewFieldError("BackendName")
}
// TODO: Figure out how to identify whether url.PathEscape(i.<Field>) is needed.
path := "/service/{service_id}/version/{version_id}/backend/{backend_name}"
path = strings.Replace(path, fmt.Sprintf("{%s}", "service_id"), i.ServiceId, -1)
path = strings.Replace(path, fmt.Sprintf("{%s}", "version_id"), i.VersionId, -1)
path = strings.Replace(path, fmt.Sprintf("{%s}", "backend_name"), i.BackendName, -1)
// 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.Put(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
}