diff --git a/_examples/custom_validation_error/README.md b/_examples/custom_validation_error/README.md new file mode 100644 index 0000000..00bd13c --- /dev/null +++ b/_examples/custom_validation_error/README.md @@ -0,0 +1,146 @@ +# Custom Validation Error Handler Example + +This example demonstrates how to use a custom validation error handler in fiber-oapi to return your own error structure when validation fails. + +## Problem + +By default, fiber-oapi returns a standard `ErrorResponse` structure when validation fails: + +```json +{ + "code": 400, + "details": "validation error message", + "type": "validation_error" +} +``` + +However, you might want all errors in your API to follow the same structure, including validation errors. + +## Solution + +Use the `ValidationErrorHandler` field in the `Config` to provide a custom function that handles validation errors: + +**Note:** When you configure a `ValidationErrorHandler`, validation is automatically enabled (`EnableValidation: true` by default). You don't need to explicitly set `EnableValidation: true` unless you're also configuring other options. + +```go +// Minimal configuration - validation is automatically enabled +oapi := fiberoapi.New(app, fiberoapi.Config{ + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + // Return your custom error structure + return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_ERROR", + }) + }, +}) +``` + +```go +// Or with explicit configuration +oapi := fiberoapi.New(app, fiberoapi.Config{ + EnableValidation: true, + EnableOpenAPIDocs: true, + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_ERROR", + }) + }, +}) +``` + +## Running the Example + +```bash +go run main.go +``` + +## Testing + +Try sending an invalid request: + +```bash +# Missing required fields +curl -X POST http://localhost:3000/users \ + -H "Content-Type: application/json" \ + -d '{}' + +# Response: +# { +# "success": false, +# "message": "Key: 'CreateUserInput.Name' Error:Field validation for 'Name' failed on the 'required' tag...", +# "code": "VALIDATION_ERROR" +# } +``` + +```bash +# Invalid email format +curl -X POST http://localhost:3000/users \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John", + "email": "invalid-email", + "age": 25 + }' + +# Response: +# { +# "success": false, +# "message": "Key: 'CreateUserInput.Email' Error:Field validation for 'Email' failed on the 'email' tag", +# "code": "VALIDATION_ERROR" +# } +``` + +```bash +# Valid request +curl -X POST http://localhost:3000/users \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "email": "john@example.com", + "age": 25 + }' + +# Response: +# { +# "id": 1, +# "name": "John Doe", +# "email": "john@example.com", +# "age": 25, +# "message": "User created successfully" +# } +``` + +## Advanced Usage + +You can also parse the validation error to extract detailed information: + +```go +ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + // Parse validator errors for more details + if validationErrs, ok := err.(validator.ValidationErrors); ok { + errors := make([]map[string]string, 0) + for _, fieldErr := range validationErrs { + errors = append(errors, map[string]string{ + "field": fieldErr.Field(), + "tag": fieldErr.Tag(), + "value": fmt.Sprintf("%v", fieldErr.Value()), + "message": fieldErr.Error(), + }) + } + return c.Status(fiber.StatusBadRequest).JSON(map[string]interface{}{ + "success": false, + "errors": errors, + }) + } + + // Fallback for other errors + return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_ERROR", + }) +}, +``` diff --git a/_examples/custom_validation_error/go.mod b/_examples/custom_validation_error/go.mod new file mode 100644 index 0000000..61b3c37 --- /dev/null +++ b/_examples/custom_validation_error/go.mod @@ -0,0 +1,31 @@ +module custom_validation_error_example + +go 1.25.1 + +replace github.com/labbs/fiber-oapi => ../.. + +require ( + github.com/gofiber/fiber/v2 v2.52.10 + github.com/labbs/fiber-oapi v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.17 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.66.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/_examples/custom_validation_error/go.sum b/_examples/custom_validation_error/go.sum new file mode 100644 index 0000000..4b834d8 --- /dev/null +++ b/_examples/custom_validation_error/go.sum @@ -0,0 +1,52 @@ +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY= +github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= +github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU= +github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_examples/custom_validation_error/main.go b/_examples/custom_validation_error/main.go new file mode 100644 index 0000000..7c2ff56 --- /dev/null +++ b/_examples/custom_validation_error/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + fiberoapi "github.com/labbs/fiber-oapi" +) + +// CustomErrorResponse is the structure for custom validation error responses +type CustomErrorResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Code string `json:"code"` +} + +// CreateUserInput is the input structure for creating a user +type CreateUserInput struct { + Name string `json:"name" validate:"required,min=3"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"required,min=18,max=100"` +} + +// CreateUserOutput is the output structure for creating a user +type CreateUserOutput struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Age int `json:"age"` + Message string `json:"message"` +} + +func main() { + app := fiber.New() + + // Configure fiber-oapi with a custom validation error handler + oapi := fiberoapi.New(app, fiberoapi.Config{ + EnableValidation: true, + EnableOpenAPIDocs: true, + // Define your custom handler for validation errors + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + // You can parse the validation error to extract more details + // or simply return your custom structure + return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_ERROR", + }) + }, + }) + + // Define your endpoint + fiberoapi.Post[CreateUserInput, CreateUserOutput, struct{}]( + oapi, + "/users", + func(c *fiber.Ctx, input CreateUserInput) (CreateUserOutput, struct{}) { + // User creation logic goes here + return CreateUserOutput{ + ID: 1, + Name: input.Name, + Email: input.Email, + Age: input.Age, + Message: "User created successfully", + }, struct{}{} + }, + fiberoapi.OpenAPIOptions{ + Summary: "Create a new user", + Description: "Creates a new user with validation", + Tags: []string{"users"}, + }, + ) + + log.Println("Server starting on :3000") + log.Println("OpenAPI docs available at http://localhost:3000/docs") + log.Fatal(oapi.Listen(":3000")) +} diff --git a/custom_validation_error_test.go b/custom_validation_error_test.go new file mode 100644 index 0000000..0346a04 --- /dev/null +++ b/custom_validation_error_test.go @@ -0,0 +1,257 @@ +package fiberoapi + +import ( + "bytes" + "encoding/json" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/assert" +) + +// CustomValidationError represents a custom error structure +type CustomValidationError struct { + Success bool `json:"success"` + Message string `json:"message"` + Code string `json:"code"` +} + +func TestCustomValidationErrorHandler(t *testing.T) { + app := fiber.New() + + // Configure with custom validation error handler + oapi := New(app, Config{ + EnableValidation: true, + EnableOpenAPIDocs: false, + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(CustomValidationError{ + Success: false, + Message: err.Error(), + Code: "CUSTOM_VALIDATION_ERROR", + }) + }, + }) + + type TestInput struct { + Name string `json:"name" validate:"required,min=3"` + Email string `json:"email" validate:"required,email"` + } + + type TestOutput struct { + Message string `json:"message"` + } + + Post[TestInput, TestOutput, struct{}]( + oapi, + "/test", + func(c *fiber.Ctx, input TestInput) (TestOutput, struct{}) { + return TestOutput{Message: "success"}, struct{}{} + }, + OpenAPIOptions{}, + ) + + // Test with invalid input (missing required field) + reqBody := map[string]interface{}{ + "name": "ab", // Too short (min=3) + } + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) + + // Verify custom error structure + body, _ := io.ReadAll(resp.Body) + t.Logf("Response body: %s", string(body)) + var customErr CustomValidationError + err = json.Unmarshal(body, &customErr) + assert.NoError(t, err) + assert.False(t, customErr.Success) + assert.Equal(t, "CUSTOM_VALIDATION_ERROR", customErr.Code) + assert.NotEmpty(t, customErr.Message) +} + +func TestDefaultValidationErrorWhenNoCustomHandler(t *testing.T) { + app := fiber.New() + + // Configure without custom validation error handler + oapi := New(app, Config{ + EnableValidation: true, + EnableOpenAPIDocs: false, + }) + + type TestInput struct { + Name string `json:"name" validate:"required"` + } + + type TestOutput struct { + Message string `json:"message"` + } + + Post[TestInput, TestOutput, struct{}]( + oapi, + "/test", + func(c *fiber.Ctx, input TestInput) (TestOutput, struct{}) { + return TestOutput{Message: "success"}, struct{}{} + }, + OpenAPIOptions{}, + ) + + // Test with invalid input + reqBody := map[string]interface{}{} + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) + + // Verify default error structure (ErrorResponse) + body, _ := io.ReadAll(resp.Body) + var defaultErr ErrorResponse + err = json.Unmarshal(body, &defaultErr) + assert.NoError(t, err) + assert.Equal(t, 400, defaultErr.Code) + assert.Equal(t, "validation_error", defaultErr.Type) + assert.NotEmpty(t, defaultErr.Details) +} + +func TestCustomValidationErrorHandlerWithDisabledDocs(t *testing.T) { + app := fiber.New() + + // Configure with custom validation error handler AND EnableOpenAPIDocs: false + // This tests that boolean config is properly respected when ValidationErrorHandler is set + oapi := New(app, Config{ + EnableValidation: true, + EnableOpenAPIDocs: false, // This should be respected + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(CustomValidationError{ + Success: false, + Message: err.Error(), + Code: "CUSTOM_ERROR", + }) + }, + }) + + type TestInput struct { + Name string `json:"name" validate:"required"` + } + + type TestOutput struct { + Message string `json:"message"` + } + + Post[TestInput, TestOutput, struct{}]( + oapi, + "/test", + func(c *fiber.Ctx, input TestInput) (TestOutput, struct{}) { + return TestOutput{Message: "success"}, struct{}{} + }, + OpenAPIOptions{}, + ) + + // Test validation error uses custom handler + reqBody := map[string]interface{}{} + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) + + // Verify custom error structure + body, _ := io.ReadAll(resp.Body) + var customErr CustomValidationError + err = json.Unmarshal(body, &customErr) + assert.NoError(t, err) + assert.Equal(t, "CUSTOM_ERROR", customErr.Code) + + // Verify that EnableOpenAPIDocs: false was respected + // The docs endpoint should not exist + req = httptest.NewRequest("GET", "/docs", nil) + resp, err = app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) +} + +func TestValidationErrorHandlerImpliesValidationEnabled(t *testing.T) { + app := fiber.New() + + // Configure ONLY ValidationErrorHandler without explicitly setting EnableValidation + // This should keep validation enabled by default since it makes sense + oapi := New(app, Config{ + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(CustomValidationError{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_HANDLER_ACTIVE", + }) + }, + }) + + type TestInput struct { + Name string `json:"name" validate:"required,min=3"` + } + + type TestOutput struct { + Message string `json:"message"` + } + + Post[TestInput, TestOutput, struct{}]( + oapi, + "/test", + func(c *fiber.Ctx, input TestInput) (TestOutput, struct{}) { + return TestOutput{Message: "success"}, struct{}{} + }, + OpenAPIOptions{}, + ) + + // Test that validation is still active and uses custom handler + reqBody := map[string]interface{}{ + "name": "ab", // Too short (min=3) + } + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) + + // Verify validation is active and custom handler was used + body, _ := io.ReadAll(resp.Body) + var customErr CustomValidationError + err = json.Unmarshal(body, &customErr) + assert.NoError(t, err) + assert.Equal(t, "VALIDATION_HANDLER_ACTIVE", customErr.Code) + assert.Contains(t, customErr.Message, "min") + + // Test with valid data to ensure endpoint works + reqBody = map[string]interface{}{ + "name": "John Doe", + } + bodyBytes, _ = json.Marshal(reqBody) + + req = httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err = app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusOK, resp.StatusCode) + + // Verify that OpenAPI docs are enabled by default (not silently disabled) + req = httptest.NewRequest("GET", "/docs", nil) + resp, err = app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusOK, resp.StatusCode, "OpenAPI docs should be enabled by default when only ValidationErrorHandler is configured") +} diff --git a/fiberoapi.go b/fiberoapi.go index d507dda..503092e 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -40,7 +40,8 @@ func New(app *fiber.App, config ...Config) *OApiApp { provided.SecuritySchemes != nil || provided.OpenAPIDocsPath != "" || provided.OpenAPIJSONPath != "" || - provided.OpenAPIYamlPath != "" + provided.OpenAPIYamlPath != "" || + provided.ValidationErrorHandler != nil // Only override boolean defaults if the config appears to be explicitly set if hasExplicitConfig { @@ -49,6 +50,24 @@ func New(app *fiber.App, config ...Config) *OApiApp { } // If no explicit config, keep the defaults (true, true, false) + // Special case: if ValidationErrorHandler is set and boolean fields seem to be using zero values, + // restore defaults since it makes no sense to have a validation error handler without validation + otherExplicitConfig := provided.EnableAuthorization || + provided.AuthService != nil || + provided.SecuritySchemes != nil || + provided.OpenAPIDocsPath != "" || + provided.OpenAPIJSONPath != "" || + provided.OpenAPIYamlPath != "" + + // Only restore defaults if ALL boolean fields are false (suggesting they weren't explicitly set) + allBooleansAreFalse := !provided.EnableValidation && !provided.EnableOpenAPIDocs && !provided.EnableAuthorization + + if provided.ValidationErrorHandler != nil && !otherExplicitConfig && allBooleansAreFalse { + // ValidationErrorHandler is the only explicit config, so restore defaults for boolean fields + cfg.EnableValidation = true // Keep validation enabled - the handler needs it + cfg.EnableOpenAPIDocs = true // Keep docs enabled - default behavior + } + // For EnableAuthorization: only set to true if explicitly provided if provided.EnableAuthorization { cfg.EnableAuthorization = provided.EnableAuthorization @@ -73,6 +92,9 @@ func New(app *fiber.App, config ...Config) *OApiApp { if provided.DefaultSecurity != nil { cfg.DefaultSecurity = provided.DefaultSecurity } + if provided.ValidationErrorHandler != nil { + cfg.ValidationErrorHandler = provided.ValidationErrorHandler + } } oapi := &OApiApp{ @@ -821,6 +843,11 @@ func Method[TInput any, TOutput any, TError any]( fiberHandler := func(c *fiber.Ctx) error { input, err := parseInput[TInput](app, c, fullPath, &options) if err != nil { + // Use custom validation error handler if configured + if app.config.ValidationErrorHandler != nil { + return app.config.ValidationErrorHandler(c, err) + } + // Default validation error response return c.Status(400).JSON(ErrorResponse{ Code: 400, Details: err.Error(), diff --git a/types.go b/types.go index 0bfc141..c4ac3fe 100644 --- a/types.go +++ b/types.go @@ -53,17 +53,22 @@ type PathInfo struct { Index int // Position in the path for validation } +// ValidationErrorHandler is a function type for handling validation errors +// It receives the fiber context and the validation error, and returns a fiber error response +type ValidationErrorHandler func(c *fiber.Ctx, err error) error + // Config represents configuration for the OApi wrapper type Config struct { - EnableValidation bool // Enable request validation (default: true) - EnableOpenAPIDocs bool // Enable automatic docs setup (default: true) - EnableAuthorization bool // Enable authorization validation (default: false) - OpenAPIDocsPath string // Path for documentation UI (default: "/docs") - OpenAPIJSONPath string // Path for OpenAPI JSON spec (default: "/openapi.json") - OpenAPIYamlPath string // Path for OpenAPI YAML spec (default: "/openapi.yaml") - AuthService AuthorizationService // Service for handling authentication and authorization - SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes - DefaultSecurity []map[string][]string // Default security requirements + EnableValidation bool // Enable request validation (default: true) + EnableOpenAPIDocs bool // Enable automatic docs setup (default: true) + EnableAuthorization bool // Enable authorization validation (default: false) + OpenAPIDocsPath string // Path for documentation UI (default: "/docs") + OpenAPIJSONPath string // Path for OpenAPI JSON spec (default: "/openapi.json") + OpenAPIYamlPath string // Path for OpenAPI YAML spec (default: "/openapi.yaml") + AuthService AuthorizationService // Service for handling authentication and authorization + SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes + DefaultSecurity []map[string][]string // Default security requirements + ValidationErrorHandler ValidationErrorHandler // Custom handler for validation errors } // OpenAPIOptions represents options for OpenAPI operations