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

Sets response headers in the OpenAPI spec #258

Merged
merged 2 commits into from
Dec 10, 2024
Merged
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
1 change: 1 addition & 0 deletions examples/petstore/controllers/pets.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
var optionPagination = option.Group(
option.QueryInt("per_page", "Number of items per page", param.Required()),
option.QueryInt("page", "Page number", param.Default(1), param.Example("1st page", 1), param.Example("42nd page", 42), param.Example("100th page", 100)),
option.ResponseHeader("Content-Range", "Total number of pets", param.StatusCodes(200, 206), param.Example("42 pets", "0-10/42")),
)

type PetsResources struct {
Expand Down
58 changes: 56 additions & 2 deletions examples/petstore/lib/testdata/doc/openapi.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,34 @@
}
}
},
"description": "OK"
"description": "OK",
"headers": {
"Content-Range": {
"examples": {
"42 pets": {
"value": "0-10/42"
}
},
"schema": {
"type": "string"
}
}
}
},
"206": {
"description": "OK",
"headers": {
"Content-Range": {
"examples": {
"42 pets": {
"value": "0-10/42"
}
},
"schema": {
"type": "string"
}
}
}
},
"400": {
"content": {
Expand Down Expand Up @@ -431,7 +458,34 @@
}
}
},
"description": "OK"
"description": "OK",
"headers": {
"Content-Range": {
"examples": {
"42 pets": {
"value": "0-10/42"
}
},
"schema": {
"type": "string"
}
}
}
},
"206": {
"description": "OK",
"headers": {
"Content-Range": {
"examples": {
"42 pets": {
"value": "0-10/42"
}
},
"schema": {
"type": "string"
}
}
}
},
"400": {
"content": {
Expand Down
11 changes: 9 additions & 2 deletions openapi_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,18 @@ type OpenAPIParam struct {
type OpenAPIParamOption struct {
Required bool
Nullable bool
Default any // Default value for the parameter
// Default value for the parameter.
// Type is checked at start-time.
Default any
Example string
Examples map[string]any
Type ParamType
GoType string // integer, string, bool
// integer, string, bool
GoType string
// Status codes for which this parameter is required.
// Only used for response parameters.
// If empty, it is required for 200 status codes.
StatusCodes []int
}

// Param registers a parameter for the route.
Expand Down
52 changes: 50 additions & 2 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,49 @@ func panicsIfNotCorrectType(openapiParam *openapi3.Parameter, exampleValue any)
return exampleValue
}

// Registers a parameter for the route. Prefer using the [Query], [QueryInt], [Header], [Cookie] shortcuts.
func OptionParam(name string, options ...func(*OpenAPIParam)) func(*BaseRoute) {
// Declare a response header for the route.
// This will be added to the OpenAPI spec, under the given default status code response.
// Example:
//
// ResponseHeader("Content-Range", "Pagination range", ParamExample("42 pets", "unit 0-9/42"), ParamDescription("https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range"))
// ResponseHeader("Set-Cookie", "Session cookie", ParamExample("session abc123", "session=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT"))
//
// The list of options is in the param package.
func OptionResponseHeader(name, description string, options ...func(*OpenAPIParam)) func(*BaseRoute) {
apiParam, openapiParam := buildParam(name, options...)

openapiParam.Name = ""
openapiParam.In = ""

if len(apiParam.StatusCodes) == 0 {
apiParam.StatusCodes = []int{200}
}

return func(r *BaseRoute) {
for _, code := range apiParam.StatusCodes {
codeString := strconv.Itoa(code)
responseForCurrentCode := r.Operation.Responses.Value(codeString)
if responseForCurrentCode == nil {
response := openapi3.NewResponse().WithDescription("OK")
r.Operation.AddResponse(code, response)
responseForCurrentCode = r.Operation.Responses.Value(codeString)
}

responseForCurrentCodeHeaders := responseForCurrentCode.Value.Headers
if responseForCurrentCodeHeaders == nil {
responseForCurrentCode.Value.Headers = make(map[string]*openapi3.HeaderRef)
}

responseForCurrentCode.Value.Headers[name] = &openapi3.HeaderRef{
Value: &openapi3.Header{
Parameter: *openapiParam,
},
}
}
}
}

func buildParam(name string, options ...func(*OpenAPIParam)) (OpenAPIParam, *openapi3.Parameter) {
param := OpenAPIParam{
Name: name,
}
Expand Down Expand Up @@ -187,6 +228,13 @@ func OptionParam(name string, options ...func(*OpenAPIParam)) func(*BaseRoute) {
openapiParam.Examples[name] = &openapi3.ExampleRef{Value: exampleOpenAPI}
}

return param, openapiParam
}

// Registers a parameter for the route. Prefer using the [Query], [QueryInt], [Header], [Cookie] shortcuts.
func OptionParam(name string, options ...func(*OpenAPIParam)) func(*BaseRoute) {
param, openapiParam := buildParam(name, options...)

return func(r *BaseRoute) {
r.Operation.AddParameter(openapiParam)
if r.Params == nil {
Expand Down
14 changes: 13 additions & 1 deletion option/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,19 @@ var Cookie = fuego.OptionCookie
// The list of options is in the param package.
var Path = fuego.OptionPath

// Registers a parameter for the route. Prefer using the [Query], [QueryInt], [Header], [Cookie] shortcuts.
// Declare a response header for the route.
// This will be added to the OpenAPI spec, under the given default status code response.
// Example:
//
// ResponseHeader("Content-Range", "Pagination range", ParamExample("42 pets", "unit 0-9/42"), ParamDescription("https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range"))
// ResponseHeader("Set-Cookie", "Session cookie", ParamExample("session abc123", "session=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT"))
//
// The list of options is in the param package.
var ResponseHeader = fuego.OptionResponseHeader

// Registers a parameter for the route.
//
// Deprecated: Use [Query], [QueryInt], [Header], [Cookie], [Path] instead.
var Param = fuego.OptionParam

// Tags adds one or more tags to the route.
Expand Down
26 changes: 26 additions & 0 deletions option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,32 @@ func TestHide(t *testing.T) {
})
}

func TestOptionResponseHeader(t *testing.T) {
t.Run("Declare a response header for the route", func(t *testing.T) {
s := fuego.NewServer()

route := fuego.Get(s, "/test", helloWorld,
fuego.OptionResponseHeader("X-Test", "test header", param.Example("test", "My Header"), param.Default("test"), param.Description("test description")),
)

require.NotNil(t, route.Operation.Responses.Value("200").Value.Headers["X-Test"])
require.Equal(t, "My Header", route.Operation.Responses.Value("200").Value.Headers["X-Test"].Value.Examples["test"].Value.Value)
require.Equal(t, "test description", route.Operation.Responses.Value("200").Value.Headers["X-Test"].Value.Description)
})

t.Run("Declare a response header for the route with multiple status codes", func(t *testing.T) {
s := fuego.NewServer()

route := fuego.Get(s, "/test", helloWorld,
fuego.OptionResponseHeader("X-Test", "test header", param.StatusCodes(200, 206)),
)

require.NotNil(t, route.Operation.Responses.Value("200").Value.Headers["X-Test"])
require.NotNil(t, route.Operation.Responses.Value("206").Value.Headers["X-Test"])
require.Nil(t, route.Operation.Responses.Value("400").Value.Headers["X-Test"])
})
}

func TestSecurity(t *testing.T) {
t.Run("single security requirement with defined scheme", func(t *testing.T) {
s := fuego.NewServer(
Expand Down
9 changes: 9 additions & 0 deletions param.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,12 @@ func ParamExample(exampleName string, value any) func(param *OpenAPIParam) {
param.Examples[exampleName] = value
}
}

// StatusCodes sets the status codes for which this parameter is required.
// Only used for response parameters.
// If empty, it is required for 200 status codes.
func ParamStatusCodes(codes ...int) func(param *OpenAPIParam) {
return func(param *OpenAPIParam) {
param.StatusCodes = codes
}
}
20 changes: 18 additions & 2 deletions param/param.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,34 @@ package param

import "github.com/go-fuego/fuego"

// Required sets the parameter as required.
// If the parameter is not present, the request will fail.
var Required = fuego.ParamRequired

// Nullable sets the parameter as nullable.
var Nullable = fuego.ParamNullable

var String = fuego.ParamString

// Integer sets the parameter type to integer.
// The query parameter is transmitted as a string in the URL, but it is parsed as an integer.
// Please prefer QueryInt for clarity.
var Integer = fuego.ParamInteger

// Bool sets the parameter type to boolean.
// The query parameter is transmitted as a string in the URL, but it is parsed as a boolean.
// Please prefer QueryBool for clarity.
var Bool = fuego.ParamBool

// Description sets the description for the parameter.
var Description = fuego.ParamDescription

// Default sets the default value for the parameter.
// Type is checked at start-time.
var Default = fuego.ParamDefault

// Example adds an example to the parameter. As per the OpenAPI 3.0 standard, the example must be given a name.
var Example = fuego.ParamExample

// StatusCodes sets the status codes for which this parameter is required.
// Only used for response parameters.
// If empty, it is required for 200 status codes.
var StatusCodes = fuego.ParamStatusCodes
Loading