From 0bdc04f1b6b12b58f6ab7abfefbb7c92174fa332 Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Mon, 23 Dec 2024 12:25:51 -0500 Subject: [PATCH 01/12] BREAKING: create generic engine.OutputOpenAPISpec for use in all engines --- engine.go | 135 +++++++++++++++++++++- examples/generate-opengraph-image/main.go | 4 +- examples/petstore/lib/server_test.go | 6 +- openapi.go | 30 +---- openapi_test.go | 30 +++-- server.go | 67 ++++------- server_test.go | 39 ++++--- 7 files changed, 203 insertions(+), 108 deletions(-) diff --git a/engine.go b/engine.go index acb89568..c566b512 100644 --- a/engine.go +++ b/engine.go @@ -1,16 +1,139 @@ package fuego -func NewEngine() *Engine { - return &Engine{ - OpenAPI: NewOpenAPI(), - ErrorHandler: ErrorHandler, +import ( + "context" + "encoding/json" + "errors" + "log/slog" + "os" + "path/filepath" +) + +func NewEngine(config ...OpenAPIConfig) *Engine { + if len(config) > 1 { + panic("config should not be more than one") + } + engine := &Engine{ + OpenAPI: NewOpenAPI(), + OpenAPIConfig: defaultOpenAPIConfig, + ErrorHandler: ErrorHandler, + } + if len(config) > 0 { + engine.setOpenAPIConfig(config[0]) } + return engine } // The Engine is the main struct of the framework. type Engine struct { - OpenAPI *OpenAPI - ErrorHandler func(error) error + OpenAPI *OpenAPI + ErrorHandler func(error) error + OpenAPIConfig OpenAPIConfig acceptedContentTypes []string } + +type EngineOpenAPIConfig struct { + // If true, the engine will not print messages + DisableMessages bool + // If true, the engine will not save the OpenAPI JSON spec locally + DisableLocalSave bool + // Local path to save the OpenAPI JSON spec + JsonFilePath string + // Pretty prints the OpenAPI spec with proper JSON indentation + PrettyFormatJson bool +} + +// OutputOpenAPISpec takes the OpenAPI spec and outputs it to a JSON file +func (e *Engine) OutputOpenAPISpec() []byte { + e.OpenAPI.computeTags() + + // Validate + err := e.OpenAPI.Description().Validate(context.Background()) + if err != nil { + slog.Error("Error validating spec", "error", err) + } + + // Marshal spec to JSON + jsonSpec, err := e.marshalSpec() + if err != nil { + slog.Error("Error marshaling spec to JSON", "error", err) + } + + if !e.OpenAPIConfig.DisableLocalSave { + err := e.saveOpenAPIToFile(e.OpenAPIConfig.JsonFilePath, jsonSpec) + if err != nil { + slog.Error("Error saving spec to local path", "error", err, "path", e.OpenAPIConfig.JsonFilePath) + } + } + return jsonSpec +} + +func (e *Engine) saveOpenAPIToFile(jsonSpecLocalPath string, jsonSpec []byte) error { + jsonFolder := filepath.Dir(jsonSpecLocalPath) + + err := os.MkdirAll(jsonFolder, 0o750) + if err != nil { + return errors.New("error creating docs directory") + } + + f, err := os.Create(jsonSpecLocalPath) // #nosec G304 (file path provided by developer, not by user) + if err != nil { + return errors.New("error creating file") + } + defer f.Close() + + _, err = f.Write(jsonSpec) + if err != nil { + return errors.New("error writing file ") + } + + e.printOpenAPIMessage("JSON file: " + jsonSpecLocalPath) + return nil +} + +func (s *Engine) marshalSpec() ([]byte, error) { + if s.OpenAPIConfig.PrettyFormatJson { + return json.MarshalIndent(s.OpenAPI.Description(), "", " ") + } + return json.Marshal(s.OpenAPI.Description()) +} + +func (e *Engine) setOpenAPIConfig(config OpenAPIConfig) { + if config.JsonUrl != "" { + e.OpenAPIConfig.JsonUrl = config.JsonUrl + } + + if config.SwaggerUrl != "" { + e.OpenAPIConfig.SwaggerUrl = config.SwaggerUrl + } + + if config.JsonFilePath != "" { + e.OpenAPIConfig.JsonFilePath = config.JsonFilePath + } + + if config.UIHandler != nil { + e.OpenAPIConfig.UIHandler = config.UIHandler + } + + e.OpenAPIConfig.DisableSwagger = config.DisableSwagger + e.OpenAPIConfig.DisableSwaggerUI = config.DisableSwaggerUI + e.OpenAPIConfig.DisableLocalSave = config.DisableLocalSave + e.OpenAPIConfig.PrettyFormatJson = config.PrettyFormatJson + + if !validateJsonSpecUrl(e.OpenAPIConfig.JsonUrl) { + slog.Error("Error serving openapi json spec. Value of 's.OpenAPIConfig.JsonSpecUrl' option is not valid", "url", e.OpenAPIConfig.JsonUrl) + return + } + + if !validateSwaggerUrl(e.OpenAPIConfig.SwaggerUrl) { + slog.Error("Error serving swagger ui. Value of 's.OpenAPIConfig.SwaggerUrl' option is not valid", "url", e.OpenAPIConfig.SwaggerUrl) + return + } +} + +func (e *Engine) printOpenAPIMessage(msg string) { + if !e.OpenAPIConfig.DisableMessages { + slog.Info(msg) + } +} diff --git a/examples/generate-opengraph-image/main.go b/examples/generate-opengraph-image/main.go index d04757c8..7e258cb5 100644 --- a/examples/generate-opengraph-image/main.go +++ b/examples/generate-opengraph-image/main.go @@ -22,7 +22,9 @@ var optionReturnsPNG = func(br *fuego.BaseRoute) { func main() { s := fuego.NewServer( fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ - PrettyFormatJson: true, + EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ + PrettyFormatJson: true, + }, }), ) diff --git a/examples/petstore/lib/server_test.go b/examples/petstore/lib/server_test.go index 9d0b8914..05a1f9a9 100644 --- a/examples/petstore/lib/server_test.go +++ b/examples/petstore/lib/server_test.go @@ -15,8 +15,10 @@ func TestPetstoreOpenAPIGeneration(t *testing.T) { server := NewPetStoreServer( fuego.WithoutStartupMessages(), fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ - JsonFilePath: "testdata/doc/openapi.json", - PrettyFormatJson: true, + EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ + JsonFilePath: "testdata/doc/openapi.json", + PrettyFormatJson: true, + }, }), ) diff --git a/openapi.go b/openapi.go index 5b4b27e5..c622bc93 100644 --- a/openapi.go +++ b/openapi.go @@ -1,7 +1,6 @@ package fuego import ( - "context" "encoding/json" "fmt" "log/slog" @@ -111,29 +110,8 @@ func (s *Server) OutputOpenAPISpec() openapi3.T { Description: "local server", }) - s.OpenAPI.computeTags() - - // Validate - err := s.OpenAPI.Description().Validate(context.Background()) - if err != nil { - slog.Error("Error validating spec", "error", err) - } - - // Marshal spec to JSON - jsonSpec, err := s.marshalSpec() - if err != nil { - slog.Error("Error marshaling spec to JSON", "error", err) - } - if !s.OpenAPIConfig.DisableSwagger { - s.registerOpenAPIRoutes(jsonSpec) - } - - if !s.OpenAPIConfig.DisableLocalSave { - err := s.saveOpenAPIToFile(s.OpenAPIConfig.JsonFilePath, jsonSpec) - if err != nil { - slog.Error("Error saving spec to local path", "error", err, "path", s.OpenAPIConfig.JsonFilePath) - } + s.registerOpenAPIRoutes(s.Engine.OutputOpenAPISpec()) } return *s.OpenAPI.Description() @@ -188,12 +166,6 @@ func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { } } -func (s *Server) printOpenAPIMessage(msg string) { - if !s.disableStartupMessages { - slog.Info(msg) - } -} - func validateJsonSpecUrl(jsonSpecUrl string) bool { jsonSpecUrlRegexp := regexp.MustCompile(`^\/[\/a-zA-Z0-9\-\_]+(.json)$`) return jsonSpecUrlRegexp.MatchString(jsonSpecUrl) diff --git a/openapi_test.go b/openapi_test.go index 0cdfbd09..a469a369 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -315,7 +315,9 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { s := NewServer( WithOpenAPIConfig( OpenAPIConfig{ - JsonFilePath: docPath, + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: docPath, + }, }, ), ) @@ -336,8 +338,10 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { s := NewServer( WithOpenAPIConfig( OpenAPIConfig{ - JsonFilePath: docPath, - DisableLocalSave: true, + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: docPath, + DisableLocalSave: true, + }, }, ), ) @@ -356,9 +360,11 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { s := NewServer( WithOpenAPIConfig( OpenAPIConfig{ - JsonFilePath: docPath, - DisableLocalSave: true, - DisableSwagger: true, + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: docPath, + DisableLocalSave: true, + }, + DisableSwagger: true, }, ), ) @@ -378,8 +384,10 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { s := NewServer( WithOpenAPIConfig( OpenAPIConfig{ - JsonFilePath: docPath, - PrettyFormatJson: true, + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: docPath, + PrettyFormatJson: true, + }, }, ), ) @@ -494,8 +502,10 @@ func TestLocalSave(t *testing.T) { func TestAutoGroupTags(t *testing.T) { s := NewServer( WithOpenAPIConfig(OpenAPIConfig{ - DisableLocalSave: true, - DisableSwagger: true, + EngineOpenAPIConfig: EngineOpenAPIConfig{ + DisableLocalSave: true, + }, + DisableSwagger: true, }), ) Get(s, "/a", func(ContextNoBody) (MyStruct, error) { diff --git a/server.go b/server.go index 6183fee3..e694323c 100644 --- a/server.go +++ b/server.go @@ -15,21 +15,26 @@ import ( ) type OpenAPIConfig struct { - DisableSwagger bool // If true, the server will not serve the Swagger UI nor the OpenAPI JSON spec - DisableSwaggerUI bool // If true, the server will not serve the Swagger UI - DisableLocalSave bool // If true, the server will not save the OpenAPI JSON spec locally - SwaggerUrl string // URL to serve the swagger UI - UIHandler func(specURL string) http.Handler // Handler to serve the OpenAPI UI from spec URL - JsonUrl string // URL to serve the OpenAPI JSON spec - JsonFilePath string // Local path to save the OpenAPI JSON spec - PrettyFormatJson bool // Pretty prints the OpenAPI spec with proper JSON indentation + EngineOpenAPIConfig + // If true, the server will not serve the Swagger UI nor the OpenAPI JSON spec + DisableSwagger bool + // If true, the server will not serve the Swagger UI + DisableSwaggerUI bool + // URL to serve the swagger UI + SwaggerUrl string + // Handler to serve the OpenAPI UI from spec URL + UIHandler func(specURL string) http.Handler + // URL to serve the OpenAPI JSON spec + JsonUrl string } var defaultOpenAPIConfig = OpenAPIConfig{ - SwaggerUrl: "/swagger", - JsonUrl: "/swagger/openapi.json", - JsonFilePath: "doc/openapi.json", - UIHandler: DefaultOpenAPIHandler, + SwaggerUrl: "/swagger", + JsonUrl: "/swagger/openapi.json", + UIHandler: DefaultOpenAPIHandler, + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: "doc/openapi.json", + }, } type Server struct { @@ -103,8 +108,6 @@ func NewServer(options ...func(*Server)) *Server { Mux: http.NewServeMux(), Engine: NewEngine(), - OpenAPIConfig: defaultOpenAPIConfig, - Security: NewSecurity(), loggingConfig: defaultLoggingConfig, @@ -359,7 +362,10 @@ func WithErrorHandler(errorHandler func(err error) error) func(*Server) { // WithoutStartupMessages disables the startup message func WithoutStartupMessages() func(*Server) { - return func(c *Server) { c.disableStartupMessages = true } + return func(c *Server) { + c.disableStartupMessages = true + c.OpenAPIConfig.DisableMessages = true + } } // WithoutLogger disables the default logger. @@ -371,36 +377,7 @@ func WithoutLogger() func(*Server) { func WithOpenAPIConfig(openapiConfig OpenAPIConfig) func(*Server) { return func(s *Server) { - if openapiConfig.JsonUrl != "" { - s.OpenAPIConfig.JsonUrl = openapiConfig.JsonUrl - } - - if openapiConfig.SwaggerUrl != "" { - s.OpenAPIConfig.SwaggerUrl = openapiConfig.SwaggerUrl - } - - if openapiConfig.JsonFilePath != "" { - s.OpenAPIConfig.JsonFilePath = openapiConfig.JsonFilePath - } - - if openapiConfig.UIHandler != nil { - s.OpenAPIConfig.UIHandler = openapiConfig.UIHandler - } - - s.OpenAPIConfig.DisableSwagger = openapiConfig.DisableSwagger - s.OpenAPIConfig.DisableSwaggerUI = openapiConfig.DisableSwaggerUI - s.OpenAPIConfig.DisableLocalSave = openapiConfig.DisableLocalSave - s.OpenAPIConfig.PrettyFormatJson = openapiConfig.PrettyFormatJson - - if !validateJsonSpecUrl(s.OpenAPIConfig.JsonUrl) { - slog.Error("Error serving openapi json spec. Value of 's.OpenAPIConfig.JsonSpecUrl' option is not valid", "url", s.OpenAPIConfig.JsonUrl) - return - } - - if !validateSwaggerUrl(s.OpenAPIConfig.SwaggerUrl) { - slog.Error("Error serving swagger ui. Value of 's.OpenAPIConfig.SwaggerUrl' option is not valid", "url", s.OpenAPIConfig.SwaggerUrl) - return - } + s.Engine.setOpenAPIConfig(openapiConfig) } } diff --git a/server_test.go b/server_test.go index c263fbed..43b1eda1 100644 --- a/server_test.go +++ b/server_test.go @@ -86,12 +86,14 @@ func TestWithOpenAPIConfig(t *testing.T) { t.Run("with custom values", func(t *testing.T) { s := NewServer( WithOpenAPIConfig(OpenAPIConfig{ - SwaggerUrl: "/api", - JsonUrl: "/api/openapi.json", - JsonFilePath: "openapi.json", - DisableSwagger: true, - DisableLocalSave: true, - PrettyFormatJson: true, + SwaggerUrl: "/api", + JsonUrl: "/api/openapi.json", + DisableSwagger: true, + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: "openapi.json", + DisableLocalSave: true, + PrettyFormatJson: true, + }, }), ) @@ -107,18 +109,22 @@ func TestWithOpenAPIConfig(t *testing.T) { t.Run("with invalid path", func(t *testing.T) { NewServer( WithOpenAPIConfig(OpenAPIConfig{ - JsonFilePath: "path/to/jsonSpec", - SwaggerUrl: "p i", - JsonUrl: "pi/op enapi.json", + SwaggerUrl: "p i", + JsonUrl: "pi/op enapi.json", + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: "path/to/jsonSpec", + }, }), ) }) t.Run("with invalid url", func(t *testing.T) { NewServer( WithOpenAPIConfig(OpenAPIConfig{ - JsonFilePath: "path/to/jsonSpec.json", - JsonUrl: "pi/op enapi.json", - SwaggerUrl: "p i", + JsonUrl: "pi/op enapi.json", + SwaggerUrl: "p i", + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: "path/to/jsonSpec.json", + }, }), ) }) @@ -126,9 +132,11 @@ func TestWithOpenAPIConfig(t *testing.T) { t.Run("with invalid url", func(t *testing.T) { NewServer( WithOpenAPIConfig(OpenAPIConfig{ - JsonFilePath: "path/to/jsonSpec.json", - JsonUrl: "/api/openapi.json", - SwaggerUrl: "invalid path", + JsonUrl: "/api/openapi.json", + SwaggerUrl: "invalid path", + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: "path/to/jsonSpec.json", + }, }), ) }) @@ -272,6 +280,7 @@ func TestWithoutStartupMessages(t *testing.T) { ) require.True(t, s.disableStartupMessages) + require.True(t, s.Engine.OpenAPIConfig.DisableMessages) } func TestWithoutAutoGroupTags(t *testing.T) { From 6203e5201ebf69cab0ae675e0576ceb0c139ec42 Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Mon, 23 Dec 2024 19:37:06 -0500 Subject: [PATCH 02/12] BREAKING: add WithEngineOptions and move WithOpenAPIConfig to a WithEngineOption --- engine.go | 81 +++++++++++------------ examples/generate-opengraph-image/main.go | 12 ++-- examples/petstore/lib/server_test.go | 14 ++-- openapi_handler_test.go | 20 +++--- openapi_test.go | 70 +++++++++++--------- server.go | 6 +- server_test.go | 74 ++++++++++++--------- 7 files changed, 153 insertions(+), 124 deletions(-) diff --git a/engine.go b/engine.go index c566b512..410b883c 100644 --- a/engine.go +++ b/engine.go @@ -9,19 +9,16 @@ import ( "path/filepath" ) -func NewEngine(config ...OpenAPIConfig) *Engine { - if len(config) > 1 { - panic("config should not be more than one") - } - engine := &Engine{ +func NewEngine(options ...func(*Engine)) *Engine { + e := &Engine{ OpenAPI: NewOpenAPI(), OpenAPIConfig: defaultOpenAPIConfig, ErrorHandler: ErrorHandler, } - if len(config) > 0 { - engine.setOpenAPIConfig(config[0]) + for _, option := range options { + option(e) } - return engine + return e } // The Engine is the main struct of the framework. @@ -44,6 +41,41 @@ type EngineOpenAPIConfig struct { PrettyFormatJson bool } +func WithOpenAPIConfig(config OpenAPIConfig) func(*Engine) { + return func(e *Engine) { + if config.JsonUrl != "" { + e.OpenAPIConfig.JsonUrl = config.JsonUrl + } + + if config.SwaggerUrl != "" { + e.OpenAPIConfig.SwaggerUrl = config.SwaggerUrl + } + + if config.JsonFilePath != "" { + e.OpenAPIConfig.JsonFilePath = config.JsonFilePath + } + + if config.UIHandler != nil { + e.OpenAPIConfig.UIHandler = config.UIHandler + } + + e.OpenAPIConfig.DisableSwagger = config.DisableSwagger + e.OpenAPIConfig.DisableSwaggerUI = config.DisableSwaggerUI + e.OpenAPIConfig.DisableLocalSave = config.DisableLocalSave + e.OpenAPIConfig.PrettyFormatJson = config.PrettyFormatJson + + if !validateJsonSpecUrl(e.OpenAPIConfig.JsonUrl) { + slog.Error("Error serving openapi json spec. Value of 's.OpenAPIConfig.JsonSpecUrl' option is not valid", "url", e.OpenAPIConfig.JsonUrl) + return + } + + if !validateSwaggerUrl(e.OpenAPIConfig.SwaggerUrl) { + slog.Error("Error serving swagger ui. Value of 's.OpenAPIConfig.SwaggerUrl' option is not valid", "url", e.OpenAPIConfig.SwaggerUrl) + return + } + } +} + // OutputOpenAPISpec takes the OpenAPI spec and outputs it to a JSON file func (e *Engine) OutputOpenAPISpec() []byte { e.OpenAPI.computeTags() @@ -99,39 +131,6 @@ func (s *Engine) marshalSpec() ([]byte, error) { return json.Marshal(s.OpenAPI.Description()) } -func (e *Engine) setOpenAPIConfig(config OpenAPIConfig) { - if config.JsonUrl != "" { - e.OpenAPIConfig.JsonUrl = config.JsonUrl - } - - if config.SwaggerUrl != "" { - e.OpenAPIConfig.SwaggerUrl = config.SwaggerUrl - } - - if config.JsonFilePath != "" { - e.OpenAPIConfig.JsonFilePath = config.JsonFilePath - } - - if config.UIHandler != nil { - e.OpenAPIConfig.UIHandler = config.UIHandler - } - - e.OpenAPIConfig.DisableSwagger = config.DisableSwagger - e.OpenAPIConfig.DisableSwaggerUI = config.DisableSwaggerUI - e.OpenAPIConfig.DisableLocalSave = config.DisableLocalSave - e.OpenAPIConfig.PrettyFormatJson = config.PrettyFormatJson - - if !validateJsonSpecUrl(e.OpenAPIConfig.JsonUrl) { - slog.Error("Error serving openapi json spec. Value of 's.OpenAPIConfig.JsonSpecUrl' option is not valid", "url", e.OpenAPIConfig.JsonUrl) - return - } - - if !validateSwaggerUrl(e.OpenAPIConfig.SwaggerUrl) { - slog.Error("Error serving swagger ui. Value of 's.OpenAPIConfig.SwaggerUrl' option is not valid", "url", e.OpenAPIConfig.SwaggerUrl) - return - } -} - func (e *Engine) printOpenAPIMessage(msg string) { if !e.OpenAPIConfig.DisableMessages { slog.Info(msg) diff --git a/examples/generate-opengraph-image/main.go b/examples/generate-opengraph-image/main.go index 7e258cb5..5c7f0637 100644 --- a/examples/generate-opengraph-image/main.go +++ b/examples/generate-opengraph-image/main.go @@ -21,11 +21,13 @@ var optionReturnsPNG = func(br *fuego.BaseRoute) { func main() { s := fuego.NewServer( - fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ - EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ - PrettyFormatJson: true, - }, - }), + fuego.WithEngineOptions( + fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ + EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ + PrettyFormatJson: true, + }, + }), + ), ) fuego.GetStd(s, "/{title}", controller.OpenGraphHandler, diff --git a/examples/petstore/lib/server_test.go b/examples/petstore/lib/server_test.go index 05a1f9a9..cc9cbbdb 100644 --- a/examples/petstore/lib/server_test.go +++ b/examples/petstore/lib/server_test.go @@ -14,12 +14,14 @@ import ( func TestPetstoreOpenAPIGeneration(t *testing.T) { server := NewPetStoreServer( fuego.WithoutStartupMessages(), - fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ - EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ - JsonFilePath: "testdata/doc/openapi.json", - PrettyFormatJson: true, - }, - }), + fuego.WithEngineOptions( + fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ + EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ + JsonFilePath: "testdata/doc/openapi.json", + PrettyFormatJson: true, + }, + }), + ), ) server.OutputOpenAPISpec() diff --git a/openapi_handler_test.go b/openapi_handler_test.go index 0e1bfe59..0b031217 100644 --- a/openapi_handler_test.go +++ b/openapi_handler_test.go @@ -37,11 +37,13 @@ func TestUIHandler(t *testing.T) { t.Run("wrap DefaultOpenAPIHandler behind a middleware", func(t *testing.T) { s := NewServer( - WithOpenAPIConfig(OpenAPIConfig{ - UIHandler: func(specURL string) http.Handler { - return dummyMiddleware(DefaultOpenAPIHandler(specURL)) - }, - }), + WithEngineOptions( + WithOpenAPIConfig(OpenAPIConfig{ + UIHandler: func(specURL string) http.Handler { + return dummyMiddleware(DefaultOpenAPIHandler(specURL)) + }, + }), + ), ) s.OutputOpenAPISpec() @@ -59,9 +61,11 @@ func TestUIHandler(t *testing.T) { t.Run("disabling UI", func(t *testing.T) { s := NewServer( - WithOpenAPIConfig(OpenAPIConfig{ - DisableSwaggerUI: true, - }), + WithEngineOptions( + WithOpenAPIConfig(OpenAPIConfig{ + DisableSwaggerUI: true, + }), + ), ) s.OutputOpenAPISpec() diff --git a/openapi_test.go b/openapi_test.go index a469a369..02f6a4ba 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -313,12 +313,14 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { docPath := "doc/openapi.json" t.Run("base", func(t *testing.T) { s := NewServer( - WithOpenAPIConfig( - OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: docPath, + WithEngineOptions( + WithOpenAPIConfig( + OpenAPIConfig{ + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: docPath, + }, }, - }, + ), ), ) Get(s, "/", func(ContextNoBody) (MyStruct, error) { @@ -336,13 +338,15 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { }) t.Run("do not print file", func(t *testing.T) { s := NewServer( - WithOpenAPIConfig( - OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: docPath, - DisableLocalSave: true, + WithEngineOptions( + WithOpenAPIConfig( + OpenAPIConfig{ + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: docPath, + DisableLocalSave: true, + }, }, - }, + ), ), ) Get(s, "/", func(ContextNoBody) (MyStruct, error) { @@ -358,14 +362,16 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { }) t.Run("swagger disabled", func(t *testing.T) { s := NewServer( - WithOpenAPIConfig( - OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: docPath, - DisableLocalSave: true, + WithEngineOptions( + WithOpenAPIConfig( + OpenAPIConfig{ + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: docPath, + DisableLocalSave: true, + }, + DisableSwagger: true, }, - DisableSwagger: true, - }, + ), ), ) Get(s, "/", func(ContextNoBody) (MyStruct, error) { @@ -382,13 +388,15 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { }) t.Run("pretty format json file", func(t *testing.T) { s := NewServer( - WithOpenAPIConfig( - OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: docPath, - PrettyFormatJson: true, + WithEngineOptions( + WithOpenAPIConfig( + OpenAPIConfig{ + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: docPath, + PrettyFormatJson: true, + }, }, - }, + ), ), ) Get(s, "/", func(ContextNoBody) (MyStruct, error) { @@ -501,12 +509,14 @@ func TestLocalSave(t *testing.T) { func TestAutoGroupTags(t *testing.T) { s := NewServer( - WithOpenAPIConfig(OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - DisableLocalSave: true, - }, - DisableSwagger: true, - }), + WithEngineOptions( + WithOpenAPIConfig(OpenAPIConfig{ + EngineOpenAPIConfig: EngineOpenAPIConfig{ + DisableLocalSave: true, + }, + DisableSwagger: true, + }), + ), ) Get(s, "/a", func(ContextNoBody) (MyStruct, error) { return MyStruct{}, nil diff --git a/server.go b/server.go index e694323c..a0389086 100644 --- a/server.go +++ b/server.go @@ -375,9 +375,11 @@ func WithoutLogger() func(*Server) { } } -func WithOpenAPIConfig(openapiConfig OpenAPIConfig) func(*Server) { +func WithEngineOptions(options ...func(*Engine)) func(*Server) { return func(s *Server) { - s.Engine.setOpenAPIConfig(openapiConfig) + for _, option := range options { + option(s.Engine) + } } } diff --git a/server_test.go b/server_test.go index 43b1eda1..f5ecba01 100644 --- a/server_test.go +++ b/server_test.go @@ -74,7 +74,9 @@ func TestWithXML(t *testing.T) { func TestWithOpenAPIConfig(t *testing.T) { t.Run("with default values", func(t *testing.T) { s := NewServer( - WithOpenAPIConfig(OpenAPIConfig{}), + WithEngineOptions( + WithOpenAPIConfig(OpenAPIConfig{}), + ), ) require.Equal(t, "/swagger", s.OpenAPIConfig.SwaggerUrl) @@ -85,16 +87,18 @@ func TestWithOpenAPIConfig(t *testing.T) { t.Run("with custom values", func(t *testing.T) { s := NewServer( - WithOpenAPIConfig(OpenAPIConfig{ - SwaggerUrl: "/api", - JsonUrl: "/api/openapi.json", - DisableSwagger: true, - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: "openapi.json", - DisableLocalSave: true, - PrettyFormatJson: true, - }, - }), + WithEngineOptions( + WithOpenAPIConfig(OpenAPIConfig{ + SwaggerUrl: "/api", + JsonUrl: "/api/openapi.json", + DisableSwagger: true, + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: "openapi.json", + DisableLocalSave: true, + PrettyFormatJson: true, + }, + }), + ), ) require.Equal(t, "/api", s.OpenAPIConfig.SwaggerUrl) @@ -108,36 +112,42 @@ func TestWithOpenAPIConfig(t *testing.T) { t.Run("with invalid local path values", func(t *testing.T) { t.Run("with invalid path", func(t *testing.T) { NewServer( - WithOpenAPIConfig(OpenAPIConfig{ - SwaggerUrl: "p i", - JsonUrl: "pi/op enapi.json", - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: "path/to/jsonSpec", - }, - }), + WithEngineOptions( + WithOpenAPIConfig(OpenAPIConfig{ + SwaggerUrl: "p i", + JsonUrl: "pi/op enapi.json", + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: "path/to/jsonSpec", + }, + }), + ), ) }) t.Run("with invalid url", func(t *testing.T) { NewServer( - WithOpenAPIConfig(OpenAPIConfig{ - JsonUrl: "pi/op enapi.json", - SwaggerUrl: "p i", - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: "path/to/jsonSpec.json", - }, - }), + WithEngineOptions( + WithOpenAPIConfig(OpenAPIConfig{ + JsonUrl: "pi/op enapi.json", + SwaggerUrl: "p i", + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: "path/to/jsonSpec.json", + }, + }), + ), ) }) t.Run("with invalid url", func(t *testing.T) { NewServer( - WithOpenAPIConfig(OpenAPIConfig{ - JsonUrl: "/api/openapi.json", - SwaggerUrl: "invalid path", - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: "path/to/jsonSpec.json", - }, - }), + WithEngineOptions( + WithOpenAPIConfig(OpenAPIConfig{ + JsonUrl: "/api/openapi.json", + SwaggerUrl: "invalid path", + EngineOpenAPIConfig: EngineOpenAPIConfig{ + JsonFilePath: "path/to/jsonSpec.json", + }, + }), + ), ) }) }) From be62197c64bc90192f1f8d58a120fa8326c833de Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Mon, 23 Dec 2024 21:46:11 -0500 Subject: [PATCH 03/12] chore: move Server (net/http) over to using Registers --- openapi.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openapi.go b/openapi.go index c622bc93..8b1360dd 100644 --- a/openapi.go +++ b/openapi.go @@ -156,12 +156,16 @@ func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s%s", s.url(), s.OpenAPIConfig.JsonUrl)) if !s.OpenAPIConfig.DisableSwaggerUI { - Register(s, Route[any, any]{ - BaseRoute: BaseRoute{ - Method: http.MethodGet, - Path: s.OpenAPIConfig.SwaggerUrl + "/", + Registers(s.Engine, netHttpRouteRegisterer[any, any]{ + s: s, + route: Route[any, any]{ + BaseRoute: BaseRoute{ + Method: http.MethodGet, + Path: s.OpenAPIConfig.SwaggerUrl + "/", + }, }, - }, s.OpenAPIConfig.UIHandler(s.OpenAPIConfig.JsonUrl)) + controller: s.OpenAPIConfig.UIHandler(s.OpenAPIConfig.JsonUrl), + }) s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: %s%s/index.html", s.url(), s.OpenAPIConfig.SwaggerUrl)) } } From df7a80d9202310d3891555aa16a984e2712ffe95 Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Mon, 23 Dec 2024 23:00:59 -0500 Subject: [PATCH 04/12] chore: json -> JSON & url -> URL Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com> --- engine.go | 32 ++++++++--------- examples/generate-opengraph-image/main.go | 2 +- examples/petstore/lib/server_test.go | 4 +-- openapi.go | 10 +++--- openapi_test.go | 10 +++--- server.go | 10 +++--- server_test.go | 42 +++++++++++------------ 7 files changed, 55 insertions(+), 55 deletions(-) diff --git a/engine.go b/engine.go index 410b883c..b53c9104 100644 --- a/engine.go +++ b/engine.go @@ -36,23 +36,23 @@ type EngineOpenAPIConfig struct { // If true, the engine will not save the OpenAPI JSON spec locally DisableLocalSave bool // Local path to save the OpenAPI JSON spec - JsonFilePath string + JSONFilePath string // Pretty prints the OpenAPI spec with proper JSON indentation - PrettyFormatJson bool + PrettyFormatJSON bool } func WithOpenAPIConfig(config OpenAPIConfig) func(*Engine) { return func(e *Engine) { - if config.JsonUrl != "" { - e.OpenAPIConfig.JsonUrl = config.JsonUrl + if config.JsonURL != "" { + e.OpenAPIConfig.JsonURL = config.JsonURL } - if config.SwaggerUrl != "" { - e.OpenAPIConfig.SwaggerUrl = config.SwaggerUrl + if config.SwaggerURL != "" { + e.OpenAPIConfig.SwaggerURL = config.SwaggerURL } - if config.JsonFilePath != "" { - e.OpenAPIConfig.JsonFilePath = config.JsonFilePath + if config.JSONFilePath != "" { + e.OpenAPIConfig.JSONFilePath = config.JSONFilePath } if config.UIHandler != nil { @@ -62,15 +62,15 @@ func WithOpenAPIConfig(config OpenAPIConfig) func(*Engine) { e.OpenAPIConfig.DisableSwagger = config.DisableSwagger e.OpenAPIConfig.DisableSwaggerUI = config.DisableSwaggerUI e.OpenAPIConfig.DisableLocalSave = config.DisableLocalSave - e.OpenAPIConfig.PrettyFormatJson = config.PrettyFormatJson + e.OpenAPIConfig.PrettyFormatJSON = config.PrettyFormatJSON - if !validateJsonSpecUrl(e.OpenAPIConfig.JsonUrl) { - slog.Error("Error serving openapi json spec. Value of 's.OpenAPIConfig.JsonSpecUrl' option is not valid", "url", e.OpenAPIConfig.JsonUrl) + if !validateJsonSpecUrl(e.OpenAPIConfig.JsonURL) { + slog.Error("Error serving openapi json spec. Value of 's.OpenAPIConfig.JsonSpecUrl' option is not valid", "url", e.OpenAPIConfig.JsonURL) return } - if !validateSwaggerUrl(e.OpenAPIConfig.SwaggerUrl) { - slog.Error("Error serving swagger ui. Value of 's.OpenAPIConfig.SwaggerUrl' option is not valid", "url", e.OpenAPIConfig.SwaggerUrl) + if !validateSwaggerUrl(e.OpenAPIConfig.SwaggerURL) { + slog.Error("Error serving swagger ui. Value of 's.OpenAPIConfig.SwaggerUrl' option is not valid", "url", e.OpenAPIConfig.SwaggerURL) return } } @@ -93,9 +93,9 @@ func (e *Engine) OutputOpenAPISpec() []byte { } if !e.OpenAPIConfig.DisableLocalSave { - err := e.saveOpenAPIToFile(e.OpenAPIConfig.JsonFilePath, jsonSpec) + err := e.saveOpenAPIToFile(e.OpenAPIConfig.JSONFilePath, jsonSpec) if err != nil { - slog.Error("Error saving spec to local path", "error", err, "path", e.OpenAPIConfig.JsonFilePath) + slog.Error("Error saving spec to local path", "error", err, "path", e.OpenAPIConfig.JSONFilePath) } } return jsonSpec @@ -125,7 +125,7 @@ func (e *Engine) saveOpenAPIToFile(jsonSpecLocalPath string, jsonSpec []byte) er } func (s *Engine) marshalSpec() ([]byte, error) { - if s.OpenAPIConfig.PrettyFormatJson { + if s.OpenAPIConfig.PrettyFormatJSON { return json.MarshalIndent(s.OpenAPI.Description(), "", " ") } return json.Marshal(s.OpenAPI.Description()) diff --git a/examples/generate-opengraph-image/main.go b/examples/generate-opengraph-image/main.go index 5c7f0637..c07857e4 100644 --- a/examples/generate-opengraph-image/main.go +++ b/examples/generate-opengraph-image/main.go @@ -24,7 +24,7 @@ func main() { fuego.WithEngineOptions( fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ - PrettyFormatJson: true, + PrettyFormatJSON: true, }, }), ), diff --git a/examples/petstore/lib/server_test.go b/examples/petstore/lib/server_test.go index cc9cbbdb..0ea49523 100644 --- a/examples/petstore/lib/server_test.go +++ b/examples/petstore/lib/server_test.go @@ -17,8 +17,8 @@ func TestPetstoreOpenAPIGeneration(t *testing.T) { fuego.WithEngineOptions( fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ - JsonFilePath: "testdata/doc/openapi.json", - PrettyFormatJson: true, + JSONFilePath: "testdata/doc/openapi.json", + PrettyFormatJSON: true, }, }), ), diff --git a/openapi.go b/openapi.go index 8b1360dd..973e5e78 100644 --- a/openapi.go +++ b/openapi.go @@ -149,11 +149,11 @@ func (s *Server) saveOpenAPIToFile(jsonSpecLocalPath string, jsonSpec []byte) er // Registers the routes to serve the OpenAPI spec and Swagger UI. func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { - GetStd(s, s.OpenAPIConfig.JsonUrl, func(w http.ResponseWriter, r *http.Request) { + GetStd(s, s.OpenAPIConfig.JsonURL, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(jsonSpec) }) - s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s%s", s.url(), s.OpenAPIConfig.JsonUrl)) + s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s%s", s.url(), s.OpenAPIConfig.JsonURL)) if !s.OpenAPIConfig.DisableSwaggerUI { Registers(s.Engine, netHttpRouteRegisterer[any, any]{ @@ -161,12 +161,12 @@ func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { route: Route[any, any]{ BaseRoute: BaseRoute{ Method: http.MethodGet, - Path: s.OpenAPIConfig.SwaggerUrl + "/", + Path: s.OpenAPIConfig.SwaggerURL + "/", }, }, - controller: s.OpenAPIConfig.UIHandler(s.OpenAPIConfig.JsonUrl), + controller: s.OpenAPIConfig.UIHandler(s.OpenAPIConfig.JsonURL), }) - s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: %s%s/index.html", s.url(), s.OpenAPIConfig.SwaggerUrl)) + s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: %s%s/index.html", s.url(), s.OpenAPIConfig.SwaggerURL)) } } diff --git a/openapi_test.go b/openapi_test.go index 02f6a4ba..80efd652 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -317,7 +317,7 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { WithOpenAPIConfig( OpenAPIConfig{ EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: docPath, + JSONFilePath: docPath, }, }, ), @@ -342,7 +342,7 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { WithOpenAPIConfig( OpenAPIConfig{ EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: docPath, + JSONFilePath: docPath, DisableLocalSave: true, }, }, @@ -366,7 +366,7 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { WithOpenAPIConfig( OpenAPIConfig{ EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: docPath, + JSONFilePath: docPath, DisableLocalSave: true, }, DisableSwagger: true, @@ -392,8 +392,8 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { WithOpenAPIConfig( OpenAPIConfig{ EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: docPath, - PrettyFormatJson: true, + JSONFilePath: docPath, + PrettyFormatJSON: true, }, }, ), diff --git a/server.go b/server.go index a0389086..e909e3fe 100644 --- a/server.go +++ b/server.go @@ -21,19 +21,19 @@ type OpenAPIConfig struct { // If true, the server will not serve the Swagger UI DisableSwaggerUI bool // URL to serve the swagger UI - SwaggerUrl string + SwaggerURL string // Handler to serve the OpenAPI UI from spec URL UIHandler func(specURL string) http.Handler // URL to serve the OpenAPI JSON spec - JsonUrl string + JsonURL string } var defaultOpenAPIConfig = OpenAPIConfig{ - SwaggerUrl: "/swagger", - JsonUrl: "/swagger/openapi.json", + SwaggerURL: "/swagger", + JsonURL: "/swagger/openapi.json", UIHandler: DefaultOpenAPIHandler, EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: "doc/openapi.json", + JSONFilePath: "doc/openapi.json", }, } diff --git a/server_test.go b/server_test.go index f5ecba01..738ed5ea 100644 --- a/server_test.go +++ b/server_test.go @@ -79,34 +79,34 @@ func TestWithOpenAPIConfig(t *testing.T) { ), ) - require.Equal(t, "/swagger", s.OpenAPIConfig.SwaggerUrl) - require.Equal(t, "/swagger/openapi.json", s.OpenAPIConfig.JsonUrl) - require.Equal(t, "doc/openapi.json", s.OpenAPIConfig.JsonFilePath) - require.False(t, s.OpenAPIConfig.PrettyFormatJson) + require.Equal(t, "/swagger", s.OpenAPIConfig.SwaggerURL) + require.Equal(t, "/swagger/openapi.json", s.OpenAPIConfig.JsonURL) + require.Equal(t, "doc/openapi.json", s.OpenAPIConfig.JSONFilePath) + require.False(t, s.OpenAPIConfig.PrettyFormatJSON) }) t.Run("with custom values", func(t *testing.T) { s := NewServer( WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ - SwaggerUrl: "/api", - JsonUrl: "/api/openapi.json", + SwaggerURL: "/api", + JsonURL: "/api/openapi.json", DisableSwagger: true, EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: "openapi.json", + JSONFilePath: "openapi.json", DisableLocalSave: true, - PrettyFormatJson: true, + PrettyFormatJSON: true, }, }), ), ) - require.Equal(t, "/api", s.OpenAPIConfig.SwaggerUrl) - require.Equal(t, "/api/openapi.json", s.OpenAPIConfig.JsonUrl) - require.Equal(t, "openapi.json", s.OpenAPIConfig.JsonFilePath) + require.Equal(t, "/api", s.OpenAPIConfig.SwaggerURL) + require.Equal(t, "/api/openapi.json", s.OpenAPIConfig.JsonURL) + require.Equal(t, "openapi.json", s.OpenAPIConfig.JSONFilePath) require.True(t, s.OpenAPIConfig.DisableSwagger) require.True(t, s.OpenAPIConfig.DisableLocalSave) - require.True(t, s.OpenAPIConfig.PrettyFormatJson) + require.True(t, s.OpenAPIConfig.PrettyFormatJSON) }) t.Run("with invalid local path values", func(t *testing.T) { @@ -114,10 +114,10 @@ func TestWithOpenAPIConfig(t *testing.T) { NewServer( WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ - SwaggerUrl: "p i", - JsonUrl: "pi/op enapi.json", + SwaggerURL: "p i", + JsonURL: "pi/op enapi.json", EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: "path/to/jsonSpec", + JSONFilePath: "path/to/jsonSpec", }, }), ), @@ -127,10 +127,10 @@ func TestWithOpenAPIConfig(t *testing.T) { NewServer( WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ - JsonUrl: "pi/op enapi.json", - SwaggerUrl: "p i", + JsonURL: "pi/op enapi.json", + SwaggerURL: "p i", EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: "path/to/jsonSpec.json", + JSONFilePath: "path/to/jsonSpec.json", }, }), ), @@ -141,10 +141,10 @@ func TestWithOpenAPIConfig(t *testing.T) { NewServer( WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ - JsonUrl: "/api/openapi.json", - SwaggerUrl: "invalid path", + JsonURL: "/api/openapi.json", + SwaggerURL: "invalid path", EngineOpenAPIConfig: EngineOpenAPIConfig{ - JsonFilePath: "path/to/jsonSpec.json", + JSONFilePath: "path/to/jsonSpec.json", }, }), ), From 2e9c9fb9afea29d1f2ff9bf37ec2f8f0dd33c3de Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Tue, 24 Dec 2024 08:52:26 -0500 Subject: [PATCH 05/12] chore: rebase.. --- openapi.go | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/openapi.go b/openapi.go index 973e5e78..25ea11fe 100644 --- a/openapi.go +++ b/openapi.go @@ -1,12 +1,9 @@ package fuego import ( - "encoding/json" "fmt" "log/slog" "net/http" - "os" - "path/filepath" "reflect" "regexp" "slices" @@ -117,36 +114,6 @@ func (s *Server) OutputOpenAPISpec() openapi3.T { return *s.OpenAPI.Description() } -func (s *Server) marshalSpec() ([]byte, error) { - if s.OpenAPIConfig.PrettyFormatJson { - return json.MarshalIndent(s.OpenAPI.Description(), "", "\t") - } - return json.Marshal(s.OpenAPI.Description()) -} - -func (s *Server) saveOpenAPIToFile(jsonSpecLocalPath string, jsonSpec []byte) error { - jsonFolder := filepath.Dir(jsonSpecLocalPath) - - err := os.MkdirAll(jsonFolder, 0o750) - if err != nil { - return fmt.Errorf("error creating docs directory: %w", err) - } - - f, err := os.Create(jsonSpecLocalPath) // #nosec G304 (file path provided by developer, not by user) - if err != nil { - return fmt.Errorf("error creating file: %w", err) - } - defer f.Close() - - _, err = f.Write(jsonSpec) - if err != nil { - return fmt.Errorf("error writing file: %w", err) - } - - s.printOpenAPIMessage("JSON file: " + jsonSpecLocalPath) - return nil -} - // Registers the routes to serve the OpenAPI spec and Swagger UI. func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { GetStd(s, s.OpenAPIConfig.JsonURL, func(w http.ResponseWriter, r *http.Request) { From eef24b85df98125404bfc597b60ff0832a3a6455 Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Wed, 25 Dec 2024 00:29:52 -0500 Subject: [PATCH 06/12] BREAKING: push OpenAPIConfig and it's pertinent fields into engine Also create OpenAPIServerConfig for to hold properties pertaining to the UI serrving of the fuego net/http server. --- engine.go | 33 +++---------- examples/generate-opengraph-image/main.go | 4 +- examples/petstore/lib/server_test.go | 6 +-- openapi.go | 34 +++++++------ openapi_handler_test.go | 24 ++++----- openapi_test.go | 36 +++++--------- server.go | 38 +++++++++++--- server_test.go | 60 +++++++++++------------ 8 files changed, 110 insertions(+), 125 deletions(-) diff --git a/engine.go b/engine.go index b53c9104..725e326a 100644 --- a/engine.go +++ b/engine.go @@ -30,7 +30,9 @@ type Engine struct { acceptedContentTypes []string } -type EngineOpenAPIConfig struct { +type OpenAPIConfig struct { + // If true, the server will not serve nor generate any OpenAPI resources + Disabled bool // If true, the engine will not print messages DisableMessages bool // If true, the engine will not save the OpenAPI JSON spec locally @@ -41,38 +43,19 @@ type EngineOpenAPIConfig struct { PrettyFormatJSON bool } +var defaultOpenAPIConfig = OpenAPIConfig{ + JSONFilePath: "doc/openapi.json", +} + func WithOpenAPIConfig(config OpenAPIConfig) func(*Engine) { return func(e *Engine) { - if config.JsonURL != "" { - e.OpenAPIConfig.JsonURL = config.JsonURL - } - - if config.SwaggerURL != "" { - e.OpenAPIConfig.SwaggerURL = config.SwaggerURL - } - if config.JSONFilePath != "" { e.OpenAPIConfig.JSONFilePath = config.JSONFilePath } - if config.UIHandler != nil { - e.OpenAPIConfig.UIHandler = config.UIHandler - } - - e.OpenAPIConfig.DisableSwagger = config.DisableSwagger - e.OpenAPIConfig.DisableSwaggerUI = config.DisableSwaggerUI + e.OpenAPIConfig.Disabled = config.Disabled e.OpenAPIConfig.DisableLocalSave = config.DisableLocalSave e.OpenAPIConfig.PrettyFormatJSON = config.PrettyFormatJSON - - if !validateJsonSpecUrl(e.OpenAPIConfig.JsonURL) { - slog.Error("Error serving openapi json spec. Value of 's.OpenAPIConfig.JsonSpecUrl' option is not valid", "url", e.OpenAPIConfig.JsonURL) - return - } - - if !validateSwaggerUrl(e.OpenAPIConfig.SwaggerURL) { - slog.Error("Error serving swagger ui. Value of 's.OpenAPIConfig.SwaggerUrl' option is not valid", "url", e.OpenAPIConfig.SwaggerURL) - return - } } } diff --git a/examples/generate-opengraph-image/main.go b/examples/generate-opengraph-image/main.go index c07857e4..0679b4e5 100644 --- a/examples/generate-opengraph-image/main.go +++ b/examples/generate-opengraph-image/main.go @@ -23,9 +23,7 @@ func main() { s := fuego.NewServer( fuego.WithEngineOptions( fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ - EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ - PrettyFormatJSON: true, - }, + PrettyFormatJSON: true, }), ), ) diff --git a/examples/petstore/lib/server_test.go b/examples/petstore/lib/server_test.go index 0ea49523..699e7377 100644 --- a/examples/petstore/lib/server_test.go +++ b/examples/petstore/lib/server_test.go @@ -16,10 +16,8 @@ func TestPetstoreOpenAPIGeneration(t *testing.T) { fuego.WithoutStartupMessages(), fuego.WithEngineOptions( fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ - EngineOpenAPIConfig: fuego.EngineOpenAPIConfig{ - JSONFilePath: "testdata/doc/openapi.json", - PrettyFormatJSON: true, - }, + JSONFilePath: "testdata/doc/openapi.json", + PrettyFormatJSON: true, }), ), ) diff --git a/openapi.go b/openapi.go index 25ea11fe..a545d333 100644 --- a/openapi.go +++ b/openapi.go @@ -107,7 +107,7 @@ func (s *Server) OutputOpenAPISpec() openapi3.T { Description: "local server", }) - if !s.OpenAPIConfig.DisableSwagger { + if !s.OpenAPIConfig.Disabled { s.registerOpenAPIRoutes(s.Engine.OutputOpenAPISpec()) } @@ -116,25 +116,27 @@ func (s *Server) OutputOpenAPISpec() openapi3.T { // Registers the routes to serve the OpenAPI spec and Swagger UI. func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { - GetStd(s, s.OpenAPIConfig.JsonURL, func(w http.ResponseWriter, r *http.Request) { + GetStd(s, s.JsonURL, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(jsonSpec) }) - s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s%s", s.url(), s.OpenAPIConfig.JsonURL)) - - if !s.OpenAPIConfig.DisableSwaggerUI { - Registers(s.Engine, netHttpRouteRegisterer[any, any]{ - s: s, - route: Route[any, any]{ - BaseRoute: BaseRoute{ - Method: http.MethodGet, - Path: s.OpenAPIConfig.SwaggerURL + "/", - }, - }, - controller: s.OpenAPIConfig.UIHandler(s.OpenAPIConfig.JsonURL), - }) - s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: %s%s/index.html", s.url(), s.OpenAPIConfig.SwaggerURL)) + s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s%s", s.url(), s.JsonURL)) + + if s.DisableSwaggerUI { + return } + Registers(s.Engine, netHttpRouteRegisterer[any, any]{ + s: s, + route: Route[any, any]{ + BaseRoute: BaseRoute{ + Method: http.MethodGet, + Path: s.SwaggerURL + "/", + }, + }, + controller: s.UIHandler(s.JsonURL), + }) + s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: %s%s/index.html", s.url(), s.SwaggerURL)) + } func validateJsonSpecUrl(jsonSpecUrl string) bool { diff --git a/openapi_handler_test.go b/openapi_handler_test.go index 0b031217..3764d6e7 100644 --- a/openapi_handler_test.go +++ b/openapi_handler_test.go @@ -23,7 +23,7 @@ func TestUIHandler(t *testing.T) { s.OutputOpenAPISpec() - require.NotNil(t, s.OpenAPIConfig.UIHandler) + require.NotNil(t, s.UIHandler) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/swagger/index.html", nil) @@ -37,17 +37,15 @@ func TestUIHandler(t *testing.T) { t.Run("wrap DefaultOpenAPIHandler behind a middleware", func(t *testing.T) { s := NewServer( - WithEngineOptions( - WithOpenAPIConfig(OpenAPIConfig{ - UIHandler: func(specURL string) http.Handler { - return dummyMiddleware(DefaultOpenAPIHandler(specURL)) - }, - }), - ), + WithOpenAPIServerConfig(OpenAPIServerConfig{ + UIHandler: func(specURL string) http.Handler { + return dummyMiddleware(DefaultOpenAPIHandler(specURL)) + }, + }), ) s.OutputOpenAPISpec() - require.NotNil(t, s.OpenAPIConfig.UIHandler) + require.NotNil(t, s.UIHandler) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/swagger/index.html", nil) @@ -61,11 +59,9 @@ func TestUIHandler(t *testing.T) { t.Run("disabling UI", func(t *testing.T) { s := NewServer( - WithEngineOptions( - WithOpenAPIConfig(OpenAPIConfig{ - DisableSwaggerUI: true, - }), - ), + WithOpenAPIServerConfig(OpenAPIServerConfig{ + DisableSwaggerUI: true, + }), ) s.OutputOpenAPISpec() diff --git a/openapi_test.go b/openapi_test.go index 80efd652..7eb2458e 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -316,9 +316,7 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { WithEngineOptions( WithOpenAPIConfig( OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JSONFilePath: docPath, - }, + JSONFilePath: docPath, }, ), ), @@ -339,14 +337,10 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { t.Run("do not print file", func(t *testing.T) { s := NewServer( WithEngineOptions( - WithOpenAPIConfig( - OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JSONFilePath: docPath, - DisableLocalSave: true, - }, - }, - ), + WithOpenAPIConfig(OpenAPIConfig{ + JSONFilePath: docPath, + DisableLocalSave: true, + }), ), ) Get(s, "/", func(ContextNoBody) (MyStruct, error) { @@ -365,11 +359,9 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { WithEngineOptions( WithOpenAPIConfig( OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JSONFilePath: docPath, - DisableLocalSave: true, - }, - DisableSwagger: true, + JSONFilePath: docPath, + DisableLocalSave: true, + Disabled: true, }, ), ), @@ -391,10 +383,8 @@ func TestServer_OutputOpenApiSpec(t *testing.T) { WithEngineOptions( WithOpenAPIConfig( OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JSONFilePath: docPath, - PrettyFormatJSON: true, - }, + JSONFilePath: docPath, + PrettyFormatJSON: true, }, ), ), @@ -511,10 +501,8 @@ func TestAutoGroupTags(t *testing.T) { s := NewServer( WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ - EngineOpenAPIConfig: EngineOpenAPIConfig{ - DisableLocalSave: true, - }, - DisableSwagger: true, + DisableLocalSave: true, + Disabled: true, }), ), ) diff --git a/server.go b/server.go index e909e3fe..9343d12e 100644 --- a/server.go +++ b/server.go @@ -14,10 +14,7 @@ import ( "github.com/golang-jwt/jwt/v5" ) -type OpenAPIConfig struct { - EngineOpenAPIConfig - // If true, the server will not serve the Swagger UI nor the OpenAPI JSON spec - DisableSwagger bool +type OpenAPIServerConfig struct { // If true, the server will not serve the Swagger UI DisableSwaggerUI bool // URL to serve the swagger UI @@ -28,13 +25,10 @@ type OpenAPIConfig struct { JsonURL string } -var defaultOpenAPIConfig = OpenAPIConfig{ +var defaultOpenAPIServerConfig = OpenAPIServerConfig{ SwaggerURL: "/swagger", JsonURL: "/swagger/openapi.json", UIHandler: DefaultOpenAPIHandler, - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JSONFilePath: "doc/openapi.json", - }, } type Server struct { @@ -108,6 +102,8 @@ func NewServer(options ...func(*Server)) *Server { Mux: http.NewServeMux(), Engine: NewEngine(), + OpenAPIServerConfig: defaultOpenAPIServerConfig, + Security: NewSecurity(), loggingConfig: defaultLoggingConfig, @@ -375,6 +371,32 @@ func WithoutLogger() func(*Server) { } } +func WithOpenAPIServerConfig(config OpenAPIServerConfig) func(*Server) { + return func(s *Server) { + if config.JsonURL != "" { + s.OpenAPIServerConfig.JsonURL = config.JsonURL + } + if config.SwaggerURL != "" { + s.OpenAPIServerConfig.SwaggerURL = config.SwaggerURL + } + if config.UIHandler != nil { + s.OpenAPIServerConfig.UIHandler = config.UIHandler + } + + s.OpenAPIServerConfig.DisableSwaggerUI = config.DisableSwaggerUI + + if !validateJsonSpecUrl(s.OpenAPIServerConfig.JsonURL) { + slog.Error("Error serving openapi json spec. Value of 's.OpenAPIServerConfig.JsonURL' option is not valid", "url", s.OpenAPIServerConfig.JsonURL) + return + } + + if !validateSwaggerUrl(s.OpenAPIServerConfig.SwaggerURL) { + slog.Error("Error serving swagger ui. Value of 's.OpenAPIServerConfig.SwaggerURL' option is not valid", "url", s.OpenAPIServerConfig.SwaggerURL) + return + } + } +} + func WithEngineOptions(options ...func(*Engine)) func(*Server) { return func(s *Server) { for _, option := range options { diff --git a/server_test.go b/server_test.go index 738ed5ea..b6002972 100644 --- a/server_test.go +++ b/server_test.go @@ -74,37 +74,35 @@ func TestWithXML(t *testing.T) { func TestWithOpenAPIConfig(t *testing.T) { t.Run("with default values", func(t *testing.T) { s := NewServer( - WithEngineOptions( - WithOpenAPIConfig(OpenAPIConfig{}), - ), + WithOpenAPIServerConfig(OpenAPIServerConfig{}), ) - require.Equal(t, "/swagger", s.OpenAPIConfig.SwaggerURL) - require.Equal(t, "/swagger/openapi.json", s.OpenAPIConfig.JsonURL) + require.Equal(t, "/swagger", s.SwaggerURL) + require.Equal(t, "/swagger/openapi.json", s.JsonURL) require.Equal(t, "doc/openapi.json", s.OpenAPIConfig.JSONFilePath) require.False(t, s.OpenAPIConfig.PrettyFormatJSON) }) t.Run("with custom values", func(t *testing.T) { s := NewServer( + WithOpenAPIServerConfig(OpenAPIServerConfig{ + SwaggerURL: "/api", + JsonURL: "/api/openapi.json", + }), WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ - SwaggerURL: "/api", - JsonURL: "/api/openapi.json", - DisableSwagger: true, - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JSONFilePath: "openapi.json", - DisableLocalSave: true, - PrettyFormatJSON: true, - }, + JSONFilePath: "openapi.json", + DisableLocalSave: true, + PrettyFormatJSON: true, + Disabled: true, }), ), ) - require.Equal(t, "/api", s.OpenAPIConfig.SwaggerURL) - require.Equal(t, "/api/openapi.json", s.OpenAPIConfig.JsonURL) + require.Equal(t, "/api", s.SwaggerURL) + require.Equal(t, "/api/openapi.json", s.JsonURL) require.Equal(t, "openapi.json", s.OpenAPIConfig.JSONFilePath) - require.True(t, s.OpenAPIConfig.DisableSwagger) + require.True(t, s.Engine.OpenAPIConfig.Disabled) require.True(t, s.OpenAPIConfig.DisableLocalSave) require.True(t, s.OpenAPIConfig.PrettyFormatJSON) }) @@ -112,26 +110,26 @@ func TestWithOpenAPIConfig(t *testing.T) { t.Run("with invalid local path values", func(t *testing.T) { t.Run("with invalid path", func(t *testing.T) { NewServer( + WithOpenAPIServerConfig(OpenAPIServerConfig{ + SwaggerURL: "p i", + JsonURL: "pi/op enapi.json", + }), WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ - SwaggerURL: "p i", - JsonURL: "pi/op enapi.json", - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JSONFilePath: "path/to/jsonSpec", - }, + JSONFilePath: "path/to/jsonSpec", }), ), ) }) t.Run("with invalid url", func(t *testing.T) { NewServer( + WithOpenAPIServerConfig(OpenAPIServerConfig{ + JsonURL: "pi/op enapi.json", + SwaggerURL: "p i", + }), WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ - JsonURL: "pi/op enapi.json", - SwaggerURL: "p i", - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JSONFilePath: "path/to/jsonSpec.json", - }, + JSONFilePath: "path/to/jsonSpec.json", }), ), ) @@ -139,13 +137,13 @@ func TestWithOpenAPIConfig(t *testing.T) { t.Run("with invalid url", func(t *testing.T) { NewServer( + WithOpenAPIServerConfig(OpenAPIServerConfig{ + JsonURL: "/api/openapi.json", + SwaggerURL: "invalid path", + }), WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ - JsonURL: "/api/openapi.json", - SwaggerURL: "invalid path", - EngineOpenAPIConfig: EngineOpenAPIConfig{ - JSONFilePath: "path/to/jsonSpec.json", - }, + JSONFilePath: "path/to/jsonSpec.json", }), ), ) From 16c1628316b8636749699a77b5b23014b15a242e Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Wed, 25 Dec 2024 00:41:20 -0500 Subject: [PATCH 07/12] chore: Url -> URL in Spec and SwaggerURL properties Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com> --- engine.go | 2 +- openapi.go | 19 +++++++++---------- openapi_test.go | 34 +++++++++++++++++----------------- server.go | 14 +++++++------- server_test.go | 12 ++++++------ 5 files changed, 40 insertions(+), 41 deletions(-) diff --git a/engine.go b/engine.go index 725e326a..141759c8 100644 --- a/engine.go +++ b/engine.go @@ -109,7 +109,7 @@ func (e *Engine) saveOpenAPIToFile(jsonSpecLocalPath string, jsonSpec []byte) er func (s *Engine) marshalSpec() ([]byte, error) { if s.OpenAPIConfig.PrettyFormatJSON { - return json.MarshalIndent(s.OpenAPI.Description(), "", " ") + return json.MarshalIndent(s.OpenAPI.Description(), "", "\t") } return json.Marshal(s.OpenAPI.Description()) } diff --git a/openapi.go b/openapi.go index a545d333..99c084ed 100644 --- a/openapi.go +++ b/openapi.go @@ -116,11 +116,11 @@ func (s *Server) OutputOpenAPISpec() openapi3.T { // Registers the routes to serve the OpenAPI spec and Swagger UI. func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { - GetStd(s, s.JsonURL, func(w http.ResponseWriter, r *http.Request) { + GetStd(s, s.SpecURL, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(jsonSpec) }) - s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s%s", s.url(), s.JsonURL)) + s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s%s", s.url(), s.SpecURL)) if s.DisableSwaggerUI { return @@ -133,20 +133,19 @@ func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { Path: s.SwaggerURL + "/", }, }, - controller: s.UIHandler(s.JsonURL), + controller: s.UIHandler(s.SpecURL), }) s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: %s%s/index.html", s.url(), s.SwaggerURL)) - } -func validateJsonSpecUrl(jsonSpecUrl string) bool { - jsonSpecUrlRegexp := regexp.MustCompile(`^\/[\/a-zA-Z0-9\-\_]+(.json)$`) - return jsonSpecUrlRegexp.MatchString(jsonSpecUrl) +func validateSpecURL(specURL string) bool { + specURLRegexp := regexp.MustCompile(`^\/[\/a-zA-Z0-9\-\_]+(.json)$`) + return specURLRegexp.MatchString(specURL) } -func validateSwaggerUrl(swaggerUrl string) bool { - swaggerUrlRegexp := regexp.MustCompile(`^\/[\/a-zA-Z0-9\-\_]+[a-zA-Z0-9\-\_]$`) - return swaggerUrlRegexp.MatchString(swaggerUrl) +func validateSwaggerURL(swaggerURL string) bool { + swaggerURLRegexp := regexp.MustCompile(`^\/[\/a-zA-Z0-9\-\_]+[a-zA-Z0-9\-\_]$`) + return swaggerURLRegexp.MatchString(swaggerURL) } // RegisterOpenAPIOperation registers the route to the OpenAPI description. diff --git a/openapi_test.go b/openapi_test.go index 7eb2458e..9cbe7e60 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -459,26 +459,26 @@ func BenchmarkServer_generateOpenAPI(b *testing.B) { } } -func TestValidateJsonSpecUrl(t *testing.T) { - require.Equal(t, true, validateJsonSpecUrl("/path/to/jsonSpec.json")) - require.Equal(t, true, validateJsonSpecUrl("/spec.json")) - require.Equal(t, true, validateJsonSpecUrl("/path_/jsonSpec.json")) - require.Equal(t, false, validateJsonSpecUrl("path/to/jsonSpec.json")) - require.Equal(t, false, validateJsonSpecUrl("/path/to/jsonSpec")) - require.Equal(t, false, validateJsonSpecUrl("/path/to/jsonSpec.jsn")) +func TestValidateJsonSpecURL(t *testing.T) { + require.Equal(t, true, validateSpecURL("/path/to/jsonSpec.json")) + require.Equal(t, true, validateSpecURL("/spec.json")) + require.Equal(t, true, validateSpecURL("/path_/jsonSpec.json")) + require.Equal(t, false, validateSpecURL("path/to/jsonSpec.json")) + require.Equal(t, false, validateSpecURL("/path/to/jsonSpec")) + require.Equal(t, false, validateSpecURL("/path/to/jsonSpec.jsn")) } func TestValidateSwaggerUrl(t *testing.T) { - require.Equal(t, true, validateSwaggerUrl("/path/to/jsonSpec")) - require.Equal(t, true, validateSwaggerUrl("/swagger")) - require.Equal(t, true, validateSwaggerUrl("/Super-useful_swagger-2000")) - require.Equal(t, true, validateSwaggerUrl("/Super-useful_swagger-")) - require.Equal(t, true, validateSwaggerUrl("/Super-useful_swagger__")) - require.Equal(t, true, validateSwaggerUrl("/Super-useful_swaggeR")) - require.Equal(t, false, validateSwaggerUrl("/spec.json")) - require.Equal(t, false, validateSwaggerUrl("/path_/swagger.json")) - require.Equal(t, false, validateSwaggerUrl("path/to/jsonSpec.")) - require.Equal(t, false, validateSwaggerUrl("path/to/jsonSpec%")) + require.Equal(t, true, validateSwaggerURL("/path/to/jsonSpec")) + require.Equal(t, true, validateSwaggerURL("/swagger")) + require.Equal(t, true, validateSwaggerURL("/Super-useful_swagger-2000")) + require.Equal(t, true, validateSwaggerURL("/Super-useful_swagger-")) + require.Equal(t, true, validateSwaggerURL("/Super-useful_swagger__")) + require.Equal(t, true, validateSwaggerURL("/Super-useful_swaggeR")) + require.Equal(t, false, validateSwaggerURL("/spec.json")) + require.Equal(t, false, validateSwaggerURL("/path_/swagger.json")) + require.Equal(t, false, validateSwaggerURL("path/to/jsonSpec.")) + require.Equal(t, false, validateSwaggerURL("path/to/jsonSpec%")) } func TestLocalSave(t *testing.T) { diff --git a/server.go b/server.go index 9343d12e..b9c6a374 100644 --- a/server.go +++ b/server.go @@ -22,12 +22,12 @@ type OpenAPIServerConfig struct { // Handler to serve the OpenAPI UI from spec URL UIHandler func(specURL string) http.Handler // URL to serve the OpenAPI JSON spec - JsonURL string + SpecURL string } var defaultOpenAPIServerConfig = OpenAPIServerConfig{ SwaggerURL: "/swagger", - JsonURL: "/swagger/openapi.json", + SpecURL: "/swagger/openapi.json", UIHandler: DefaultOpenAPIHandler, } @@ -373,8 +373,8 @@ func WithoutLogger() func(*Server) { func WithOpenAPIServerConfig(config OpenAPIServerConfig) func(*Server) { return func(s *Server) { - if config.JsonURL != "" { - s.OpenAPIServerConfig.JsonURL = config.JsonURL + if config.SpecURL != "" { + s.OpenAPIServerConfig.SpecURL = config.SpecURL } if config.SwaggerURL != "" { s.OpenAPIServerConfig.SwaggerURL = config.SwaggerURL @@ -385,12 +385,12 @@ func WithOpenAPIServerConfig(config OpenAPIServerConfig) func(*Server) { s.OpenAPIServerConfig.DisableSwaggerUI = config.DisableSwaggerUI - if !validateJsonSpecUrl(s.OpenAPIServerConfig.JsonURL) { - slog.Error("Error serving openapi json spec. Value of 's.OpenAPIServerConfig.JsonURL' option is not valid", "url", s.OpenAPIServerConfig.JsonURL) + if !validateSpecURL(s.OpenAPIServerConfig.SpecURL) { + slog.Error("Error serving openapi json spec. Value of 's.OpenAPIServerConfig.SpecURL' option is not valid", "url", s.OpenAPIServerConfig.SpecURL) return } - if !validateSwaggerUrl(s.OpenAPIServerConfig.SwaggerURL) { + if !validateSwaggerURL(s.OpenAPIServerConfig.SwaggerURL) { slog.Error("Error serving swagger ui. Value of 's.OpenAPIServerConfig.SwaggerURL' option is not valid", "url", s.OpenAPIServerConfig.SwaggerURL) return } diff --git a/server_test.go b/server_test.go index b6002972..618a28e2 100644 --- a/server_test.go +++ b/server_test.go @@ -78,7 +78,7 @@ func TestWithOpenAPIConfig(t *testing.T) { ) require.Equal(t, "/swagger", s.SwaggerURL) - require.Equal(t, "/swagger/openapi.json", s.JsonURL) + require.Equal(t, "/swagger/openapi.json", s.SpecURL) require.Equal(t, "doc/openapi.json", s.OpenAPIConfig.JSONFilePath) require.False(t, s.OpenAPIConfig.PrettyFormatJSON) }) @@ -87,7 +87,7 @@ func TestWithOpenAPIConfig(t *testing.T) { s := NewServer( WithOpenAPIServerConfig(OpenAPIServerConfig{ SwaggerURL: "/api", - JsonURL: "/api/openapi.json", + SpecURL: "/api/openapi.json", }), WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ @@ -100,7 +100,7 @@ func TestWithOpenAPIConfig(t *testing.T) { ) require.Equal(t, "/api", s.SwaggerURL) - require.Equal(t, "/api/openapi.json", s.JsonURL) + require.Equal(t, "/api/openapi.json", s.SpecURL) require.Equal(t, "openapi.json", s.OpenAPIConfig.JSONFilePath) require.True(t, s.Engine.OpenAPIConfig.Disabled) require.True(t, s.OpenAPIConfig.DisableLocalSave) @@ -112,7 +112,7 @@ func TestWithOpenAPIConfig(t *testing.T) { NewServer( WithOpenAPIServerConfig(OpenAPIServerConfig{ SwaggerURL: "p i", - JsonURL: "pi/op enapi.json", + SpecURL: "pi/op enapi.json", }), WithEngineOptions( WithOpenAPIConfig(OpenAPIConfig{ @@ -124,7 +124,7 @@ func TestWithOpenAPIConfig(t *testing.T) { t.Run("with invalid url", func(t *testing.T) { NewServer( WithOpenAPIServerConfig(OpenAPIServerConfig{ - JsonURL: "pi/op enapi.json", + SpecURL: "pi/op enapi.json", SwaggerURL: "p i", }), WithEngineOptions( @@ -138,7 +138,7 @@ func TestWithOpenAPIConfig(t *testing.T) { t.Run("with invalid url", func(t *testing.T) { NewServer( WithOpenAPIServerConfig(OpenAPIServerConfig{ - JsonURL: "/api/openapi.json", + SpecURL: "/api/openapi.json", SwaggerURL: "invalid path", }), WithEngineOptions( From aeeccb00b9b1e663c4c395162df5c11ec9b725ce Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Wed, 25 Dec 2024 18:58:48 -0500 Subject: [PATCH 08/12] chore: do not embed OpenAPIServerConfig --- openapi.go | 12 ++++++------ openapi_handler_test.go | 4 ++-- server.go | 4 ++-- server_test.go | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/openapi.go b/openapi.go index 99c084ed..5739bd09 100644 --- a/openapi.go +++ b/openapi.go @@ -116,13 +116,13 @@ func (s *Server) OutputOpenAPISpec() openapi3.T { // Registers the routes to serve the OpenAPI spec and Swagger UI. func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { - GetStd(s, s.SpecURL, func(w http.ResponseWriter, r *http.Request) { + GetStd(s, s.OpenAPIServerConfig.SpecURL, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(jsonSpec) }) - s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s%s", s.url(), s.SpecURL)) + s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s%s", s.url(), s.OpenAPIServerConfig.SpecURL)) - if s.DisableSwaggerUI { + if s.OpenAPIServerConfig.DisableSwaggerUI { return } Registers(s.Engine, netHttpRouteRegisterer[any, any]{ @@ -130,12 +130,12 @@ func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) { route: Route[any, any]{ BaseRoute: BaseRoute{ Method: http.MethodGet, - Path: s.SwaggerURL + "/", + Path: s.OpenAPIServerConfig.SwaggerURL + "/", }, }, - controller: s.UIHandler(s.SpecURL), + controller: s.OpenAPIServerConfig.UIHandler(s.OpenAPIServerConfig.SpecURL), }) - s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: %s%s/index.html", s.url(), s.SwaggerURL)) + s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: %s%s/index.html", s.url(), s.OpenAPIServerConfig.SwaggerURL)) } func validateSpecURL(specURL string) bool { diff --git a/openapi_handler_test.go b/openapi_handler_test.go index 3764d6e7..2e3e6323 100644 --- a/openapi_handler_test.go +++ b/openapi_handler_test.go @@ -23,7 +23,7 @@ func TestUIHandler(t *testing.T) { s.OutputOpenAPISpec() - require.NotNil(t, s.UIHandler) + require.NotNil(t, s.OpenAPIServerConfig.UIHandler) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/swagger/index.html", nil) @@ -45,7 +45,7 @@ func TestUIHandler(t *testing.T) { ) s.OutputOpenAPISpec() - require.NotNil(t, s.UIHandler) + require.NotNil(t, s.OpenAPIServerConfig.UIHandler) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/swagger/index.html", nil) diff --git a/server.go b/server.go index b9c6a374..1dbdb0af 100644 --- a/server.go +++ b/server.go @@ -74,10 +74,10 @@ type Server struct { startTime time.Time - OpenAPIConfig OpenAPIConfig - loggingConfig LoggingConfig + OpenAPIServerConfig OpenAPIServerConfig + isTLS bool } diff --git a/server_test.go b/server_test.go index 618a28e2..2dca20e8 100644 --- a/server_test.go +++ b/server_test.go @@ -77,8 +77,8 @@ func TestWithOpenAPIConfig(t *testing.T) { WithOpenAPIServerConfig(OpenAPIServerConfig{}), ) - require.Equal(t, "/swagger", s.SwaggerURL) - require.Equal(t, "/swagger/openapi.json", s.SpecURL) + require.Equal(t, "/swagger", s.OpenAPIServerConfig.SwaggerURL) + require.Equal(t, "/swagger/openapi.json", s.OpenAPIServerConfig.SpecURL) require.Equal(t, "doc/openapi.json", s.OpenAPIConfig.JSONFilePath) require.False(t, s.OpenAPIConfig.PrettyFormatJSON) }) @@ -99,8 +99,8 @@ func TestWithOpenAPIConfig(t *testing.T) { ), ) - require.Equal(t, "/api", s.SwaggerURL) - require.Equal(t, "/api/openapi.json", s.SpecURL) + require.Equal(t, "/api", s.OpenAPIServerConfig.SwaggerURL) + require.Equal(t, "/api/openapi.json", s.OpenAPIServerConfig.SpecURL) require.Equal(t, "openapi.json", s.OpenAPIConfig.JSONFilePath) require.True(t, s.Engine.OpenAPIConfig.Disabled) require.True(t, s.OpenAPIConfig.DisableLocalSave) From 0a7b77163a17356c45efec80d4486c9b4b75acb6 Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Thu, 26 Dec 2024 10:34:38 -0500 Subject: [PATCH 09/12] chore: add docs for WithEngineOptions and NewEngine --- engine.go | 12 ++++++++++++ server.go | 16 +++++++++++++++- server_test.go | 13 +++++++------ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/engine.go b/engine.go index 141759c8..a42785bc 100644 --- a/engine.go +++ b/engine.go @@ -9,6 +9,18 @@ import ( "path/filepath" ) +// NewEngine creates a new Engine with the given options. +// For example: +// +// engine := fuego.NewEngin( +// WithOpenAPIConfig( +// OpenAPIConfig{ +// PrettyFormatJSON: true, +// }, +// ), +// ) +// +// Options all begin with `With`. func NewEngine(options ...func(*Engine)) *Engine { e := &Engine{ OpenAPI: NewOpenAPI(), diff --git a/server.go b/server.go index 1dbdb0af..307a12ac 100644 --- a/server.go +++ b/server.go @@ -89,7 +89,7 @@ type Server struct { // fuego.WithoutLogger(), // ) // -// Option all begin with `With`. +// Options all begin with `With`. // Some default options are set in the function body. func NewServer(options ...func(*Server)) *Server { s := &Server{ @@ -397,6 +397,20 @@ func WithOpenAPIServerConfig(config OpenAPIServerConfig) func(*Server) { } } +// WithEngineOptions allows for setting of Engine options +// +// app := fuego.NewServer( +// fuego.WithAddr(":8080"), +// fuego.WithEngineOptions( +// WithOpenAPIConfig( +// OpenAPIConfig{ +// PrettyFormatJSON: true, +// }, +// ), +// ), +// ) +// +// Options all begin with `With`. func WithEngineOptions(options ...func(*Engine)) func(*Server) { return func(s *Server) { for _, option := range options { diff --git a/server_test.go b/server_test.go index 2dca20e8..2aa85634 100644 --- a/server_test.go +++ b/server_test.go @@ -90,12 +90,13 @@ func TestWithOpenAPIConfig(t *testing.T) { SpecURL: "/api/openapi.json", }), WithEngineOptions( - WithOpenAPIConfig(OpenAPIConfig{ - JSONFilePath: "openapi.json", - DisableLocalSave: true, - PrettyFormatJSON: true, - Disabled: true, - }), + WithOpenAPIConfig( + OpenAPIConfig{ + JSONFilePath: "openapi.json", + DisableLocalSave: true, + PrettyFormatJSON: true, + Disabled: true, + }), ), ) From b5c9cbae09a2a22c1c4e3510b2360d3c9ea91c3c Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Sun, 29 Dec 2024 12:13:02 -0500 Subject: [PATCH 10/12] BREAKING: WithErrorHandler server option to engine option --- engine.go | 11 +++++++++++ engine_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ server.go | 5 ----- 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 engine_test.go diff --git a/engine.go b/engine.go index a42785bc..d2168f31 100644 --- a/engine.go +++ b/engine.go @@ -71,6 +71,17 @@ func WithOpenAPIConfig(config OpenAPIConfig) func(*Engine) { } } +// WithErrorHandler sets a customer error handler for the server +func WithErrorHandler(errorHandler func(err error) error) func(*Engine) { + return func(e *Engine) { + if errorHandler == nil { + panic("errorHandler cannot be nil") + } + + e.ErrorHandler = errorHandler + } +} + // OutputOpenAPISpec takes the OpenAPI spec and outputs it to a JSON file func (e *Engine) OutputOpenAPISpec() []byte { e.OpenAPI.computeTags() diff --git a/engine_test.go b/engine_test.go new file mode 100644 index 00000000..3962716f --- /dev/null +++ b/engine_test.go @@ -0,0 +1,40 @@ +package fuego + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWithErrorHandler(t *testing.T) { + t.Run("default engine", func(t *testing.T) { + e := NewEngine() + err := NotFoundError{ + Err: errors.New("Not Found :c"), + } + errResponse := e.ErrorHandler(err) + require.ErrorAs(t, errResponse, &HTTPError{}) + }) + t.Run("custom handler", func(t *testing.T) { + e := NewEngine( + WithErrorHandler(func(err error) error { + return fmt.Errorf("%w foobar", err) + }), + ) + err := NotFoundError{ + Err: errors.New("Not Found :c"), + } + errResponse := e.ErrorHandler(err) + require.ErrorAs(t, errResponse, &HTTPError{}) + require.ErrorContains(t, errResponse, "Not Found :c foobar") + }) + t.Run("should be fatal", func(t *testing.T) { + require.Panics(t, func() { + NewEngine( + WithErrorHandler(nil), + ) + }) + }) +} diff --git a/server.go b/server.go index 307a12ac..450c4fa5 100644 --- a/server.go +++ b/server.go @@ -351,11 +351,6 @@ func WithErrorSerializer(serializer ErrorSender) func(*Server) { return func(c *Server) { c.SerializeError = serializer } } -// WithErrorHandler sets a customer error handler for the server -func WithErrorHandler(errorHandler func(err error) error) func(*Server) { - return func(c *Server) { c.ErrorHandler = errorHandler } -} - // WithoutStartupMessages disables the startup message func WithoutStartupMessages() func(*Server) { return func(c *Server) { From d011f7853f9ef04eeff25e1a82f09dc66992fac3 Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Mon, 6 Jan 2025 14:46:29 -0500 Subject: [PATCH 11/12] chore: wrap errors Co-authored-by: Zwanga Mukwevho <53586112+ZwangaMukwevho@users.noreply.github.com> --- engine.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/engine.go b/engine.go index d2168f31..b171ac7c 100644 --- a/engine.go +++ b/engine.go @@ -3,7 +3,7 @@ package fuego import ( "context" "encoding/json" - "errors" + "fmt" "log/slog" "os" "path/filepath" @@ -112,18 +112,18 @@ func (e *Engine) saveOpenAPIToFile(jsonSpecLocalPath string, jsonSpec []byte) er err := os.MkdirAll(jsonFolder, 0o750) if err != nil { - return errors.New("error creating docs directory") + return fmt.Errorf("error creating docs directory: %w", err) } f, err := os.Create(jsonSpecLocalPath) // #nosec G304 (file path provided by developer, not by user) if err != nil { - return errors.New("error creating file") + return fmt.Errorf("error creating file: %w", err) } defer f.Close() _, err = f.Write(jsonSpec) if err != nil { - return errors.New("error writing file ") + return fmt.Errorf("error writing file: %w", err) } e.printOpenAPIMessage("JSON file: " + jsonSpecLocalPath) From 22340089d873a12e7c4a08a3a62a280129b4dc89 Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Tue, 7 Jan 2025 16:09:47 +0100 Subject: [PATCH 12/12] docs: Adds comment to explain the Engine abstraction --- server.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server.go b/server.go index 450c4fa5..53413bae 100644 --- a/server.go +++ b/server.go @@ -82,6 +82,8 @@ type Server struct { } // NewServer creates a new server with the given options. +// Fuego's [Server] is built on top of the standard library's [http.Server]. +// The OpenAPI and data flow is handled by the [Engine], a lightweight abstraction available for all kind of routers (net/http, Gin, Echo). // For example: // // app := fuego.NewServer( @@ -90,6 +92,7 @@ type Server struct { // ) // // Options all begin with `With`. +// Some options are at engine level, and can be set with [WithEngineOptions]. // Some default options are set in the function body. func NewServer(options ...func(*Server)) *Server { s := &Server{ @@ -405,7 +408,7 @@ func WithOpenAPIServerConfig(config OpenAPIServerConfig) func(*Server) { // ), // ) // -// Options all begin with `With`. +// Engine Options all begin with `With`. func WithEngineOptions(options ...func(*Engine)) func(*Server) { return func(s *Server) { for _, option := range options {