krystal/go-katapult

View on GitHub
next/templates/client.tmpl

Summary

Maintainability
Test Coverage
const (
    clientVersion = "0.2.0" // x-release-please-version
)

var (
    ErrRequestFailed = errors.New("request was not successful")
    ErrNotFound = errors.New("resource not found")
)

// RequestEditorFn  is the function signature for the RequestEditor callback function
type RequestEditorFn func(ctx context.Context, req *http.Request) error

// Doer performs HTTP requests.
//
// The standard http.Client implements this interface.
type HttpRequestDoer interface {
    Do(req *http.Request) (*http.Response, error)
}

{{$clientTypeName := opts.OutputOptions.ClientTypeName -}}

// {{ $clientTypeName }} which conforms to the OpenAPI3 specification for this service.
type {{ $clientTypeName }} struct {
    // The endpoint of the server conforming to this interface, with scheme,
    // https://api.deepmap.com for example. This can contain a path relative
    // to the server, such as https://api.deepmap.com/dev-test, and all the
    // paths in the swagger spec will be appended to the server.
    Server string

    // Doer for performing requests, typically a *http.Client with any
    // customized settings, such as certificate chains.
    Client HttpRequestDoer

    // A list of callbacks for modifying requests which are generated before sending over
    // the network.
    RequestEditors []RequestEditorFn
}

// ClientOption allows setting custom parameters during construction
type ClientOption func(*{{ $clientTypeName }}) error

// Creates a new {{ $clientTypeName }}, with reasonable defaults
func NewClient(server string, token string, opts ...ClientOption) (*{{ $clientTypeName }}, error) {
    // create a client with sane default values
    client := {{ $clientTypeName }}{
        Server: server,
    }

    bearerTokenProvider, bearerTokenProviderErr := securityprovider.NewSecurityProviderBearerToken(token)
    if bearerTokenProviderErr != nil {
            return nil, bearerTokenProviderErr
    }

    client.RequestEditors = append(client.RequestEditors, bearerTokenProvider.Intercept)
    client.RequestEditors = append(client.RequestEditors, func(ctx context.Context, req *http.Request) error {
        req.Header.Set("User-Agent", "go-katapult/"+clientVersion)
        
        return nil
    })

    // mutate client and add all optional params
    for _, o := range opts {
        if err := o(&client); err != nil {
            return nil, err
        }
    }


    // ensure the server URL always has a trailing slash
    if !strings.HasSuffix(client.Server, "/") {
        client.Server += "/"
    }
    // create httpClient, if not already present
    if client.Client == nil {
        client.Client = &http.Client{}
    }
    return &client, nil
}


// WithHTTPClient allows overriding the default Doer, which is
// automatically created using http.Client. This is useful for tests.
func WithHTTPClient(doer HttpRequestDoer) ClientOption {
    return func(c *{{ $clientTypeName }}) error {
        c.Client = doer
        return nil
    }
}

// WithRequestEditorFn allows setting up a callback function, which will be
// called right before sending the request. This can be used to mutate the request.
func WithRequestEditorFn(fn RequestEditorFn) ClientOption {
    return func(c *{{ $clientTypeName }}) error {
        c.RequestEditors = append(c.RequestEditors, fn)
        return nil
    }
}

