Skip to content

Commit

Permalink
Moved Options & Params to main package
Browse files Browse the repository at this point in the history
Functions in param and option packages are aliases to the ones in the main package.
  • Loading branch information
EwenQuim committed Oct 30, 2024
1 parent 15b1845 commit 6727a0f
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 269 deletions.
12 changes: 6 additions & 6 deletions mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions openapi_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down
249 changes: 249 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
@@ -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(&param)
}

// 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
}
}
Loading

0 comments on commit 6727a0f

Please sign in to comment.