Skip to content

Commit bc62855

Browse files
refactor: restructure the project to have shared code pkg to reduce code redundancy
1 parent f39b035 commit bc62855

File tree

8 files changed

+252
-180
lines changed

8 files changed

+252
-180
lines changed

v5/go.mod renamed to echov5/go.mod

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
module github.com/oapi-codegen/echo-middleware/v5
1+
module github.com/oapi-codegen/echo-middleware/echov5
22

33
go 1.25.0
44

55
require (
66
github.com/getkin/kin-openapi v0.135.0
77
github.com/labstack/echo/v5 v5.1.0
8+
github.com/oapi-codegen/echo-middleware v0.0.0-00010101000000-000000000000
89
github.com/stretchr/testify v1.11.1
910
)
1011

@@ -20,8 +21,9 @@ require (
2021
github.com/oasdiff/yaml3 v0.0.9 // indirect
2122
github.com/perimeterx/marshmallow v1.1.5 // indirect
2223
github.com/pmezard/go-difflib v1.0.0 // indirect
23-
github.com/ugorji/go/codec v1.2.11 // indirect
2424
github.com/woodsbury/decimal128 v1.3.0 // indirect
2525
golang.org/x/time v0.14.0 // indirect
2626
gopkg.in/yaml.v3 v3.0.1 // indirect
2727
)
28+
29+
replace github.com/oapi-codegen/echo-middleware => ../
File renamed without changes.
Lines changed: 58 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Provide HTTP middleware functionality to validate that incoming requests conform to a given OpenAPI 3.x specification.
22
//
3-
// This provides middleware for an echo/v4 HTTP server.
3+
// This provides middleware for an echo/v5 HTTP server.
44
//
55
// This package is a lightweight wrapper over https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3filter from https://pkg.go.dev/github.com/getkin/kin-openapi.
66
//
@@ -13,16 +13,15 @@ import (
1313
"fmt"
1414
"log"
1515
"net/http"
16-
"net/url"
1716
"os"
18-
"strings"
1917

2018
"github.com/getkin/kin-openapi/openapi3"
2119
"github.com/getkin/kin-openapi/openapi3filter"
2220
"github.com/getkin/kin-openapi/routers"
2321
"github.com/getkin/kin-openapi/routers/gorillamux"
2422
"github.com/labstack/echo/v5"
2523
echomiddleware "github.com/labstack/echo/v5/middleware"
24+
"github.com/oapi-codegen/echo-middleware/internal/validation"
2625
)
2726

2827
const (
@@ -126,21 +125,13 @@ func OapiRequestValidatorWithOptions(spec *openapi3.T, options *Options) echo.Mi
126125
}
127126
}
128127

