diff --git a/ctx.go b/ctx.go index f5f7b18f..64deaad9 100644 --- a/ctx.go +++ b/ctx.go @@ -326,3 +326,9 @@ func body[B any](c netHttpContext[B]) (B, error) { return body, err } + +type ContextWithBodyAndParams[Body any, ParamsIn any, ParamsOut any] interface { + ContextWithBody[Body] + Params() (ParamsIn, error) + SetParams(ParamsOut) error +} diff --git a/documentation/docs/guides/controllers.md b/documentation/docs/guides/controllers.md index 798d64c8..e99ceea8 100644 --- a/documentation/docs/guides/controllers.md +++ b/documentation/docs/guides/controllers.md @@ -19,6 +19,54 @@ func (c fuego.ContextWithBody[MyInput]) (MyResponse, error) Used when the request has a body. Fuego will automatically parse the body and validate it using the input struct. +> 🚧 Below contains incoming syntax, not available currently + +```go +func(c fuego.ContextWithBodyAndParams[MyInput, ParamsIn, ParamsOut]) (MyResponse, error) +``` + +This controller is used to declare params with strong static typing. + +```go +type CreateUserRequest struct { + Name string `json:"name"` + Email string `json:"email"` +} + +type UserParams struct { + Limit *int `query:"limit"` + Group *string `header:"X-User-Group"` +} + +type UserResponseParams struct { + CustomHeader string `header:"X-Rate-Limit"` + SessionToken string `cookie:"session_token"` +} + +func CreateUserController( + c fuego.ContextWithBodyAndParams[CreateUserRequest, UserParams, UserResponseParams] +) (User, error) { + params, err := c.Params() + if err != nil { + return User{}, err + } + body, err := c.Body() + if err != nil { + return User{}, err + } + user, err := createUser(body, *params.Limit, *params.Group) + if err != nil { + return User{}, err + } + c.SetHeader("X-Rate-Limit", "100") + c.SetCookie(http.Cookie{ + Name: "session_token", + Value: generateSessionToken(), + }) + return user, nil +} +``` + ### Returning HTML ```go diff --git a/openapi.go b/openapi.go index cd9d4adc..c8094cd3 100644 --- a/openapi.go +++ b/openapi.go @@ -1,6 +1,7 @@ package fuego import ( + "errors" "fmt" "log/slog" "net/http" @@ -252,6 +253,40 @@ func RegisterOpenAPIOperation[T, B any](openapi *OpenAPI, route Route[T, B]) (*o return route.Operation, nil } +// RegisterParams registers the parameters of a given type to an OpenAPI operation. +// It inspects the fields of the provided struct, looking for "header" tags, and creates +// OpenAPI parameters for each tagged field. +func (route *RouteWithParams[Params, ResponseBody, RequestBody]) RegisterParams() error { + if route.Operation == nil { + route.Operation = openapi3.NewOperation() + } + params := *new(Params) + typeOfParams := reflect.TypeOf(params) + if typeOfParams == nil { + return errors.New("params cannot be nil") + } + if typeOfParams.Kind() == reflect.Ptr { + typeOfParams = typeOfParams.Elem() + } + + if typeOfParams.Kind() == reflect.Struct { + for i := range typeOfParams.NumField() { + field := typeOfParams.Field(i) + if headerKey, ok := field.Tag.Lookup("header"); ok { + OptionHeader(headerKey, "string")(&route.BaseRoute) + } + if queryKey, ok := field.Tag.Lookup("query"); ok { + OptionQuery(queryKey, "string")(&route.BaseRoute) + } + if cookieKey, ok := field.Tag.Lookup("cookie"); ok { + OptionCookie(cookieKey, "string")(&route.BaseRoute) + } + } + } + + return nil +} + func newRequestBody[RequestBody any](tag SchemaTag, consumes []string) *openapi3.RequestBody { content := openapi3.NewContentWithSchemaRef(&tag.SchemaRef, consumes) return openapi3.NewRequestBody(). diff --git a/parameter_registration_test.go b/parameter_registration_test.go new file mode 100644 index 00000000..b6f79a3a --- /dev/null +++ b/parameter_registration_test.go @@ -0,0 +1,65 @@ +package fuego + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_RegisterOpenAPIOperation(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) {} + s := NewServer() + + t.Run("Nil operation handling", func(t *testing.T) { + route := NewRouteWithParams[struct{}, struct{}, struct{}]( + http.MethodGet, + "/test", + handler, + s.Engine, + ) + route.Operation = nil + err := route.RegisterParams() + require.NoError(t, err) + assert.NotNil(t, route.Operation) + }) + + t.Run("Register with params", func(t *testing.T) { + route := NewRouteWithParams[struct { + QueryParam string `query:"queryParam"` + HeaderParam string `header:"headerParam"` + }, struct{}, struct{}]( + http.MethodGet, + "/some/path/{pathParam}", + handler, + s.Engine, + ) + err := route.RegisterParams() + require.NoError(t, err) + operation := route.Operation + assert.NotNil(t, operation) + assert.Len(t, operation.Parameters, 2) + + queryParam := operation.Parameters.GetByInAndName("query", "queryParam") + assert.NotNil(t, queryParam) + assert.Equal(t, "queryParam", queryParam.Name) + + headerParam := operation.Parameters.GetByInAndName("header", "headerParam") + assert.NotNil(t, headerParam) + assert.Equal(t, "headerParam", headerParam.Name) + }) + + t.Run("RegisterParams should not with interfaces", func(t *testing.T) { + route := NewRouteWithParams[any, struct{}, struct{}]( + http.MethodGet, + "/no-interfaces", + handler, + s.Engine, + OptionDefaultStatusCode(201), + ) + + err := route.RegisterParams() + require.Error(t, err) + }) +} diff --git a/route.go b/route.go index 75693938..612475f8 100644 --- a/route.go +++ b/route.go @@ -7,6 +7,16 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) +func NewRouteWithParams[RequestParams, ResponseBody, RequestBody any](method, path string, handler any, e *Engine, options ...func(*BaseRoute)) RouteWithParams[RequestParams, ResponseBody, RequestBody] { + return RouteWithParams[RequestParams, ResponseBody, RequestBody]{ + Route: NewRoute[ResponseBody, RequestBody](method, path, handler, e, options...), + } +} + +type RouteWithParams[RequestParams any, ResponseBody any, RequestBody any] struct { + Route[ResponseBody, RequestBody] +} + func NewRoute[T, B any](method, path string, handler any, e *Engine, options ...func(*BaseRoute)) Route[T, B] { return Route[T, B]{ BaseRoute: NewBaseRoute(method, path, handler, e, options...),