Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,17 @@
}
}

// WithWalkSchemas registers a function that will be called on every schema
// in the OpenAPI document during [Engine.OutputOpenAPISpec]. The callback may
// modify schemas in place, making it useful for post-processing transformations
// like converting nullable representations or normalizing types.
// Multiple calls append additional walk functions; they run in registration order.
func WithWalkSchemas(fn openapi3.WalkSchemasFunc) EngineOption {
return func(e *Engine) {
e.OpenAPI.walkSchemasFns = append(e.OpenAPI.walkSchemasFns, fn)
}
}

// WithErrorHandler sets a customer error handler for the server
func WithErrorHandler(errorHandler func(ctx context.Context, err error) error) EngineOption {
return func(e *Engine) {
Expand Down Expand Up @@ -207,6 +218,10 @@
// spec generation
e.OpenAPI.resolveSchemaRefs()

for _, fn := range e.OpenAPI.walkSchemasFns {
e.OpenAPI.Description().WalkSchemas(fn)

Check failure on line 222 in engine.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `(*github.com/getkin/kin-openapi/openapi3.T).WalkSchemas` is not checked (errcheck)
}

// Validate
err := e.OpenAPI.Description().Validate(context.Background())
if err != nil {
Expand Down
94 changes: 30 additions & 64 deletions examples/basic/main.go
Original file line number Diff line number Diff line change
@@ -1,84 +1,50 @@
package main

import (
"context"
"errors"
"net/http"
"strings"

chiMiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/rs/cors"
"encoding/json"
"fmt"

"github.com/getkin/kin-openapi/openapi3"
"github.com/go-fuego/fuego"
"github.com/go-fuego/fuego/option"
)

type Received struct {
Name string `json:"name" validate:"required"`
type Owner struct {
Name string `json:"name"`
}

type MyResponse struct {
Message string `json:"message"`
BestFramework string `json:"best"`
type Widget struct {
Name *string `json:"name"` // pointer -> nullable scalar
Tags []string `json:"tags"` // slice
Owner *Owner `json:"owner"` // pointer to a named struct -> $ref
}

func main() {
s := fuego.NewServer(
fuego.WithAddr("localhost:8088"),
)

fuego.Use(s, cors.Default().Handler)
fuego.Use(s, chiMiddleware.Compress(5, "text/html", "text/css"))

// Fuego 🔥 handler with automatic OpenAPI generation, validation, (de)serialization and error handling
fuego.Post(s, "/", func(c fuego.ContextWithBody[Received]) (MyResponse, error) {
data, err := c.Body()
if err != nil {
return MyResponse{}, err
}

// read the request header test
if c.Request().Header.Get("test") != "test" {
return MyResponse{}, errors.New("test header not equal to 'test'")
}

c.Response().Header().Set("X-Hello", "World")

return MyResponse{
Message: "Hello, " + data.Name,
BestFramework: "Fuego!",
}, nil
},
option.Description("Say hello to the world"),
option.Header("test", "Just a test header"),
option.Cookie("test", "A Cookie!"),
fuego.WithEngineOptions(
fuego.WithWalkSchemas(func(_ string, ref *openapi3.SchemaRef) error {
schema := ref.Value
if schema.Nullable && schema.Type != nil && !schema.Type.Includes("null") {
*schema.Type = append(*schema.Type, "null")
schema.Nullable = false
}
return nil
}),
),
)

// Standard net/http handler with automatic OpenAPI route declaration
fuego.GetStd(s, "/std", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
fuego.Get(s, "/widget", func(c fuego.ContextNoBody) (Widget, error) {
return Widget{}, nil
})

s.Run()
}
// Owner is also returned on its own, where it is never null.
fuego.Get(s, "/owner", func(c fuego.ContextNoBody) (Owner, error) {
return Owner{}, nil
})

// InTransform will be called when using c.Body().
// It can be used to transform the entity and raise custom errors
func (r *Received) InTransform(context.Context) error {
r.Name = strings.ToLower(r.Name)
if r.Name == "fuego" {
return errors.New("fuego is not a name")
doc := s.OutputOpenAPISpec()
fmt.Println("openapi:", doc.OpenAPI)
for _, name := range []string{"Widget", "Owner"} {
out, _ := json.MarshalIndent(doc.Components.Schemas[name].Value, "", " ")
fmt.Printf("%s: %s\n", name, out)
}
return nil
}

// OutTransform will be called before sending data
func (r *MyResponse) OutTransform(context.Context) error {
r.Message = strings.ToUpper(r.Message)
return nil
}

var (
_ fuego.InTransformer = &Received{} // Ensure that *Received implements fuego.InTransformer
_ fuego.OutTransformer = &MyResponse{} // Ensure that *MyResponse implements fuego.OutTransformer
)
1 change: 1 addition & 0 deletions openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type OpenAPI struct {
generator *openapi3gen.Generator
globalOpenAPIResponses []openAPIResponse
Config OpenAPIConfig
walkSchemasFns []openapi3.WalkSchemasFunc
}

func (openAPI *OpenAPI) Description() *openapi3.T {
Expand Down
2 changes: 1 addition & 1 deletion schema_customizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func determineFieldConstraints(t reflect.Type, schema *openapi3.Schema) {
continue
}
if isReferenceType(f.Type.Kind()) && !hasRequired {
if !prop.Value.Type.Includes("null") {
if prop.Value != nil && !prop.Value.Type.Includes("null") {
types := openapi3.Types(append(prop.Value.Type.Slice(), "null"))
prop.Value.Type = &types
}
Expand Down
16 changes: 16 additions & 0 deletions schema_customizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ func TestDetermineFieldConstraints(t *testing.T) {
assert.True(t, schema.Properties["meta"].Value.Type.Includes("null"))
})

t.Run("pointer field is not handled by determineFieldConstraints", func(t *testing.T) {
type S struct {
Name *string `json:"name"`
}
schema := &openapi3.Schema{
Properties: openapi3.Schemas{
"name": &openapi3.SchemaRef{Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Nullable: true,
}},
},
}
determineFieldConstraints(reflect.TypeFor[S](), schema)
assert.True(t, schema.Properties["name"].Value.Nullable)
})

t.Run("string field is not nullable", func(t *testing.T) {
type S struct {
Name string `json:"name"`
Expand Down
Loading