Repository: getkin/kin-openapi
Pull request #956 (2024-06-02 12:35:51)

openapi3: move ref codegen to Go
Keeps things in the go ecosystem and it will make a change I'll like to do far easier with go available.

No changes to the generated refs.go other than a warning that it's generated to devs and IDEs.

I realise there are other bash files which generated code, I could do the same to them which means all generated code can be generated with a single `go generate ./...`


Patch:
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
index ef878a0..7c95093 100644
--- a/.github/docs/openapi3.txt
+++ b/.github/docs/openapi3.txt
@@ -4,6 +4,8 @@ Package openapi3 parses and writes OpenAPI 3 specification documents.
 
 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md
 
+Code generated by go generate; DO NOT EDIT.
+
 CONSTANTS
 
 const (
diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt
index 0fd7027..43540c4 100644
--- a/.github/docs/openapi3filter.txt
+++ b/.github/docs/openapi3filter.txt
@@ -182,7 +182,7 @@ type ErrCode int
     occur during validation. These may be used to write an appropriate response
     in ErrFunc.
 
-type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error)
+type ErrFunc func(ctx context.Context, w http.ResponseWriter, status int, code ErrCode, err error)
     ErrFunc handles errors that may occur during validation.
 
 type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter)
@@ -198,7 +198,7 @@ type Headerer interface {
     Headerer, the provided headers will be applied to the response writer,
     after the Content-Type is set.
 
-type LogFunc func(message string, err error)
+type LogFunc func(ctx context.Context, message string, err error)
     LogFunc handles log messages that may occur during validation.
 
 type Options struct {
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index acb3a0f..fa32c1b 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -52,7 +52,7 @@ jobs:
 
     - uses: actions/checkout@v2
 
-    - run: ./refs.sh | tee openapi3/refs.go
+    - run: go generate openapi3/refsgenerator.go
     - run: git --no-pager diff --exit-code
 
     - run: ./maps.sh
diff --git a/openapi3/ref.go b/openapi3/ref.go
index a937de4..0706073 100644
--- a/openapi3/ref.go
+++ b/openapi3/ref.go
@@ -1,5 +1,7 @@
 package openapi3
 
+//go:generate go run refsgenerator.go
+
 // Ref is specified by OpenAPI/Swagger 3.0 standard.
 // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object
 type Ref struct {
diff --git a/openapi3/refs.go b/openapi3/refs.go
index 99237f1..087e5ab 100644
--- a/openapi3/refs.go
+++ b/openapi3/refs.go
@@ -1,3 +1,4 @@
+// Code generated by go generate; DO NOT EDIT.
 package openapi3
 
 import (
diff --git a/refs.sh b/openapi3/refs.tmpl
old mode 100755
new mode 100644
similarity index 56%
rename from refs.sh
rename to openapi3/refs.tmpl
index a28ef3f..638d646
--- a/refs.sh
+++ b/openapi3/refs.tmpl
@@ -1,19 +1,5 @@
-#!/bin/bash -eux
-set -o pipefail
-
-types=()
-types+=("Callback")
-types+=("Example")
-types+=("Header")
-types+=("Link")
-types+=("Parameter")
-types+=("RequestBody")
-types+=("Response")
-types+=("Schema")
-types+=("SecurityScheme")
-
-cat <<EOF
-package openapi3
+// Code generated by go generate; DO NOT EDIT.
+package {{ .Package }}
 
 import (
 	"context"
@@ -24,44 +10,38 @@ import (
 	"github.com/go-openapi/jsonpointer"
 	"github.com/perimeterx/marshmallow"
 )
-EOF
-
-for type in "${types[@]}"; do
-	cat <<EOF
-
-// ${type}Ref represents either a ${type} or a \$ref to a ${type}.
+{{ range $type := .Types }}
+// {{ $type }}Ref represents either a {{ $type }} or a $ref to a {{ $type }}.
 // When serializing and both fields are set, Ref is preferred over Value.
-type ${type}Ref struct {
+type {{ $type }}Ref struct {
 	Ref   string
-	Value *${type}
+	Value *{{ $type }}
 	extra []string
 }
 
-var _ jsonpointer.JSONPointable = (*${type}Ref)(nil)
+var _ jsonpointer.JSONPointable = (*{{ $type }}Ref)(nil)
 
-func (x *${type}Ref) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
+func (x *{{ $type }}Ref) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
-// MarshalYAML returns the YAML encoding of ${type}Ref.
-func (x ${type}Ref) MarshalYAML() (interface{}, error) {
+// MarshalYAML returns the YAML encoding of {{ $type }}Ref.
+func (x {{ $type }}Ref) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
 		return &Ref{Ref: ref}, nil
 	}
 	return x.Value.MarshalYAML()
 }
 
-// MarshalJSON returns the JSON encoding of ${type}Ref.
-func (x ${type}Ref) MarshalJSON() ([]byte, error) {
+// MarshalJSON returns the JSON encoding of {{ $type }}Ref.
+func (x {{ $type }}Ref) MarshalJSON() ([]byte, error) {
 	y, err := x.MarshalYAML()
 	if err != nil {
 		return nil, err
 	}
 	return json.Marshal(y)
-EOF
-	cat <<EOF
 }
 
-// UnmarshalJSON sets ${type}Ref to a copy of data.
-func (x *${type}Ref) UnmarshalJSON(data []byte) error {
+// UnmarshalJSON sets {{ $type }}Ref to a copy of data.
+func (x *{{ $type }}Ref) UnmarshalJSON(data []byte) error {
 	var refOnly Ref
 	if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
 		x.Ref = refOnly.Ref
@@ -77,8 +57,8 @@ func (x *${type}Ref) UnmarshalJSON(data []byte) error {
 	return json.Unmarshal(data, &x.Value)
 }
 
-// Validate returns an error if ${type}Ref does not comply with the OpenAPI spec.
-func (x *${type}Ref) Validate(ctx context.Context, opts ...ValidationOption) error {
+// Validate returns an error if {{ $type }}Ref does not comply with the OpenAPI spec.
+func (x *{{ $type }}Ref) Validate(ctx context.Context, opts ...ValidationOption) error {
 	ctx = WithValidationOptions(ctx, opts...)
 	if extra := x.extra; len(extra) != 0 {
 		extras := make([]string, 0, len(extra))
@@ -102,13 +82,11 @@ func (x *${type}Ref) Validate(ctx context.Context, opts ...ValidationOption) err
 }
 
 // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
-func (x *${type}Ref) JSONLookup(token string) (interface{}, error) {
-	if token == "\$ref" {
+func (x *{{ $type }}Ref) JSONLookup(token string) (interface{}, error) {
+	if token == "$ref" {
 		return x.Ref, nil
 	}
 	ptr, _, err := jsonpointer.GetForToken(x.Value, token)
 	return ptr, err
 }
-EOF
-
-done
+{{ end -}}
diff --git a/openapi3/refsgenerator.go b/openapi3/refsgenerator.go
new file mode 100644
index 0000000..5bddfe2
--- /dev/null
+++ b/openapi3/refsgenerator.go
@@ -0,0 +1,49 @@
+//go:build ignore
+// +build ignore
+
+// The program generates refs.go, invoke `go generate ./...` to run.
+package main
+
+import (
+	_ "embed"
+	"os"
+	"text/template"
+)
+
+//go:embed refs.tmpl
+var tmplData string
+
+func main() {
+	file, err := os.Create("refs.go")
+	if err != nil {
+		panic(err)
+	}
+
+	defer func() {
+		if err := file.Close(); err != nil {
+			panic(err)
+		}
+	}()
+
+	packageTemplate := template.Must(template.New("openapi3-refs").Parse(tmplData))
+
+	if err := packageTemplate.Execute(file, struct {
+		Package string
+		Types   []string
+	}{
+		Package: os.Getenv("GOPACKAGE"), // set by the go:generate directive
+		Types: []string{
+			"Callback",
+			"Example",
+			"Header",
+			"Link",
+			"Parameter",
+			"RequestBody",
+			"Response",
+			"Schema",
+			"SecurityScheme",
+		},
+	}); err != nil {
+		panic(err)
+	}
+}
diff --git a/openapi3filter/middleware.go b/openapi3filter/middleware.go
index 0009d61..d20889e 100644
--- a/openapi3filter/middleware.go
+++ b/openapi3filter/middleware.go
@@ -2,6 +2,7 @@ package openapi3filter
 
 import (
 	"bytes"
+	"context"
 	"io"
 	"log"
 	"net/http"
@@ -19,10 +20,10 @@ type Validator struct {
 }
 
 // ErrFunc handles errors that may occur during validation.
-type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error)
+type ErrFunc func(ctx context.Context, w http.ResponseWriter, status int, code ErrCode, err error)
 
 // LogFunc handles log messages that may occur during validation.
-type LogFunc func(message string, err error)
+type LogFunc func(ctx context.Context, message string, err error)
 
 // ErrCode is used for classification of different types of errors that may
 // occur during validation. These may be used to write an appropriate response
@@ -61,10 +62,10 @@ func (e ErrCode) responseText() string {
 func NewValidator(router routers.Router, options ...ValidatorOption) *Validator {
 	v := &Validator{
 		router: router,
-		errFunc: func(w http.ResponseWriter, status int, code ErrCode, _ error) {
+		errFunc: func(_ context.Context, w http.ResponseWriter, status int, code ErrCode, _ error) {
 			http.Error(w, code.responseText(), status)
 		},
-		logFunc: func(message string, err error) {
+		logFunc: func(_ context.Context, message string, err error) {
 			log.Printf("%s: %v", message, err)
 		},
 	}
@@ -117,10 +118,11 @@ func ValidationOptions(options Options) ValidatorOption {
 // request and response validation.
 func (v *Validator) Middleware(h http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		ctx := r.Context()
 		route, pathParams, err := v.router.FindRoute(r)
 		if err != nil {
-			v.logFunc("validation error: failed to find route for "+r.URL.String(), err)
-			v.errFunc(w, http.StatusNotFound, ErrCodeCannotFindRoute, err)
+			v.logFunc(ctx, "validation error: failed to find route for "+r.URL.String(), err)
+			v.errFunc(ctx, w, http.StatusNotFound, ErrCodeCannotFindRoute, err)
 			return
 		}
 		requestValidationInput := &RequestValidationInput{
@@ -129,9 +131,9 @@ func (v *Validator) Middleware(h http.Handler) http.Handler {
 			Route:      route,
 			Options:    &v.options,
 		}
-		if err = ValidateRequest(r.Context(), requestValidationInput); err != nil {
-			v.logFunc("invalid request", err)
-			v.errFunc(w, http.StatusBadRequest, ErrCodeRequestInvalid, err)
+		if err = ValidateRequest(ctx, requestValidationInput); err != nil {
+			v.logFunc(ctx, "invalid request", err)
+			v.errFunc(ctx, w, http.StatusBadRequest, ErrCodeRequestInvalid, err)
 			return
 		}
 
@@ -144,22 +146,22 @@ func (v *Validator) Middleware(h http.Handler) http.Handler {
 
 		h.ServeHTTP(wr, r)
 
-		if err = ValidateResponse(r.Context(), &ResponseValidationInput{
+		if err = ValidateResponse(ctx, &ResponseValidationInput{
 			RequestValidationInput: requestValidationInput,
 			Status:                 wr.statusCode(),
 			Header:                 wr.Header(),
 			Body:                   io.NopCloser(bytes.NewBuffer(wr.bodyContents())),
 			Options:                &v.options,
 		}); err != nil {
-			v.logFunc("invalid response", err)
+			v.logFunc(ctx, "invalid response", err)
 			if v.strict {
-				v.errFunc(w, http.StatusInternalServerError, ErrCodeResponseInvalid, err)
+				v.errFunc(ctx, w, http.StatusInternalServerError, ErrCodeResponseInvalid, err)
 			}
 			return
 		}
 
 		if err = wr.flushBodyContents(); err != nil {
-			v.logFunc("failed to write response", err)
+			v.logFunc(ctx, "failed to write response", err)
 		}
 	})
 }


Test patch:
diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go
index 1260ac5..137228c 100644
--- a/openapi3filter/middleware_test.go
+++ b/openapi3filter/middleware_test.go
@@ -2,6 +2,7 @@ package openapi3filter_test
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -489,7 +490,7 @@ paths:
 	// testing a service against its spec in development and CI. In production,
 	// availability may be more important than strictness.
 	v := openapi3filter.NewValidator(router, openapi3filter.Strict(true),
-		openapi3filter.OnErr(func(w http.ResponseWriter, status int, code openapi3filter.ErrCode, err error) {
+		openapi3filter.OnErr(func(_ context.Context, w http.ResponseWriter, status int, code openapi3filter.ErrCode, err error) {
 			// Customize validation error responses to use JSON
 			w.Header().Set("Content-Type", "application/json")
 			w.WriteHeader(status)