Repository: getkin/kin-openapi
Pull request #468 (2021-12-20 13:31:15)

document union behaviour of XyzRef.s


Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index fc619f0..4a5b87c 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -64,7 +64,6 @@ jobs:
     - if: runner.os == 'Linux'
       run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]]
 
-    - run: go test -count=10 -v -run TestIssue356 ./routers
     - run: go test ./...
     - run: go test -v -run TestRaceyPatternSchema -race ./...
       env:
diff --git a/openapi3/refs.go b/openapi3/refs.go
index 4b64035..3122509 100644
--- a/openapi3/refs.go
+++ b/openapi3/refs.go
@@ -12,6 +12,8 @@ type Ref struct {
 	Ref string `json:"$ref" yaml:"$ref"`
 }
 
+// CallbackRef represents either a Callback or a $ref to a Callback.
+// When serializing and both fields are set, Ref is preferred over Value.
 type CallbackRef struct {
 	Ref   string
 	Value *Callback
@@ -43,6 +45,8 @@ func (value CallbackRef) JSONLookup(token string) (interface{}, error) {
 	return ptr, err
 }
 
+// ExampleRef represents either a Example or a $ref to a Example.
+// When serializing and both fields are set, Ref is preferred over Value.
 type ExampleRef struct {
 	Ref   string
 	Value *Example
@@ -74,6 +78,8 @@ func (value ExampleRef) JSONLookup(token string) (interface{}, error) {
 	return ptr, err
 }
 
+// HeaderRef represents either a Header or a $ref to a Header.
+// When serializing and both fields are set, Ref is preferred over Value.
 type HeaderRef struct {
 	Ref   string
 	Value *Header
@@ -105,6 +111,8 @@ func (value HeaderRef) JSONLookup(token string) (interface{}, error) {
 	return ptr, err
 }
 
+// LinkRef represents either a Link or a $ref to a Link.
+// When serializing and both fields are set, Ref is preferred over Value.
 type LinkRef struct {
 	Ref   string
 	Value *Link
@@ -125,6 +133,8 @@ func (value *LinkRef) Validate(ctx context.Context) error {
 	return foundUnresolvedRef(value.Ref)
 }
 
+// ParameterRef represents either a Parameter or a $ref to a Parameter.
+// When serializing and both fields are set, Ref is preferred over Value.
 type ParameterRef struct {
 	Ref   string
 	Value *Parameter
@@ -156,6 +166,8 @@ func (value ParameterRef) JSONLookup(token string) (interface{}, error) {
 	return ptr, err
 }
 
+// ResponseRef represents either a Response or a $ref to a Response.
+// When serializing and both fields are set, Ref is preferred over Value.
 type ResponseRef struct {
 	Ref   string
 	Value *Response
@@ -187,6 +199,8 @@ func (value ResponseRef) JSONLookup(token string) (interface{}, error) {
 	return ptr, err
 }
 
+// RequestBodyRef represents either a RequestBody or a $ref to a RequestBody.
+// When serializing and both fields are set, Ref is preferred over Value.
 type RequestBodyRef struct {
 	Ref   string
 	Value *RequestBody
@@ -218,6 +232,8 @@ func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) {
 	return ptr, err
 }
 
+// SchemaRef represents either a Schema or a $ref to a Schema.
+// When serializing and both fields are set, Ref is preferred over Value.
 type SchemaRef struct {
 	Ref   string
 	Value *Schema
@@ -256,6 +272,8 @@ func (value SchemaRef) JSONLookup(token string) (interface{}, error) {
 	return ptr, err
 }
 
+// SecuritySchemeRef represents either a SecurityScheme or a $ref to a SecurityScheme.
+// When serializing and both fields are set, Ref is preferred over Value.
 type SecuritySchemeRef struct {
 	Ref   string
 	Value *SecurityScheme
diff --git a/openapi3filter/middleware.go b/openapi3filter/middleware.go
new file mode 100644
index 0000000..3709faf
--- /dev/null
+++ b/openapi3filter/middleware.go
@@ -0,0 +1,273 @@
+package openapi3filter
+
+import (
+	"bytes"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+
+	"github.com/getkin/kin-openapi/routers"
+)
+
+// Validator provides HTTP request and response validation middleware.
+type Validator struct {
+	router  routers.Router
+	errFunc ErrFunc
+	logFunc LogFunc
+	strict  bool
+}
+
+// ErrFunc handles errors that may occur during validation.
+type ErrFunc func(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)
+
+// ErrCode is used for classification of different types of errors that may
+// occur during validation. These may be used to write an appropriate response
+// in ErrFunc.
+type ErrCode int
+
+const (
+	// ErrCodeOK indicates no error. It is also the default value.
+	ErrCodeOK = 0
+	// ErrCodeCannotFindRoute happens when the validator fails to resolve the
+	// request to a defined OpenAPI route.
+	ErrCodeCannotFindRoute = iota
+	// ErrCodeRequestInvalid happens when the inbound request does not conform
+	// to the OpenAPI 3 specification.
+	ErrCodeRequestInvalid = iota
+	// ErrCodeResponseInvalid happens when the wrapped handler response does
+	// not conform to the OpenAPI 3 specification.
+	ErrCodeResponseInvalid = iota
+)
+
+func (e ErrCode) responseText() string {
+	switch e {
+	case ErrCodeOK:
+		return "OK"
+	case ErrCodeCannotFindRoute:
+		return "not found"
+	case ErrCodeRequestInvalid:
+		return "bad request"
+	default:
+		return "server error"
+	}
+}
+
+// NewValidator returns a new response validation middlware, using the given
+// routes from an OpenAPI 3 specification.
+func NewValidator(router routers.Router, options ...ValidatorOption) *Validator {
+	v := &Validator{
+		router: router,
+		errFunc: func(w http.ResponseWriter, status int, code ErrCode, _ error) {
+			http.Error(w, code.responseText(), status)
+		},
+		logFunc: func(message string, err error) {
+			log.Printf("%s: %v", message, err)
+		},
+	}
+	for i := range options {
+		options[i](v)
+	}
+	return v
+}
+
+// ValidatorOption defines an option that may be specified when creating a
+// Validator.
+type ValidatorOption func(*Validator)
+
+// OnErr provides a callback that handles writing an HTTP response on a
+// validation error. This allows customization of error responses without
+// prescribing a particular form. This callback is only called on response
+// validator errors in Strict mode.
+func OnErr(f ErrFunc) ValidatorOption {
+	return func(v *Validator) {
+		v.errFunc = f
+	}
+}
+
+// OnLog provides a callback that handles logging in the Validator. This allows
+// the validator to integrate with a services' existing logging system without
+// prescribing a particular one.
+func OnLog(f LogFunc) ValidatorOption {
+	return func(v *Validator) {
+		v.logFunc = f
+	}
+}
+
+// Strict, if set, causes an internal server error to be sent if the wrapped
+// handler response fails response validation. If not set, the response is sent
+// and the error is only logged.
+func Strict(strict bool) ValidatorOption {
+	return func(v *Validator) {
+		v.strict = strict
+	}
+}
+
+// Middleware returns an http.Handler which wraps the given handler with
+// request and response validation.
+func (v *Validator) Middleware(h http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		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)
+			return
+		}
+		requestValidationInput := &RequestValidationInput{
+			Request:    r,
+			PathParams: pathParams,
+			Route:      route,
+		}
+		if err = ValidateRequest(r.Context(), requestValidationInput); err != nil {
+			v.logFunc("invalid request", err)
+			v.errFunc(w, http.StatusBadRequest, ErrCodeRequestInvalid, err)
+			return
+		}
+
+		var wr responseWrapper
+		if v.strict {
+			wr = &strictResponseWrapper{w: w}
+		} else {
+			wr = newWarnResponseWrapper(w)
+		}
+
+		h.ServeHTTP(wr, r)
+
+		if err = ValidateResponse(r.Context(), &ResponseValidationInput{
+			RequestValidationInput: requestValidationInput,
+			Status:                 wr.statusCode(),
+			Header:                 wr.Header(),
+			Body:                   ioutil.NopCloser(bytes.NewBuffer(wr.bodyContents())),
+		}); err != nil {
+			v.logFunc("invalid response", err)
+			if v.strict {
+				v.errFunc(w, http.StatusInternalServerError, ErrCodeResponseInvalid, err)
+			}
+			return
+		}
+
+		if err = wr.flushBodyContents(); err != nil {
+			v.logFunc("failed to write response", err)
+		}
+	})
+}
+
+type responseWrapper interface {
+	http.ResponseWriter
+
+	// flushBodyContents writes the buffered response to the client, if it has
+	// not yet been written.
+	flushBodyContents() error
+
+	// statusCode returns the response status code, 0 if not set yet.
+	statusCode() int
+
+	// bodyContents returns the buffered
+	bodyContents() []byte
+}
+
+type warnResponseWrapper struct {
+	w             http.ResponseWriter
+	headerWritten bool
+	status        int
+	body          bytes.Buffer
+	tee           io.Writer
+}
+
+func newWarnResponseWrapper(w http.ResponseWriter) *warnResponseWrapper {
+	wr := &warnResponseWrapper{
+		w: w,
+	}
+	wr.tee = io.MultiWriter(w, &wr.body)
+	return wr
+}
+
+// Write implements http.ResponseWriter.
+func (wr *warnResponseWrapper) Write(b []byte) (int, error) {
+	if !wr.headerWritten {
+		wr.WriteHeader(http.StatusOK)
+	}
+	return wr.tee.Write(b)
+}
+
+// WriteHeader implements http.ResponseWriter.
+func (wr *warnResponseWrapper) WriteHeader(status int) {
+	if !wr.headerWritten {
+		// If the header hasn't been written, record the status for response
+		// validation.
+		wr.status = status
+		wr.headerWritten = true
+	}
+	wr.w.WriteHeader(wr.status)
+}
+
+// Header implements http.ResponseWriter.
+func (wr *warnResponseWrapper) Header() http.Header {
+	return wr.w.Header()
+}
+
+// Flush implements the optional http.Flusher interface.
+func (wr *warnResponseWrapper) Flush() {
+	// If the wrapped http.ResponseWriter implements optional http.Flusher,
+	// pass through.
+	if fl, ok := wr.w.(http.Flusher); ok {
+		fl.Flush()
+	}
+}
+
+func (wr *warnResponseWrapper) flushBodyContents() error {
+	return nil
+}
+
+func (wr *warnResponseWrapper) statusCode() int {
+	return wr.status
+}
+
+func (wr *warnResponseWrapper) bodyContents() []byte {
+	return wr.body.Bytes()
+}
+
+type strictResponseWrapper struct {
+	w             http.ResponseWriter
+	headerWritten bool
+	status        int
+	body          bytes.Buffer
+}
+
+// Write implements http.ResponseWriter.
+func (wr *strictResponseWrapper) Write(b []byte) (int, error) {
+	if !wr.headerWritten {
+		wr.WriteHeader(http.StatusOK)
+	}
+	return wr.body.Write(b)
+}
+
+// WriteHeader implements http.ResponseWriter.
+func (wr *strictResponseWrapper) WriteHeader(status int) {
+	if !wr.headerWritten {
+		wr.status = status
+		wr.headerWritten = true
+	}
+}
+
+// Header implements http.ResponseWriter.
+func (wr *strictResponseWrapper) Header() http.Header {
+	return wr.w.Header()
+}
+
+func (wr *strictResponseWrapper) flushBodyContents() error {
+	wr.w.WriteHeader(wr.status)
+	_, err := wr.w.Write(wr.body.Bytes())
+	return err
+}
+
+func (wr *strictResponseWrapper) statusCode() int {
+	return wr.status
+}
+
+func (wr *strictResponseWrapper) bodyContents() []byte {
+	return wr.body.Bytes()
+}


Test patch:
diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go
new file mode 100644
index 0000000..4d88aaf
--- /dev/null
+++ b/openapi3filter/middleware_test.go
@@ -0,0 +1,513 @@
+package openapi3filter_test
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"path"
+	"regexp"
+	"strconv"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/openapi3filter"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+const validatorSpec = `
+openapi: 3.0.0
+info:
+  title: 'Validator'
+  version: '0.0.0'
+paths:
+  /test:
+    post:
+      operationId: newTest
+      description: create a new test
+      parameters:
+        - in: query
+          name: version
+          schema:
+            type: string
+          required: true
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema: { $ref: '#/components/schemas/TestContents' }
+      responses:
+        '201':
+          description: 'created test'
+          content:
+            application/json:
+              schema: { $ref: '#/components/schemas/TestResource' }
+        '400': { $ref: '#/components/responses/ErrorResponse' }
+        '500': { $ref: '#/components/responses/ErrorResponse' }
+  /test/{id}:
+    get:
+      operationId: getTest
+      description: get a test
+      parameters:
+        - in: path
+          name: id
+          schema:
+            type: string
+          required: true
+        - in: query
+          name: version
+          schema:
+            type: string
+          required: true
+      responses:
+        '200':
+          description: 'respond with test resource'
+          content:
+            application/json:
+              schema: { $ref: '#/components/schemas/TestResource' }
+        '400': { $ref: '#/components/responses/ErrorResponse' }
+        '404': { $ref: '#/components/responses/ErrorResponse' }
+        '500': { $ref: '#/components/responses/ErrorResponse' }
+components:
+  schemas:
+    TestContents:
+      type: object
+      properties:
+        name:
+          type: string
+        expected:
+          type: number
+        actual:
+          type: number
+      required: [name, expected, actual]
+      additionalProperties: false
+    TestResource:
+      type: object
+      properties:
+        id:
+          type: string
+        contents:
+          { $ref: '#/components/schemas/TestContents' }
+      required: [id, contents]
+      additionalProperties: false
+    Error:
+      type: object
+      properties:
+        code:
+          type: string
+        message:
+          type: string
+      required: [code, message]
+      additionalProperties: false
+  responses:
+    ErrorResponse:
+      description: 'an error occurred'
+      content:
+        application/json:
+          schema: { $ref: '#/components/schemas/Error' }
+`
+
+type validatorTestHandler struct {
+	contentType       string
+	getBody, postBody string
+	errBody           string
+	errStatusCode     int
+}
+
+const validatorOkResponse = `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}}`
+
+func (h validatorTestHandler) withDefaults() validatorTestHandler {
+	if h.contentType == "" {
+		h.contentType = "application/json"
+	}
+	if h.getBody == "" {
+		h.getBody = validatorOkResponse
+	}
+	if h.postBody == "" {
+		h.postBody = validatorOkResponse
+	}
+	if h.errBody == "" {
+		h.errBody = `{"code":"bad","message":"bad things"}`
+	}
+	return h
+}
+
+var testUrlRE = regexp.MustCompile(`^/test(/\d+)?$`)
+
+func (h *validatorTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", h.contentType)
+	if h.errStatusCode != 0 {
+		w.WriteHeader(h.errStatusCode)
+		w.Write([]byte(h.errBody))
+		return
+	}
+	if !testUrlRE.MatchString(r.URL.Path) {
+		w.WriteHeader(http.StatusNotFound)
+		w.Write([]byte(h.errBody))
+		return
+	}
+	switch r.Method {
+	case "GET":
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(h.getBody))
+	case "POST":
+		w.WriteHeader(http.StatusCreated)
+		w.Write([]byte(h.postBody))
+	default:
+		http.Error(w, h.errBody, http.StatusMethodNotAllowed)
+	}
+}
+
+func TestValidator(t *testing.T) {
+	doc, err := openapi3.NewLoader().LoadFromData([]byte(validatorSpec))
+	require.NoError(t, err, "failed to load test fixture spec")
+
+	ctx := context.Background()
+	err = doc.Validate(ctx)
+	require.NoError(t, err, "invalid test fixture spec")
+
+	type testRequest struct {
+		method, path, body, contentType string
+	}
+	type testResponse struct {
+		statusCode int
+		body       string
+	}
+	tests := []struct {
+		name     string
+		handler  validatorTestHandler
+		options  []openapi3filter.ValidatorOption
+		request  testRequest
+		response testResponse
+		strict   bool
+	}{{
+		name:    "valid GET",
+		handler: validatorTestHandler{}.withDefaults(),
+		request: testRequest{
+			method: "GET",
+			path:   "/test/42?version=1",
+		},
+		response: testResponse{
+			200, validatorOkResponse,
+		},
+		strict: true,
+	}, {
+		name:    "valid POST",
+		handler: validatorTestHandler{}.withDefaults(),
+		request: testRequest{
+			method:      "POST",
+			path:        "/test?version=1",
+			body:        `{"name": "foo", "expected": 9, "actual": 10}`,
+			contentType: "application/json",
+		},
+		response: testResponse{
+			201, validatorOkResponse,
+		},
+		strict: true,
+	}, {
+		name:    "not found; no GET operation for /test",
+		handler: validatorTestHandler{}.withDefaults(),
+		request: testRequest{
+			method: "GET",
+			path:   "/test?version=1",
+		},
+		response: testResponse{
+			404, "not found\n",
+		},
+		strict: true,
+	}, {
+		name:    "not found; no POST operation for /test/42",
+		handler: validatorTestHandler{}.withDefaults(),
+		request: testRequest{
+			method: "POST",
+			path:   "/test/42?version=1",
+		},
+		response: testResponse{
+			404, "not found\n",
+		},
+		strict: true,
+	}, {
+		name:    "invalid request; missing version",
+		handler: validatorTestHandler{}.withDefaults(),
+		request: testRequest{
+			method: "GET",
+			path:   "/test/42",
+		},
+		response: testResponse{
+			400, "bad request\n",
+		},
+		strict: true,
+	}, {
+		name:    "invalid POST request; wrong property type",
+		handler: validatorTestHandler{}.withDefaults(),
+		request: testRequest{
+			method:      "POST",
+			path:        "/test?version=1",
+			body:        `{"name": "foo", "expected": "nine", "actual": "ten"}`,
+			contentType: "application/json",
+		},
+		response: testResponse{
+			400, "bad request\n",
+		},
+		strict: true,
+	}, {
+		name:    "invalid POST request; missing property",
+		handler: validatorTestHandler{}.withDefaults(),
+		request: testRequest{
+			method:      "POST",
+			path:        "/test?version=1",
+			body:        `{"name": "foo", "expected": 9}`,
+			contentType: "application/json",
+		},
+		response: testResponse{
+			400, "bad request\n",
+		},
+		strict: true,
+	}, {
+		name:    "invalid POST request; extra property",
+		handler: validatorTestHandler{}.withDefaults(),
+		request: testRequest{
+			method:      "POST",
+			path:        "/test?version=1",
+			body:        `{"name": "foo", "expected": 9, "actual": 10, "ideal": 8}`,
+			contentType: "application/json",
+		},
+		response: testResponse{
+			400, "bad request\n",
+		},
+		strict: true,
+	}, {
+		name: "valid response; 404 error",
+		handler: validatorTestHandler{
+			contentType:   "application/json",
+			errBody:       `{"code": "404", "message": "not found"}`,
+			errStatusCode: 404,
+		}.withDefaults(),
+		request: testRequest{
+			method: "GET",
+			path:   "/test/42?version=1",
+		},
+		response: testResponse{
+			404, `{"code": "404", "message": "not found"}`,
+		},
+		strict: true,
+	}, {
+		name: "invalid response; invalid error",
+		handler: validatorTestHandler{
+			errBody:       `"not found"`,
+			errStatusCode: 404,
+		}.withDefaults(),
+		request: testRequest{
+			method: "GET",
+			path:   "/test/42?version=1",
+		},
+		response: testResponse{
+			500, "server error\n",
+		},
+		strict: true,
+	}, {
+		name: "invalid POST response; not strict",
+		handler: validatorTestHandler{
+			postBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`,
+		}.withDefaults(),
+		request: testRequest{
+			method:      "POST",
+			path:        "/test?version=1",
+			body:        `{"name": "foo", "expected": 9, "actual": 10}`,
+			contentType: "application/json",
+		},
+		response: testResponse{
+			statusCode: 201,
+			body:       `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`,
+		},
+		strict: false,
+	}}
+	for i, test := range tests {
+		t.Logf("test#%d: %s", i, test.name)
+		t.Run(test.name, func(t *testing.T) {
+			// Set up a test HTTP server
+			var h http.Handler
+			s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				h.ServeHTTP(w, r)
+			}))
+			defer s.Close()
+
+			// Update the OpenAPI servers section with the test server URL This is
+			// needed by the router which matches request routes for OpenAPI
+			// validation.
+			doc.Servers = []*openapi3.Server{{URL: s.URL}}
+			err = doc.Validate(ctx)
+			require.NoError(t, err, "failed to validate with test server")
+
+			// Create the router and validator
+			router, err := gorillamux.NewRouter(doc)
+			require.NoError(t, err, "failed to create router")
+
+			// Now wrap the test handler with the validator middlware
+			v := openapi3filter.NewValidator(router, append(test.options, openapi3filter.Strict(test.strict))...)
+			h = v.Middleware(&test.handler)
+
+			// Test: make a client request
+			var requestBody io.Reader
+			if test.request.body != "" {
+				requestBody = bytes.NewBufferString(test.request.body)
+			}
+			req, err := http.NewRequest(test.request.method, s.URL+test.request.path, requestBody)
+			require.NoError(t, err, "failed to create request")
+
+			if test.request.contentType != "" {
+				req.Header.Set("Content-Type", test.request.contentType)
+			}
+			resp, err := s.Client().Do(req)
+			require.NoError(t, err, "request failed")
+			defer resp.Body.Close()
+			require.Equalf(t, test.response.statusCode, resp.StatusCode,
+				"response code expect %d got %d", test.response.statusCode, resp.StatusCode)
+
+			body, err := ioutil.ReadAll(resp.Body)
+			require.NoError(t, err, "failed to read response body")
+			require.Equalf(t, test.response.body, string(body),
+				"response body expect %q got %q", test.response.body, string(body))
+		})
+	}
+}
+
+func ExampleValidator() {
+	// OpenAPI specification for a simple service that squares integers, with
+	// some limitations.
+	doc, err := openapi3.NewLoader().LoadFromData([]byte(`
+info:
+openapi: 3.0.0
+info:
+  title: 'Validator - square example'
+  version: '0.0.0'
+paths:
+  /square/{x}:
+    get:
+      description: square an integer
+      parameters:
+        - name: x
+          in: path
+          schema:
+            type: integer
+          required: true
+      responses:
+        '200':
+          description: squared integer response
+          content:
+            "application/json":
+              schema:
+                type: object
+                properties:
+                  result:
+                    type: integer
+                    minimum: 0
+                    maximum: 1000000
+                required: [result]
+                additionalProperties: false`[1:]))
+	if err != nil {
+		panic(err)
+	}
+
+	// Square service handler sanity checks inputs, but just crashes on invalid
+	// requests.
+	squareHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		xParam := path.Base(r.URL.Path)
+		x, err := strconv.Atoi(xParam)
+		if err != nil {
+			panic(err)
+		}
+		w.Header().Set("Content-Type", "application/json")
+		result := map[string]interface{}{"result": x * x}
+		if x == 42 {
+			// An easter egg. Unfortunately, the spec does not allow additional properties...
+			result["comment"] = "the answer to the ulitimate question of life, the universe, and everything"
+		}
+		if err = json.NewEncoder(w).Encode(&result); err != nil {
+			panic(err)
+		}
+	})
+
+	// Start an http server.
+	var mainHandler http.Handler
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		// Why are we wrapping the main server handler with a closure here?
+		// Validation matches request Host: to server URLs in the spec. With an
+		// httptest.Server, the URL is dynamic and we have to create it first!
+		// In a real configured service, this is less likely to be needed.
+		mainHandler.ServeHTTP(w, r)
+	}))
+	defer srv.Close()
+
+	// Patch the OpenAPI spec to match the httptest.Server.URL. Only needed
+	// because the server URL is dynamic here.
+	doc.Servers = []*openapi3.Server{{URL: srv.URL}}
+	if err := doc.Validate(context.Background()); err != nil { // Assert our OpenAPI is valid!
+		panic(err)
+	}
+	// This router is used by the validator to match requests with the OpenAPI
+	// spec. It does not place restrictions on how the wrapped handler routes
+	// requests; use of gorilla/mux is just a validator implementation detail.
+	router, err := gorillamux.NewRouter(doc)
+	if err != nil {
+		panic(err)
+	}
+	// Strict validation will respond HTTP 500 if the service tries to emit a
+	// response that does not conform to the OpenAPI spec. Very useful for
+	// 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) {
+			// Customize validation error responses to use JSON
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(status)
+			json.NewEncoder(w).Encode(map[string]interface{}{
+				"status":  status,
+				"message": http.StatusText(status),
+			})
+		}))
+	// Now we can finally set the main server handler.
+	mainHandler = v.Middleware(squareHandler)
+
+	printResp := func(resp *http.Response, err error) {
+		if err != nil {
+			panic(err)
+		}
+		defer resp.Body.Close()
+		contents, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			panic(err)
+		}
+		fmt.Println(resp.StatusCode, strings.TrimSpace(string(contents)))
+	}
+	// Valid requests to our sum service
+	printResp(srv.Client().Get(srv.URL + "/square/2"))
+	printResp(srv.Client().Get(srv.URL + "/square/789"))
+	// 404 Not found requests - method or path not found
+	printResp(srv.Client().Post(srv.URL+"/square/2", "application/json", bytes.NewBufferString(`{"result": 5}`)))
+	printResp(srv.Client().Get(srv.URL + "/sum/2"))
+	printResp(srv.Client().Get(srv.URL + "/square/circle/4")) // Handler would process this; validation rejects it
+	printResp(srv.Client().Get(srv.URL + "/square"))
+	// 400 Bad requests - note they never reach the wrapped square handler (which would panic)
+	printResp(srv.Client().Get(srv.URL + "/square/five"))
+	// 500 Invalid responses
+	printResp(srv.Client().Get(srv.URL + "/square/42"))    // Our "easter egg" added a property which is not allowed
+	printResp(srv.Client().Get(srv.URL + "/square/65536")) // Answer overflows the maximum allowed value (1000000)
+	// Output:
+	// 200 {"result":4}
+	// 200 {"result":622521}
+	// 404 {"message":"Not Found","status":404}
+	// 404 {"message":"Not Found","status":404}
+	// 404 {"message":"Not Found","status":404}
+	// 404 {"message":"Not Found","status":404}
+	// 400 {"message":"Bad Request","status":400}
+	// 500 {"message":"Internal Server Error","status":500}
+	// 500 {"message":"Internal Server Error","status":500}
+}