// The interface specification for the client above.
type ClientInterface interface {
{{range . -}}
{{$hasParams := .RequiresParamObject -}}
{{$pathParams := .PathParams -}}
{{$opid := .OperationId -}}
    // {{$opid}}{{if .HasBody}}WithBody{{end}} request{{if .HasBody}} with any body{{end}}
    {{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error)
{{range .Bodies}}
    {{if .IsSupportedByClient -}}
    {{$opid}}{{.Suffix}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error)
    {{end -}}
{{end}}{{/* range .Bodies */}}
{{end}}{{/* range . $opid := .OperationId */}}
}


{{/* Generate client methods */}}
{{range . -}}
{{$hasParams := .RequiresParamObject -}}
{{$pathParams := .PathParams -}}
{{$opid := .OperationId -}}

func (c *{{ $clientTypeName }}) {{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error) {
    req, err := New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(c.Server{{genParamNames .PathParams}}{{if $hasParams}}, params{{end}}{{if .HasBody}}, contentType, body{{end}})
    if err != nil {
        return nil, err
    }
    req = req.WithContext(ctx)
    if err := c.applyEditors(ctx, req, reqEditors); err != nil {
        return nil, err
    }
    return c.Client.Do(req)
}

{{range .Bodies}}
{{if .IsSupportedByClient -}}
func (c *{{ $clientTypeName }}) {{$opid}}{{.Suffix}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error) {
    req, err := New{{$opid}}Request{{.Suffix}}(c.Server{{genParamNames $pathParams}}{{if $hasParams}}, params{{end}}, body)
    if err != nil {
        return nil, err
    }
    req = req.WithContext(ctx)
    if err := c.applyEditors(ctx, req, reqEditors); err != nil {
        return nil, err
    }
    return c.Client.Do(req)
}
{{end -}}{{/* if .IsSupported */}}
{{end}}{{/* range .Bodies */}}
{{end}}

{{/* Generate request builders */}}
{{range .}}
{{$hasParams := .RequiresParamObject -}}
{{$pathParams := .PathParams -}}
{{$bodyRequired := .BodyRequired -}}
{{$opid := .OperationId -}}

{{range .Bodies}}
{{if .IsSupportedByClient -}}
// New{{$opid}}Request{{.Suffix}} calls the generic {{$opid}} builder with {{.ContentType}} body
func New{{$opid}}Request{{.Suffix}}(server string{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody) (*http.Request, error) {
    var bodyReader io.Reader
    {{if .IsJSON -}}
        buf, err := json.Marshal(body)
        if err != nil {
            return nil, err
        }
        bodyReader = bytes.NewReader(buf)
    {{else if eq .NameTag "Formdata" -}}
        bodyStr, err := runtime.MarshalForm(body, nil)
        if err != nil {
            return nil, err
        }
        bodyReader = strings.NewReader(bodyStr.Encode())
    {{else if eq .NameTag "Text" -}}
        bodyReader = strings.NewReader(string(body))
    {{end -}}
    return New{{$opid}}RequestWithBody(server{{genParamNames $pathParams}}{{if $hasParams}}, params{{end}}, "{{.ContentType}}", bodyReader)
}
{{end -}}
{{end}}

// New{{$opid}}Request{{if .HasBody}}WithBody{{end}} generates requests for {{$opid}}{{if .HasBody}} with any type of body{{end}}
func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}) (*http.Request, error) {
    var err error
{{range $paramIdx, $param := .PathParams}}
    var pathParam{{$paramIdx}} string
    {{if .IsPassThrough}}
    pathParam{{$paramIdx}} = {{.GoVariableName}}
    {{end}}
    {{if .IsJson}}
    var pathParamBuf{{$paramIdx}} []byte
    pathParamBuf{{$paramIdx}}, err = json.Marshal({{.GoVariableName}})
    if err != nil {
        return nil, err
    }
    pathParam{{$paramIdx}} = string(pathParamBuf{{$paramIdx}})
    {{end}}
    {{if .IsStyled}}
    pathParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationPath, {{.GoVariableName}})
    if err != nil {
        return nil, err
    }
    {{end}}
{{end}}
    serverURL, err := url.Parse(server)
    if err != nil {
        return nil, err
    }

    operationPath := fmt.Sprintf("{{genParamFmtString .Path}}"{{range $paramIdx, $param := .PathParams}}, pathParam{{$paramIdx}}{{end}})
    if operationPath[0] == '/' {
        operationPath = "." + operationPath
    }

    queryURL, err := serverURL.Parse(operationPath)
    if err != nil {
        return nil, err
    }

{{if .QueryParams}}
    if params != nil {
        queryValues := queryURL.Query()
            {{range $paramIdx, $param := .QueryParams}}
            {{if not .Required}} if params.{{.GoName}} != nil { {{end}}
            {{if .IsPassThrough}}
            queryValues.Add("{{.ParamName}}", {{if not .Required}}*{{end}}params.{{.GoName}})
            {{end}}
            {{if .IsJson}}
            if queryParamBuf, err := json.Marshal({{if not .Required}}*{{end}}params.{{.GoName}}); err != nil {
                return nil, err
            } else {
                queryValues.Add("{{.ParamName}}", string(queryParamBuf))
            }

            {{end}}
            {{if .IsStyled}}
            if queryFrag, err := runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationQuery, {{if not .Required}}*{{end}}params.{{.GoName}}); err != nil {
                return nil, err
            } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
               return nil, err
            } else {
               for k, v := range parsed {
                   for _, v2 := range v {
                       queryValues.Add(k, v2)
                   }
               }
            }
            {{end}}
            {{if not .Required}}}{{end}}
        {{end}}
        queryURL.RawQuery = queryValues.Encode()
    }
{{end}}{{/* if .QueryParams */}}
    req, err := http.NewRequest("{{.Method}}", queryURL.String(), {{if .HasBody}}body{{else}}nil{{end}})
    if err != nil {
        return nil, err
    }

    {{if .HasBody}}req.Header.Add("Content-Type", contentType){{end}}
{{ if .HeaderParams }}
    if params != nil {
    {{range $paramIdx, $param := .HeaderParams}}
        {{if not .Required}} if params.{{.GoName}} != nil { {{end}}
        var headerParam{{$paramIdx}} string
        {{if .IsPassThrough}}
        headerParam{{$paramIdx}} = {{if not .Required}}*{{end}}params.{{.GoName}}
        {{end}}
        {{if .IsJson}}
        var headerParamBuf{{$paramIdx}} []byte
        headerParamBuf{{$paramIdx}}, err = json.Marshal({{if not .Required}}*{{end}}params.{{.GoName}})
        if err != nil {
            return nil, err
        }
        headerParam{{$paramIdx}} = string(headerParamBuf{{$paramIdx}})
        {{end}}
        {{if .IsStyled}}
        headerParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationHeader, {{if not .Required}}*{{end}}params.{{.GoName}})
        if err != nil {
            return nil, err
        }
        {{end}}
        req.Header.Set("{{.ParamName}}", headerParam{{$paramIdx}})
        {{if not .Required}}}{{end}}
    {{end}}
    }
{{- end }}{{/* if .HeaderParams */}}