129-
// ValidateRequestFromContext is called from the middleware above and actually does the work
130-
// of validating a request.
131-
func ValidateRequestFromContext(ctx *echo.Context, router routers.Router, options *Options) *echo.HTTPError {
132-
req := ctx.Request()
128+
// ValidateRequestFromContext validates an incoming request using the OpenAPI spec and returns an error if validation fails.
129+
// It is called from the middleware and does the actual work of validating a request.
130+
func ValidateRequestFromContext(c *echo.Context, router routers.Router, options *Options) *echo.HTTPError {
131+
req := c.Request()
133132

134-
if options != nil && options.Prefix != "" {
135-
// Clone the request so downstream handlers still see the original path.
136-
clone := req.Clone(req.Context())
137-
clone.URL.Path = strings.TrimPrefix(clone.URL.Path, options.Prefix)
138-
req = clone
139-
}
140-
141-
route, pathParams, err := router.FindRoute(req)
142-
143-
// We failed to find a matching route for the request.
133+
// Find the matching route
134+
route, pathParams, err := validation.FindRoute(req, router, getPrefix(options))
144135
if err != nil {
145136
if errors.Is(err, routers.ErrMethodNotAllowed) {
146137
return echo.NewHTTPError(http.StatusMethodNotAllowed, "")
@@ -152,58 +143,33 @@ func ValidateRequestFromContext(ctx *echo.Context, router routers.Router, option
152143
// either server, or path, or something.
153144
return echo.NewHTTPError(http.StatusNotFound, e.Reason)
154145
default:
155-
// This should never happen today, but if our upstream code changes,
156-
// we don't want to crash the server, so handle the unexpected error.
146+
// If our upstream code changes, we don't want to crash the server,
147+
// so handle the unexpected error.
157148
return echo.NewHTTPError(http.StatusInternalServerError,
158149
fmt.Sprintf("error validating route: %s", err.Error()))
159150
}
160151
}
161152

162-
// gorillamux uses UseEncodedPath(), so path parameters are returned in
163-
// their percent-encoded form. Unescape them before passing to
164-
// openapi3filter, which expects decoded values.
165-
for k, v := range pathParams {
166-
if unescaped, err := url.PathUnescape(v); err == nil {
167-
pathParams[k] = unescaped
168-
}
169-
}
170-
171-
validationInput := &openapi3filter.RequestValidationInput{
172-
Request: req,
173-
PathParams: pathParams,
174-
Route: route,
153+
// Build validation context with Echo context and user data
154+
requestContext := context.WithValue(context.Background(), validation.EchoContextKey, c) //nolint:staticcheck
155+
if options != nil && options.UserData != nil {
156+
requestContext = context.WithValue(requestContext, validation.UserDataKey, options.UserData) //nolint:staticcheck
175157
}
176158

177-
// Pass the Echo context into the request validator, so that any callbacks
178-
// which it invokes make it available.
179-
requestContext := context.WithValue(context.Background(), EchoContextKey, ctx) //nolint:staticcheck
180-
181-
if options != nil {
182-
validationInput.Options = &options.Options
183-
validationInput.ParamDecoder = options.ParamDecoder
184-
requestContext = context.WithValue(requestContext, UserDataKey, options.UserData) //nolint:staticcheck
185-
}
186-
187-
err = openapi3filter.ValidateRequest(requestContext, validationInput)
188-
if err != nil {
189-
me := openapi3.MultiError{}
190-
if errors.As(err, &me) {
191-
errFunc := getMultiErrorHandlerFromOptions(options)
192-
return errFunc(me)
159+
// Perform OpenAPI validation
160+
validationErr := validation.ValidateRequest(requestContext, req, route, pathParams, getFilterOptions(options), getParamDecoder(options))
161+
if validationErr != nil {
162+
if validationErr.IsMultiError {
163+
multiErr := validationErr.MultiErrors
164+
if options != nil && options.MultiErrorHandler != nil {
165+
return options.MultiErrorHandler(multiErr)
166+
}
167+
return defaultMultiErrorHandler(multiErr)
193168
}
194169

195-
switch e := err.(type) {
196-
case *openapi3filter.RequestError:
197-
// We've got a bad request
198-
// Split up the verbose error by lines and return the first one
199-
// openapi errors seem to be multi-line with a decent message on the first
200-
errorLines := strings.Split(e.Error(), "\n")
201-
return &echo.HTTPError{
202-
Code: http.StatusBadRequest,
203-
Message: errorLines[0],
204-
}
205-
case *openapi3filter.SecurityRequirementsError:
206-
for _, err := range e.Errors {
170+
// Handle SecurityRequirementsError by extracting HTTPError if present
171+
if validationErr.IsSecurityError {
172+
for _, err := range validationErr.SecurityErrors {
207173
var httpErr *echo.HTTPError
208174
if errors.As(err, &httpErr) {
209175
return httpErr
@@ -213,19 +179,14 @@ func ValidateRequestFromContext(ctx *echo.Context, router routers.Router, option
213179
return echo.NewHTTPError(coder.StatusCode(), err.Error())
214180
}
215181
}
216-
return &echo.HTTPError{
217-
Code: http.StatusForbidden,
218-
Message: e.Error(),
219-
}
220-
default:
221-
// This should never happen today, but if our upstream code changes,
222-
// we don't want to crash the server, so handle the unexpected error.
223-
return &echo.HTTPError{
224-
Code: http.StatusInternalServerError,
225-
Message: fmt.Sprintf("error validating request: %s", err),
226-
}
182+
}
183+
184+
return &echo.HTTPError{
185+
Code: validationErr.StatusCode,
186+
Message: validationErr.Message,
227187
}
228188
}
189+
229190
return nil
230191
}
231192

@@ -260,26 +221,36 @@ func getSkipperFromOptions(options *Options) echomiddleware.Skipper {
260221
return options.Skipper
261222
}
262223

263-
// attempt to get the MultiErrorHandler from the options. If it is not set,
264-
// return a default handler
265-
func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler {
266-
if options == nil {
267-
return defaultMultiErrorHandler
224+
// defaultMultiErrorHandler returns a StatusBadRequest (400) and a list
225+
// of all of the errors. This method is called if there are no other
226+
// methods defined on the options.
227+
func defaultMultiErrorHandler(me openapi3.MultiError) *echo.HTTPError {
228+
return &echo.HTTPError{
229+
Code: http.StatusBadRequest,
230+
Message: me.Error(),
268231
}
232+
}
269233

270-
if options.MultiErrorHandler == nil {
271-
return defaultMultiErrorHandler
234+
// getPrefix gets the prefix from options if set
235+
func getPrefix(options *Options) string {
236+
if options == nil {
237+
return ""
272238
}
239+
return options.Prefix
240+
}
273241

274-
return options.MultiErrorHandler
242+
// getFilterOptions gets the openapi3filter.Options from options if set
243+
func getFilterOptions(options *Options) *openapi3filter.Options {
244+
if options == nil {
245+
return nil
246+
}
247+
return &options.Options
275248
}
276249

277-
// defaultMultiErrorHandler returns a StatusBadRequest (400) and a list
278-
// of all of the errors. This method is called if there are no other
279-
// methods defined on the options.
280-
func defaultMultiErrorHandler(me openapi3.MultiError) *echo.HTTPError {
281-
return &echo.HTTPError{
282-
Code: http.StatusBadRequest,
283-
Message: me.Error(),
284-
}
250+
// getParamDecoder gets the ParamDecoder from options if set
251+
func getParamDecoder(options *Options) openapi3filter.ContentParameterDecoder {
252+
if options == nil {
253+
return nil
254+
}
255+
return options.ParamDecoder
285256
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/getkin/kin-openapi/openapi3"
1313
"github.com/getkin/kin-openapi/openapi3filter"
1414
"github.com/labstack/echo/v5"
15-
middleware "github.com/oapi-codegen/echo-middleware/v5"
15+
middleware "github.com/oapi-codegen/echo-middleware/echov5"
1616
)
1717

1818
func ExampleOapiRequestValidatorWithOptions() {
File renamed without changes.

internal/validation/validate.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Package validation provides framework-agnostic OpenAPI request validation logic.
2+
//
3+
// This package contains the core validation logic that's independent of the Echo framework version.
4+
// It validates incoming HTTP requests against an OpenAPI 3.x specification.
5+
package validation
6+
7+
import (
8+
"context"
9+
"errors"
10+
"net/http"
11+
"net/url"
12+
"strings"
13+
14+
"github.com/getkin/kin-openapi/openapi3"
15+
"github.com/getkin/kin-openapi/openapi3filter"
16+
"github.com/getkin/kin-openapi/routers"
17+
)
18+
19+
const (
20+
EchoContextKey = "oapi-codegen/echo-context"
21+
UserDataKey = "oapi-codegen/user-data"
22+
)
23+
24+
// RequestValidationError represents an OpenAPI validation error
25+
type RequestValidationError struct {
26+
// StatusCode is the HTTP status code for this error
27+
StatusCode int
28+
// Message is a human-readable error message
29+
Message string
30+
// Internal is the underlying error
31+
Internal error
32+
// IsMultiError indicates if this is a MultiError
33+
IsMultiError bool
34+
// MultiErrors contains the multi-error if IsMultiError is true
35+
MultiErrors openapi3.MultiError
36+
// ErrorLines contains split error message lines
37+
ErrorLines []string
38+
// IsSecurityError indicates if this is a SecurityRequirementsError
39+
IsSecurityError bool
40+
// SecurityErrors contains the inner errors from SecurityRequirementsError
41+
SecurityErrors []error
42+
}
43+
44+
// FindRoute finds the matching route for a request, with optional prefix stripping
45+
func FindRoute(req *http.Request, router routers.Router, prefix string) (*routers.Route, map[string]string, error) {
46+
// Apply prefix stripping if needed
47+
if prefix != "" {
48+
clone := req.Clone(req.Context())
49+
clone.URL.Path = strings.TrimPrefix(clone.URL.Path, prefix)
50+
req = clone
51+
}
52+
53+
return router.FindRoute(req)
54+
}
55+
56+
// ValidateRequest validates an HTTP request against a matched route.
57+
// It returns nil if validation passes, or a RequestValidationError if it fails.
58+
// The context should have EchoContextKey and UserDataKey set by the caller.
59+
func ValidateRequest(ctx context.Context, req *http.Request, route *routers.Route, pathParams map[string]string, options *openapi3filter.Options, paramDecoder openapi3filter.ContentParameterDecoder) *RequestValidationError {
60+
// gorillamux uses UseEncodedPath(), so path parameters are returned in
61+
// their percent-encoded form. Unescape them before passing to
62+
// openapi3filter, which expects decoded values.
63+
for k, v := range pathParams {
64+
if unescaped, err := url.PathUnescape(v); err == nil {
65+
pathParams[k] = unescaped
66+
}
67+
}
68+
69+
// Build validation input
70+
validationInput := &openapi3filter.RequestValidationInput{
71+
Request: req,
72+
PathParams: pathParams,
73+
Route: route,
74+
}
75+
76+
if options != nil {
77+
validationInput.Options = options
78+
}
79+
80+
if paramDecoder != nil {
81+
validationInput.ParamDecoder = paramDecoder
82+
}
83+
84+
// Perform validation
85+
err := openapi3filter.ValidateRequest(ctx, validationInput)
86+
if err == nil {
87+
return nil // validation passed
88+
}
89+
90+
// Handle MultiError
91+
me := openapi3.MultiError{}
92+
if errors.As(err, &me) {
93+
return &RequestValidationError{
94+
StatusCode: http.StatusBadRequest,
95+
Message: me.Error(),
96+
Internal: me,
97+
IsMultiError: true,
98+
MultiErrors: me,
99+
}
100+
}
101+
102+
// Handle RequestError
103+
if re, ok := err.(*openapi3filter.RequestError); ok {
104+
errorLines := strings.Split(re.Error(), "\n")
105+
return &RequestValidationError{
106+
StatusCode: http.StatusBadRequest,
107+
Message: errorLines[0],
108+
Internal: err,
109+
ErrorLines: errorLines,
110+
}
111+
}
112+
113+
// Handle SecurityRequirementsError
114+
if sre, ok := err.(*openapi3filter.SecurityRequirementsError); ok {
115+
return &RequestValidationError{
116+
StatusCode: http.StatusForbidden,
117+
Message: sre.Error(),
118+
Internal: err,
119+
IsSecurityError: true,
120+
SecurityErrors: sre.Errors,
121+
}
122+
}
123+
124+
// Handle unknown error
125+
return &RequestValidationError{
126+
StatusCode: http.StatusInternalServerError,
127+
Message: "error validating request: " + err.Error(),
128+
Internal: err,
129+
}
130+
}

0 commit comments

Comments
 (0)