diff --git a/mux.go b/mux.go index cd537b7..08331a0 100644 --- a/mux.go +++ b/mux.go @@ -58,7 +58,7 @@ type BaseRoute struct { AcceptedContentTypes []string // Content types accepted for the request body. If nil, all content types (*/*) are accepted. Hidden bool // If true, the route will not be documented in the OpenAPI spec - MainRouter *Server // PRIVATE ref to the main router, used to register the route in the OpenAPI spec + mainRouter *Server // ref to the main router, used to register the route in the OpenAPI spec } // Capture all methods (GET, POST, PUT, PATCH, DELETE) and register a controller. @@ -127,7 +127,7 @@ func Register[T, B any](s *Server, route Route[T, B], controller http.Handler, o if route.Operation.OperationID == "" { route.Operation.OperationID = route.Method + "_" + strings.ReplaceAll(strings.ReplaceAll(route.Path, "{", ":"), "}", "") } - route.MainRouter = s + route.mainRouter = s return &route } @@ -181,12 +181,12 @@ func registerFuegoController[T, B any, Contexted ctx[B]](s *Server, method, path Path: path, FullName: FuncName(controller), Operation: openapi3.NewOperation(), - MainRouter: s.mainRouter, + mainRouter: s.mainRouter, } - if route.MainRouter == nil { - route.MainRouter = s + if route.mainRouter == nil { + route.mainRouter = s } - route.AcceptedContentTypes = route.MainRouter.acceptedContentTypes + route.AcceptedContentTypes = route.mainRouter.acceptedContentTypes acceptHeaderParameter := openapi3.NewHeaderParameter("Accept") acceptHeaderParameter.Schema = openapi3.NewStringSchema().NewRef() diff --git a/openapi_operations.go b/openapi_operations.go index 775a6bc..99db414 100644 --- a/openapi_operations.go +++ b/openapi_operations.go @@ -139,7 +139,7 @@ func (r Route[ResponseBody, RequestBody]) Tags(tags ...string) Route[ResponseBod // // fuego.Post(s, "/test", testControllerWithBody, option.RequestContentType("application/json")) func (r Route[ResponseBody, RequestBody]) RequestContentType(consumes ...string) Route[ResponseBody, RequestBody] { - bodyTag := SchemaTagFromType(r.MainRouter, *new(RequestBody)) + bodyTag := SchemaTagFromType(r.mainRouter, *new(RequestBody)) if bodyTag.Name != "unknown-interface" { requestBody := newRequestBody[RequestBody](bodyTag, consumes) @@ -168,7 +168,7 @@ func (r Route[ResponseBody, RequestBody]) AddTags(tags ...string) Route[Response // // Deprecated: Use `option.AddError` from github.com/go-fuego/fuego/option instead. func (r Route[ResponseBody, RequestBody]) AddError(code int, description string, errorType ...any) Route[ResponseBody, RequestBody] { - addResponse(r.MainRouter, r.Operation, code, description, errorType...) + addResponse(r.mainRouter, r.Operation, code, description, errorType...) return r } diff --git a/option.go b/option.go new file mode 100644 index 0000000..5b75b81 --- /dev/null +++ b/option.go @@ -0,0 +1,249 @@ +package fuego + +import ( + "net/http" + + "github.com/getkin/kin-openapi/openapi3" +) + +// Group allows to group routes under a common path. +// Useful to group often used middlewares or options and reuse them. +// Example: +// +// optionsPagination := option.Group( +// option.QueryInt("per_page", "Number of items per page", ParamRequired()), +// option.QueryInt("page", "Page number", ParamDefault(1)), +// ) +func GroupOptions(options ...func(*BaseRoute)) func(*BaseRoute) { + return func(r *BaseRoute) { + for _, option := range options { + option(r) + } + } +} + +// Middleware adds one or more route-scoped middleware. +func OptionMiddleware(middleware ...func(http.Handler) http.Handler) func(*BaseRoute) { + return func(r *BaseRoute) { + r.Middlewares = append(r.Middlewares, middleware...) + } +} + +// Declare a query parameter for the route. +// This will be added to the OpenAPI spec. +// Example: +// +// Query("name", "Filter by name", ParamExample("cat name", "felix"), ParamNullable()) +// +// The list of options is in the param package. +func OptionQuery(name, description string, options ...func(*OpenAPIParam)) func(*BaseRoute) { + options = append(options, ParamDescription(description), paramType(QueryParamType), ParamString()) + return func(r *BaseRoute) { + OptionParam(name, options...)(r) + } +} + +// Declare an integer query parameter for the route. +// This will be added to the OpenAPI spec. +// The query parameter is transmitted as a string in the URL, but it is parsed as an integer. +// Example: +// +// QueryInt("age", "Filter by age (in years)", ParamExample("3 years old", 3), ParamNullable()) +// +// The list of options is in the param package. +func OptionQueryInt(name, description string, options ...func(*OpenAPIParam)) func(*BaseRoute) { + options = append(options, ParamDescription(description), paramType(QueryParamType), ParamInteger()) + return func(r *BaseRoute) { + OptionParam(name, options...)(r) + } +} + +// Declare a boolean query parameter for the route. +// This will be added to the OpenAPI spec. +// The query parameter is transmitted as a string in the URL, but it is parsed as a boolean. +// Example: +// +// QueryBool("is_active", "Filter by active status", ParamExample("true", true), ParamNullable()) +// +// The list of options is in the param package. +func OptionQueryBool(name, description string, options ...func(*OpenAPIParam)) func(*BaseRoute) { + options = append(options, ParamDescription(description), paramType(QueryParamType), ParamBool()) + return func(r *BaseRoute) { + OptionParam(name, options...)(r) + } +} + +// Declare a header parameter for the route. +// This will be added to the OpenAPI spec. +// Example: +// +// Header("Authorization", "Bearer token", ParamRequired()) +// +// The list of options is in the param package. +func OptionHeader(name, description string, options ...func(*OpenAPIParam)) func(*BaseRoute) { + options = append(options, ParamDescription(description), paramType(HeaderParamType)) + return func(r *BaseRoute) { + OptionParam(name, options...)(r) + } +} + +// Declare a cookie parameter for the route. +// This will be added to the OpenAPI spec. +// Example: +// +// Cookie("session_id", "Session ID", ParamRequired()) +// +// The list of options is in the param package. +func OptionCookie(name, description string, options ...func(*OpenAPIParam)) func(*BaseRoute) { + options = append(options, ParamDescription(description), paramType(CookieParamType)) + return func(r *BaseRoute) { + OptionParam(name, options...)(r) + } +} + +func paramType(paramType ParamType) func(*OpenAPIParam) { + return func(param *OpenAPIParam) { + param.Type = paramType + } +} + +func panicsIfNotCorrectType(openapiParam *openapi3.Parameter, exampleValue any) any { + if exampleValue == nil { + return nil + } + if openapiParam.Schema.Value.Type.Is("integer") { + _, ok := exampleValue.(int) + if !ok { + panic("example value must be an integer") + } + } + if openapiParam.Schema.Value.Type.Is("boolean") { + _, ok := exampleValue.(bool) + if !ok { + panic("example value must be a boolean") + } + } + if openapiParam.Schema.Value.Type.Is("string") { + _, ok := exampleValue.(string) + if !ok { + panic("example value must be a string") + } + } + 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) { + param := OpenAPIParam{ + Name: name, + } + // Applies options to OpenAPIParam + for _, option := range options { + option(¶m) + } + + // Applies OpenAPIParam to openapi3.Parameter + // Why not use openapi3.NewHeaderParameter(name) directly? + // Because we might change the openapi3 library in the future, + // and we want to keep the flexibility to change the implementation without changing the API. + openapiParam := &openapi3.Parameter{ + Name: name, + In: string(param.Type), + Description: param.Description, + Schema: openapi3.NewStringSchema().NewRef(), + } + if param.GoType != "" { + openapiParam.Schema.Value.Type = &openapi3.Types{param.GoType} + } + openapiParam.Schema.Value.Nullable = param.Nullable + openapiParam.Schema.Value.Default = panicsIfNotCorrectType(openapiParam, param.Default) + + if param.Required { + openapiParam.Required = param.Required + } + for name, exampleValue := range param.Examples { + if openapiParam.Examples == nil { + openapiParam.Examples = make(openapi3.Examples) + } + exampleOpenAPI := openapi3.NewExample(name) + exampleOpenAPI.Value = panicsIfNotCorrectType(openapiParam, exampleValue) + openapiParam.Examples[name] = &openapi3.ExampleRef{Value: exampleOpenAPI} + } + + return func(r *BaseRoute) { + r.Operation.AddParameter(openapiParam) + if r.Params == nil { + r.Params = make(map[string]OpenAPIParam) + } + r.Params[name] = param + } +} + +// Tags adds one or more tags to the route. +func OptionTags(tags ...string) func(*BaseRoute) { + return func(r *BaseRoute) { + r.Operation.Tags = append(r.Operation.Tags, tags...) + } +} + +// Summary adds a summary to the route. +func OptionSummary(summary string) func(*BaseRoute) { + return func(r *BaseRoute) { + r.Operation.Summary = summary + } +} + +// Description adds a description to the route. +func OptionDescription(description string) func(*BaseRoute) { + return func(r *BaseRoute) { + r.Operation.Description = description + } +} + +// OperationID adds an operation ID to the route. +func OptionOperationID(operationID string) func(*BaseRoute) { + return func(r *BaseRoute) { + r.Operation.OperationID = operationID + } +} + +// Deprecated marks the route as deprecated. +func OptionDeprecated() func(*BaseRoute) { + return func(r *BaseRoute) { + r.Operation.Deprecated = true + } +} + +// AddError adds an error to the route. +func OptionAddError(code int, description string, errorType ...any) func(*BaseRoute) { + var responseSchema SchemaTag + return func(r *BaseRoute) { + if len(errorType) > 0 { + responseSchema = SchemaTagFromType(r.mainRouter, errorType[0]) + } else { + responseSchema = SchemaTagFromType(r.mainRouter, HTTPError{}) + } + content := openapi3.NewContentWithSchemaRef(&responseSchema.SchemaRef, []string{"application/json"}) + + response := openapi3.NewResponse(). + WithDescription(description). + WithContent(content) + r.Operation.AddResponse(code, response) + } +} + +// RequestContentType sets the accepted content types for the route. +// By default, the accepted content types is */*. +// This will override any options set at the server level. +func OptionRequestContentType(consumes ...string) func(*BaseRoute) { + return func(r *BaseRoute) { + r.AcceptedContentTypes = consumes + } +} + +// Hide hides the route from the OpenAPI spec. +func OptionHide() func(*BaseRoute) { + return func(r *BaseRoute) { + r.Hidden = true + } +} diff --git a/option/option.go b/option/option.go index 0e9c6dc..487b4f6 100644 --- a/option/option.go +++ b/option/option.go @@ -1,12 +1,7 @@ package option import ( - "net/http" - - "github.com/getkin/kin-openapi/openapi3" - "github.com/go-fuego/fuego" - "github.com/go-fuego/fuego/param" ) // Group allows to group routes under a common path. @@ -17,20 +12,10 @@ import ( // option.QueryInt("per_page", "Number of items per page", param.Required()), // option.QueryInt("page", "Page number", param.Default(1)), // ) -func Group(options ...func(*fuego.BaseRoute)) func(*fuego.BaseRoute) { - return func(r *fuego.BaseRoute) { - for _, option := range options { - option(r) - } - } -} +var Group = fuego.GroupOptions // Middleware adds one or more route-scoped middleware. -func Middleware(middleware ...func(http.Handler) http.Handler) func(*fuego.BaseRoute) { - return func(r *fuego.BaseRoute) { - r.Middlewares = append(r.Middlewares, middleware...) - } -} +var Middleware = fuego.OptionMiddleware // Declare a query parameter for the route. // This will be added to the OpenAPI spec. @@ -39,12 +24,7 @@ func Middleware(middleware ...func(http.Handler) http.Handler) func(*fuego.BaseR // Query("name", "Filter by name", param.Example("cat name", "felix"), param.Nullable()) // // The list of options is in the param package. -func Query(name, description string, options ...func(*fuego.OpenAPIParam)) func(*fuego.BaseRoute) { - options = append(options, param.Description(description), paramType(fuego.QueryParamType), param.String()) - return func(r *fuego.BaseRoute) { - Param(name, options...)(r) - } -} +var Query = fuego.OptionQuery // Declare an integer query parameter for the route. // This will be added to the OpenAPI spec. @@ -54,12 +34,7 @@ func Query(name, description string, options ...func(*fuego.OpenAPIParam)) func( // QueryInt("age", "Filter by age (in years)", param.Example("3 years old", 3), param.Nullable()) // // The list of options is in the param package. -func QueryInt(name, description string, options ...func(*fuego.OpenAPIParam)) func(*fuego.BaseRoute) { - options = append(options, param.Description(description), paramType(fuego.QueryParamType), param.Integer()) - return func(r *fuego.BaseRoute) { - Param(name, options...)(r) - } -} +var QueryInt = fuego.OptionQueryInt // Declare a boolean query parameter for the route. // This will be added to the OpenAPI spec. @@ -69,12 +44,7 @@ func QueryInt(name, description string, options ...func(*fuego.OpenAPIParam)) fu // QueryBool("is_active", "Filter by active status", param.Example("true", true), param.Nullable()) // // The list of options is in the param package. -func QueryBool(name, description string, options ...func(*fuego.OpenAPIParam)) func(*fuego.BaseRoute) { - options = append(options, param.Description(description), paramType(fuego.QueryParamType), param.Bool()) - return func(r *fuego.BaseRoute) { - Param(name, options...)(r) - } -} +var QueryBool = fuego.OptionQueryBool // Declare a header parameter for the route. // This will be added to the OpenAPI spec. @@ -83,12 +53,7 @@ func QueryBool(name, description string, options ...func(*fuego.OpenAPIParam)) f // Header("Authorization", "Bearer token", param.Required()) // // The list of options is in the param package. -func Header(name, description string, options ...func(*fuego.OpenAPIParam)) func(*fuego.BaseRoute) { - options = append(options, param.Description(description), paramType(fuego.HeaderParamType)) - return func(r *fuego.BaseRoute) { - Param(name, options...)(r) - } -} +var Header = fuego.OptionHeader // Declare a cookie parameter for the route. // This will be added to the OpenAPI spec. @@ -97,156 +62,33 @@ func Header(name, description string, options ...func(*fuego.OpenAPIParam)) func // Cookie("session_id", "Session ID", param.Required()) // // The list of options is in the param package. -func Cookie(name, description string, options ...func(*fuego.OpenAPIParam)) func(*fuego.BaseRoute) { - options = append(options, param.Description(description), paramType(fuego.CookieParamType)) - return func(r *fuego.BaseRoute) { - Param(name, options...)(r) - } -} - -func paramType(paramType fuego.ParamType) func(*fuego.OpenAPIParam) { - return func(param *fuego.OpenAPIParam) { - param.Type = paramType - } -} - -func panicsIfNotCorrectType(openapiParam *openapi3.Parameter, exampleValue any) any { - if exampleValue == nil { - return nil - } - if openapiParam.Schema.Value.Type.Is("integer") { - _, ok := exampleValue.(int) - if !ok { - panic("example value must be an integer") - } - } - if openapiParam.Schema.Value.Type.Is("boolean") { - _, ok := exampleValue.(bool) - if !ok { - panic("example value must be a boolean") - } - } - if openapiParam.Schema.Value.Type.Is("string") { - _, ok := exampleValue.(string) - if !ok { - panic("example value must be a string") - } - } - return exampleValue -} +var Cookie = fuego.OptionCookie // Registers a parameter for the route. Prefer using the [Query], [QueryInt], [Header], [Cookie] shortcuts. -func Param(name string, options ...func(*fuego.OpenAPIParam)) func(*fuego.BaseRoute) { - param := fuego.OpenAPIParam{ - Name: name, - } - // Applies options to fuego.OpenAPIParam - for _, option := range options { - option(¶m) - } - - // Applies fuego.OpenAPIParam to openapi3.Parameter - // Why not use openapi3.NewHeaderParameter(name) directly? - // Because we might change the openapi3 library in the future, - // and we want to keep the flexibility to change the implementation without changing the API. - openapiParam := &openapi3.Parameter{ - Name: name, - In: string(param.Type), - Description: param.Description, - Schema: openapi3.NewStringSchema().NewRef(), - } - if param.GoType != "" { - openapiParam.Schema.Value.Type = &openapi3.Types{param.GoType} - } - openapiParam.Schema.Value.Nullable = param.Nullable - openapiParam.Schema.Value.Default = panicsIfNotCorrectType(openapiParam, param.Default) - - if param.Required { - openapiParam.Required = param.Required - } - for name, exampleValue := range param.Examples { - if openapiParam.Examples == nil { - openapiParam.Examples = make(openapi3.Examples) - } - exampleOpenAPI := openapi3.NewExample(name) - exampleOpenAPI.Value = panicsIfNotCorrectType(openapiParam, exampleValue) - openapiParam.Examples[name] = &openapi3.ExampleRef{Value: exampleOpenAPI} - } - - return func(r *fuego.BaseRoute) { - r.Operation.AddParameter(openapiParam) - if r.Params == nil { - r.Params = make(map[string]fuego.OpenAPIParam) - } - r.Params[name] = param - } -} +var Param = fuego.OptionParam // Tags adds one or more tags to the route. -func Tags(tags ...string) func(*fuego.BaseRoute) { - return func(r *fuego.BaseRoute) { - r.Operation.Tags = append(r.Operation.Tags, tags...) - } -} +var Tags = fuego.OptionTags // Summary adds a summary to the route. -func Summary(summary string) func(*fuego.BaseRoute) { - return func(r *fuego.BaseRoute) { - r.Operation.Summary = summary - } -} +var Summary = fuego.OptionSummary // Description adds a description to the route. -func Description(description string) func(*fuego.BaseRoute) { - return func(r *fuego.BaseRoute) { - r.Operation.Description = description - } -} +var Description = fuego.OptionDescription // OperationID adds an operation ID to the route. -func OperationID(operationID string) func(*fuego.BaseRoute) { - return func(r *fuego.BaseRoute) { - r.Operation.OperationID = operationID - } -} +var OperationID = fuego.OptionOperationID // Deprecated marks the route as deprecated. -func Deprecated() func(*fuego.BaseRoute) { - return func(r *fuego.BaseRoute) { - r.Operation.Deprecated = true - } -} +var Deprecated = fuego.OptionDeprecated // AddError adds an error to the route. -func AddError(code int, description string, errorType ...any) func(*fuego.BaseRoute) { - var responseSchema fuego.SchemaTag - return func(r *fuego.BaseRoute) { - if len(errorType) > 0 { - responseSchema = fuego.SchemaTagFromType(r.MainRouter, errorType[0]) - } else { - responseSchema = fuego.SchemaTagFromType(r.MainRouter, fuego.HTTPError{}) - } - content := openapi3.NewContentWithSchemaRef(&responseSchema.SchemaRef, []string{"application/json"}) - - response := openapi3.NewResponse(). - WithDescription(description). - WithContent(content) - r.Operation.AddResponse(code, response) - } -} +var AddError = fuego.OptionAddError // RequestContentType sets the accepted content types for the route. // By default, the accepted content types is */*. // This will override any options set at the server level. -func RequestContentType(consumes ...string) func(*fuego.BaseRoute) { - return func(r *fuego.BaseRoute) { - r.AcceptedContentTypes = consumes - } -} +var RequestContentType = fuego.OptionRequestContentType // Hide hides the route from the OpenAPI spec. -func Hide() func(*fuego.BaseRoute) { - return func(r *fuego.BaseRoute) { - r.Hidden = true - } -} +var Hide = fuego.OptionHide diff --git a/option/option_test.go b/option_test.go similarity index 83% rename from option/option_test.go rename to option_test.go index 84ae8d6..7706440 100644 --- a/option/option_test.go +++ b/option_test.go @@ -1,4 +1,4 @@ -package option +package fuego_test import ( "log/slog" @@ -57,7 +57,7 @@ func TestPerRouteMiddleware(t *testing.T) { fuego.Get(s, "/withMiddleware", func(ctx *fuego.ContextNoBody) (string, error) { return "withmiddleware", nil - }, Middleware(dummyMiddleware)) + }, fuego.OptionMiddleware(dummyMiddleware)) fuego.Get(s, "/withoutMiddleware", func(ctx *fuego.ContextNoBody) (string, error) { return "withoutmiddleware", nil @@ -144,8 +144,8 @@ func TestUse(t *testing.T) { fuego.Get(s, "/test", func(ctx *fuego.ContextNoBody) (string, error) { return "test", nil }, - Middleware(orderMiddleware("Fourth!")), - Middleware(orderMiddleware("Fifth!")), + fuego.OptionMiddleware(orderMiddleware("Fourth!")), + fuego.OptionMiddleware(orderMiddleware("Fifth!")), ) r := httptest.NewRequest(http.MethodGet, "/test", nil) @@ -160,7 +160,7 @@ func TestUse(t *testing.T) { type ans struct{} -func TestParam(t *testing.T) { +func TestOptions(t *testing.T) { t.Run("warn if param is not found in openAPI config but called in controller (possibly typo)", func(t *testing.T) { handler := slogassert.New(t, slog.LevelWarn, nil) @@ -172,16 +172,16 @@ func TestParam(t *testing.T) { c.QueryParam("quantity") return ans{}, nil }, - Query("quantity", "some description"), - QueryInt("number", "some description", param.Example("3", 3)), - QueryBool("is_active", "some description"), + fuego.OptionQuery("quantity", "some description"), + fuego.OptionQueryInt("number", "some description", param.Example("3", 3)), + fuego.OptionQueryBool("is_active", "some description"), ) fuego.Get(s, "/typo", func(c fuego.ContextNoBody) (ans, error) { c.QueryParam("quantityy-with-a-typo") return ans{}, nil }, - Query("quantity", "some description"), + fuego.OptionQuery("quantity", "some description"), ) t.Run("correct param", func(t *testing.T) { @@ -213,7 +213,7 @@ func TestHeader(t *testing.T) { s := fuego.NewServer() fuego.Get(s, "/test", helloWorld, - Header("X-Test", "test header", param.Required(), param.Example("test", "My Header"), param.Default("test")), + fuego.OptionHeader("X-Test", "test header", param.Required(), param.Example("test", "My Header"), param.Default("test")), ) r := httptest.NewRequest(http.MethodGet, "/test", nil) @@ -230,25 +230,25 @@ func TestOpenAPI(t *testing.T) { s := fuego.NewServer() route := fuego.Get(s, "/test", helloWorld, - Summary("test summary"), - Description("test description"), - Tags("first-tag", "second-tag"), - Deprecated(), - OperationID("test-operation-id"), + fuego.OptionSummary("test summary"), + fuego.OptionDescription("test description"), + fuego.OptionTags("first-tag", "second-tag"), + fuego.OptionDeprecated(), + fuego.OptionOperationID("test-operation-id"), ) require.Equal(t, "test summary", route.Operation.Summary) - require.Equal(t, "controller: `github.com/go-fuego/fuego/option.helloWorld`\n\n---\n\ntest description", route.Operation.Description) + require.Equal(t, "controller: `github.com/go-fuego/fuego_test.helloWorld`\n\n---\n\ntest description", route.Operation.Description) require.Equal(t, []string{"first-tag", "second-tag"}, route.Operation.Tags) require.True(t, route.Operation.Deprecated) }) } func TestGroup(t *testing.T) { - paramsGroup := Group( - Header("X-Test", "test header", param.Required(), param.Example("test", "My Header"), param.Default("test")), - Query("name", "Filter by name", param.Example("cat name", "felix"), param.Nullable()), - Cookie("session", "Session cookie", param.Example("session", "1234"), param.Nullable()), + paramsGroup := fuego.GroupOptions( + fuego.OptionHeader("X-Test", "test header", param.Required(), param.Example("test", "My Header"), param.Default("test")), + fuego.OptionQuery("name", "Filter by name", param.Example("cat name", "felix"), param.Nullable()), + fuego.OptionCookie("session", "Session cookie", param.Example("session", "1234"), param.Nullable()), ) t.Run("Declare a group parameter for the route", func(t *testing.T) { @@ -270,13 +270,13 @@ func TestQuery(t *testing.T) { require.Panics(t, func() { fuego.Get(s, "/test", helloWorld, - QueryInt("age", "Filter by age (in years)", param.Example("3 years old", "3 but string"), param.Nullable()), + fuego.OptionQueryInt("age", "Filter by age (in years)", param.Example("3 years old", "3 but string"), param.Nullable()), ) }) require.Panics(t, func() { fuego.Get(s, "/test", helloWorld, - QueryBool("is_active", "Filter by active status", param.Example("true", 3), param.Nullable()), + fuego.OptionQueryBool("is_active", "Filter by active status", param.Example("true", 3), param.Nullable()), ) }) }) @@ -286,7 +286,7 @@ func TestQuery(t *testing.T) { require.Panics(t, func() { fuego.Get(s, "/test", helloWorld, - Query("name", "Filter by name", param.Default(3), param.Nullable()), + fuego.OptionQuery("name", "Filter by name", param.Default(3), param.Nullable()), ) }) }) @@ -296,7 +296,7 @@ func TestRequestContentType(t *testing.T) { t.Run("Declare a request content type for the route", func(t *testing.T) { s := fuego.NewServer() - route := fuego.Get(s, "/test", dummyController, RequestContentType("application/json")) + route := fuego.Get(s, "/test", dummyController, fuego.OptionRequestContentType("application/json")) r := httptest.NewRequest(http.MethodGet, "/test", nil) w := httptest.NewRecorder() @@ -311,7 +311,7 @@ func TestRequestContentType(t *testing.T) { t.Run("base", func(t *testing.T) { s := fuego.NewServer() route := fuego.Post(s, "/base", dummyController, - RequestContentType("application/json"), + fuego.OptionRequestContentType("application/json"), ) t.Log("route.Operation", route.Operation) @@ -326,7 +326,7 @@ func TestRequestContentType(t *testing.T) { t.Run("variadic", func(t *testing.T) { s := fuego.NewServer() route := fuego.Post(s, "/test", dummyController, - RequestContentType("application/json", "my/content-type"), + fuego.OptionRequestContentType("application/json", "my/content-type"), ) content := route.Operation.RequestBody.Value.Content @@ -343,7 +343,7 @@ func TestRequestContentType(t *testing.T) { s := fuego.NewServer(fuego.WithRequestContentType("application/json", "application/xml")) route := fuego.Post( s, "/test", dummyController, - RequestContentType("my/content-type"), + fuego.OptionRequestContentType("my/content-type"), ) content := route.Operation.RequestBody.Value.Content @@ -360,7 +360,7 @@ func TestAddError(t *testing.T) { t.Run("Declare an error for the route", func(t *testing.T) { s := fuego.NewServer() - route := fuego.Get(s, "/test", helloWorld, AddError(409, "Conflict: Pet with the same name already exists")) + route := fuego.Get(s, "/test", helloWorld, fuego.OptionAddError(409, "Conflict: Pet with the same name already exists")) t.Log("route.Operation.Responses", route.Operation.Responses) require.Equal(t, 5, route.Operation.Responses.Len()) // 200, 400, 409, 500, default @@ -373,7 +373,7 @@ func TestAddError(t *testing.T) { func TestHide(t *testing.T) { s := fuego.NewServer() - fuego.Get(s, "/hidden", helloWorld, Hide()) + fuego.Get(s, "/hidden", helloWorld, fuego.OptionHide()) fuego.Get(s, "/visible", helloWorld) spec := s.OutputOpenAPISpec() diff --git a/param.go b/param.go new file mode 100644 index 0000000..c441e70 --- /dev/null +++ b/param.go @@ -0,0 +1,53 @@ +package fuego + +func ParamRequired() func(param *OpenAPIParam) { + return func(param *OpenAPIParam) { + param.Required = true + } +} + +func ParamNullable() func(param *OpenAPIParam) { + return func(param *OpenAPIParam) { + param.Nullable = true + } +} + +func ParamString() func(param *OpenAPIParam) { + return func(param *OpenAPIParam) { + param.GoType = "string" + } +} + +func ParamInteger() func(param *OpenAPIParam) { + return func(param *OpenAPIParam) { + param.GoType = "integer" + } +} + +func ParamBool() func(param *OpenAPIParam) { + return func(param *OpenAPIParam) { + param.GoType = "boolean" + } +} + +func ParamDescription(description string) func(param *OpenAPIParam) { + return func(param *OpenAPIParam) { + param.Description = description + } +} + +func ParamDefault(value any) func(param *OpenAPIParam) { + return func(param *OpenAPIParam) { + param.Default = value + } +} + +// Example adds an example to the parameter. As per the OpenAPI 3.0 standard, the example must be given a name. +func ParamExample(exampleName string, value any) func(param *OpenAPIParam) { + return func(param *OpenAPIParam) { + if param.Examples == nil { + param.Examples = make(map[string]any) + } + param.Examples[exampleName] = value + } +} diff --git a/param/param.go b/param/param.go index f70b8cb..ad5dbd4 100644 --- a/param/param.go +++ b/param/param.go @@ -2,54 +2,18 @@ package param import "github.com/go-fuego/fuego" -func Required() func(param *fuego.OpenAPIParam) { - return func(param *fuego.OpenAPIParam) { - param.Required = true - } -} - -func Nullable() func(param *fuego.OpenAPIParam) { - return func(param *fuego.OpenAPIParam) { - param.Nullable = true - } -} - -func String() func(param *fuego.OpenAPIParam) { - return func(param *fuego.OpenAPIParam) { - param.GoType = "string" - } -} - -func Integer() func(param *fuego.OpenAPIParam) { - return func(param *fuego.OpenAPIParam) { - param.GoType = "integer" - } -} - -func Bool() func(param *fuego.OpenAPIParam) { - return func(param *fuego.OpenAPIParam) { - param.GoType = "boolean" - } -} - -func Description(description string) func(param *fuego.OpenAPIParam) { - return func(param *fuego.OpenAPIParam) { - param.Description = description - } -} - -func Default(value any) func(param *fuego.OpenAPIParam) { - return func(param *fuego.OpenAPIParam) { - param.Default = value - } -} - -// Example adds an example to the parameter. As per the OpenAPI 3.0 standard, the example must be given a name. -func Example(exampleName string, value any) func(param *fuego.OpenAPIParam) { - return func(param *fuego.OpenAPIParam) { - if param.Examples == nil { - param.Examples = make(map[string]any) - } - param.Examples[exampleName] = value - } -} +var Required = fuego.ParamRequired + +var Nullable = fuego.ParamNullable + +var String = fuego.ParamString + +var Integer = fuego.ParamInteger + +var Bool = fuego.ParamBool + +var Description = fuego.ParamDescription + +var Default = fuego.ParamDefault + +var Example = fuego.ParamExample diff --git a/param/param_test.go b/param_test.go similarity index 73% rename from param/param_test.go rename to param_test.go index 3eb94f4..412505d 100644 --- a/param/param_test.go +++ b/param_test.go @@ -1,16 +1,16 @@ -package param_test +package fuego_test import ( "strconv" "testing" + "github.com/stretchr/testify/require" + "github.com/go-fuego/fuego" "github.com/go-fuego/fuego/option" - "github.com/go-fuego/fuego/param" - "github.com/stretchr/testify/require" ) -func TestParam(t *testing.T) { +func TestParams(t *testing.T) { t.Run("All options", func(t *testing.T) { s := fuego.NewServer() @@ -21,9 +21,9 @@ func TestParam(t *testing.T) { return name + strconv.Itoa(age) + strconv.FormatBool(isok), nil }, - option.Query("name", "Name", param.Required(), param.Default("hey"), param.Example("example1", "you")), - option.QueryInt("age", "Age", param.Nullable(), param.Default(18), param.Example("example1", 1)), - option.QueryBool("is_ok", "Is OK?", param.Default(true), param.Example("example1", true)), + option.Query("name", "Name", fuego.ParamRequired(), fuego.ParamDefault("hey"), fuego.ParamExample("example1", "you")), + option.QueryInt("age", "Age", fuego.ParamNullable(), fuego.ParamDefault(18), fuego.ParamExample("example1", 1)), + option.QueryBool("is_ok", "Is OK?", fuego.ParamDefault(true), fuego.ParamExample("example1", true)), ) require.NotNil(t, route) diff --git a/options.go b/server.go similarity index 100% rename from options.go rename to server.go diff --git a/options_test.go b/server_test.go similarity index 100% rename from options_test.go rename to server_test.go