{{ if .CookieParams }}
    if params != nil {
    {{range $paramIdx, $param := .CookieParams}}
        {{if not .Required}} if params.{{.GoName}} != nil { {{end}}
        var cookieParam{{$paramIdx}} string
        {{if .IsPassThrough}}
        cookieParam{{$paramIdx}} = {{if not .Required}}*{{end}}params.{{.GoName}}
        {{end}}
        {{if .IsJson}}
        var cookieParamBuf{{$paramIdx}} []byte
        cookieParamBuf{{$paramIdx}}, err = json.Marshal({{if not .Required}}*{{end}}params.{{.GoName}})
        if err != nil {
            return nil, err
        }
        cookieParam{{$paramIdx}} = url.QueryEscape(string(cookieParamBuf{{$paramIdx}}))
        {{end}}
        {{if .IsStyled}}
        cookieParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("simple", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationCookie, {{if not .Required}}*{{end}}params.{{.GoName}})
        if err != nil {
            return nil, err
        }
        {{end}}
        cookie{{$paramIdx}} := &http.Cookie{
            Name:"{{.ParamName}}",
            Value:cookieParam{{$paramIdx}},
        }
        req.AddCookie(cookie{{$paramIdx}})
        {{if not .Required}}}{{end}}
    {{ end -}}
    }
{{- end }}{{/* if .CookieParams */}}
    return req, nil
}

{{end}}{{/* Range */}}

func (c *{{ $clientTypeName }}) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
    for _, r := range c.RequestEditors {
        if err := r(ctx, req); err != nil {
            return err
        }
    }
    for _, r := range additionalEditors {
        if err := r(ctx, req); err != nil {
            return err
        }
    }
    return nil
}