Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added function in openapi3.operation #370

Merged
merged 20 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
18 changes: 18 additions & 0 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,21 @@ func body[B any](c netHttpContext[B]) (B, error) {

return body, err
}

type ParamsIn struct {
Name string `query:"name"`
Authorization string `header:"Authorization"`
Token *http.Cookie `cookie:"Token"`
Limit *int `query:"limit"`
}

type ParamsOut struct {
CustomHeader string `header:"MyHeader"`
Token string `cookie:"Token,httpOnly,secure"`
}
TheRanomial marked this conversation as resolved.
Show resolved Hide resolved

type ContextWithBodyAndParams[Body any, ParamsIn any, ParamsOut any] interface {
ContextWithBody[Body]
Params() (ParamsIn, error)
SetParams(ParamsOut) error
}
49 changes: 49 additions & 0 deletions documentation/docs/guides/controllers.md
TheRanomial marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,55 @@ 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.

```go
func(c fuego.ContextWithBodyAndParams[MyInput,ParamsIn,ParamsOut]) (MyResponse, error)
```

This controller is used for advanced scenarios where you need:
Request body parsing and validation
Input parameter extraction from query, headers, cookies
Output parameter configuration

```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
Expand Down
72 changes: 64 additions & 8 deletions openapi.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fuego

import (
"errors"
"fmt"
"log/slog"
"net/http"
Expand Down Expand Up @@ -173,30 +174,67 @@ func RegisterOpenAPIOperation[T, B any](openapi *OpenAPI, route Route[T, B]) (*o
}

route.GenerateDefaultDescription()

if route.Operation.Summary == "" {
route.Operation.Summary = route.NameFromNamespace(camelToHuman)
}

if route.Operation.OperationID == "" {
route.GenerateDefaultOperationID()
}

// Request Body
if route.Operation.RequestBody == nil {
bodyTag := SchemaTagFromType(openapi, *new(B))

if bodyTag.Name != "unknown-interface" {
requestBody := newRequestBody[B](bodyTag, route.RequestContentTypes)

// add request body to operation
route.Operation.RequestBody = &openapi3.RequestBodyRef{
Value: requestBody,
}
}
}

// Response - globals
TheRanomial marked this conversation as resolved.
Show resolved Hide resolved
typeOfT := reflect.TypeOf(*new(T))
TheRanomial marked this conversation as resolved.
Show resolved Hide resolved
if typeOfT != nil {
if typeOfT.Kind() == reflect.Ptr {
typeOfT = typeOfT.Elem()
}
if typeOfT.Kind() == reflect.Struct {
for i := 0; i < typeOfT.NumField(); i++ {
field := typeOfT.Field(i)
if headerKey, ok := field.Tag.Lookup("header"); ok {
param := &openapi3.Parameter{
Name: headerKey,
In: "header",
Schema: openapi3.NewStringSchema().NewRef(),
}
if err := RegisterParameters(route.Operation, param); err != nil {
return nil, fmt.Errorf("failed to register parameter: %w", err)
}
}
if queryKey, ok := field.Tag.Lookup("query"); ok {
param := &openapi3.Parameter{
Name: queryKey,
In: "query",
Schema: openapi3.NewStringSchema().NewRef(),
}
if err := RegisterParameters(route.Operation, param); err != nil {
return nil, fmt.Errorf("failed to register parameter: %w", err)
}
}

if cookieKey, ok := field.Tag.Lookup("cookie"); ok {
param := &openapi3.Parameter{
Name: cookieKey,
In: "cookie",
Schema: openapi3.NewStringSchema().NewRef(),
}
if err := RegisterParameters(route.Operation, param); err != nil {
return nil, fmt.Errorf("failed to register parameter: %w", err)
}
}
}
}
}

for _, openAPIGlobalResponse := range openapi.globalOpenAPIResponses {
addResponseIfNotSet(
openapi,
Expand Down Expand Up @@ -236,9 +274,9 @@ func RegisterOpenAPIOperation[T, B any](openapi *OpenAPI, route Route[T, B]) (*o
if strings.HasSuffix(pathParam, "...") {
parameter.Description += " (might contain slashes)"
}

route.Operation.AddParameter(parameter)
}

for _, params := range route.Operation.Parameters {
if params.Value.In == "path" {
if !strings.Contains(route.Path, "{"+params.Value.Name) {
Expand All @@ -248,7 +286,6 @@ func RegisterOpenAPIOperation[T, B any](openapi *OpenAPI, route Route[T, B]) (*o
}

openapi.Description().AddOperation(route.Path, route.Method, route.Operation)

return route.Operation, nil
}

Expand Down Expand Up @@ -500,3 +537,22 @@ func transformTypeName(s string) string {

return prefix + "_" + inside
}

type MyOperation struct {
*openapi3.Operation
}

func RegisterParameters(operation *openapi3.Operation, parameters ...*openapi3.Parameter) error {
if operation.Parameters == nil {
operation.Parameters = openapi3.Parameters{}
}
for _, param := range parameters {
if param == nil {
return errors.New("parameter cannot be nil")
}
}
for _, param := range parameters {
operation.Parameters = append(operation.Parameters, &openapi3.ParameterRef{Value: param})
}
TheRanomial marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
103 changes: 103 additions & 0 deletions register_parameter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package fuego

import (
"testing"

"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_RegisterParameters(t *testing.T) {
TheRanomial marked this conversation as resolved.
Show resolved Hide resolved
t.Run("Add parameters to empty operation", func(t *testing.T) {
operation := &openapi3.Operation{}

param1 := &openapi3.Parameter{
Name: "testParam1",
In: "query",
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
}

err := RegisterParameters(operation, param1)

require.NoError(t, err)
assert.Len(t, operation.Parameters, 1)
assert.Equal(t, param1, operation.Parameters[0].Value)
})

t.Run("Add multiple parameters", func(t *testing.T) {
operation := &openapi3.Operation{}

param1 := &openapi3.Parameter{
Name: "testParam1",
In: "query",
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
}

param2 := &openapi3.Parameter{
Name: "testParam2",
In: "header",
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
}

err := RegisterParameters(operation, param1, param2)

require.NoError(t, err)
assert.Len(t, operation.Parameters, 2)
assert.Equal(t, param1, operation.Parameters[0].Value)
assert.Equal(t, param2, operation.Parameters[1].Value)
})

t.Run("Add parameters to operation with existing parameters", func(t *testing.T) {
operation := &openapi3.Operation{
Parameters: openapi3.Parameters{
&openapi3.ParameterRef{
Value: &openapi3.Parameter{
Name: "existingParam",
In: "path",
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
},
},
},
}

param1 := &openapi3.Parameter{
Name: "testParam1",
In: "query",
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
}

err := RegisterParameters(operation, param1)

require.NoError(t, err)
assert.Len(t, operation.Parameters, 2)
assert.Equal(t, "existingParam", operation.Parameters[0].Value.Name)
assert.Equal(t, "testParam1", operation.Parameters[1].Value.Name)
})

t.Run("Nil parameter results in error", func(t *testing.T) {
operation := &openapi3.Operation{}

err := RegisterParameters(operation, nil)

assert.Error(t, err)
assert.Contains(t, err.Error(), "parameter cannot be nil")
assert.Len(t, operation.Parameters, 0)
})

t.Run("Nil parameter in multi-parameter call", func(t *testing.T) {
operation := &openapi3.Operation{}

param1 := &openapi3.Parameter{
Name: "testParam1",
In: "query",
Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
}

err := RegisterParameters(operation, param1, nil)

assert.Error(t, err)
assert.Contains(t, err.Error(), "parameter cannot be nil")
assert.Len(t, operation.Parameters, 0)
})
}
Loading