Skip to content

Commit

Permalink
BREAKING: create generic engine.OutputOpenAPISpec for use in all engines
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanhitt committed Dec 23, 2024
1 parent a0f00d5 commit 72988f0
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 144 deletions.
135 changes: 129 additions & 6 deletions engine.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 3 additions & 1 deletion examples/generate-opengraph-image/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}),
)

Expand Down
6 changes: 4 additions & 2 deletions examples/petstore/lib/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}),
)

Expand Down
64 changes: 1 addition & 63 deletions openapi.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
package fuego

import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
"reflect"
"regexp"
"slices"
Expand Down Expand Up @@ -112,64 +107,13 @@ 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()
}

func (s *Server) marshalSpec() ([]byte, error) {
if s.OpenAPIConfig.PrettyFormatJson {
return json.MarshalIndent(s.OpenAPI.Description(), "", " ")
}
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 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 ")
}

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) {
Expand All @@ -189,12 +133,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)
Expand Down
30 changes: 20 additions & 10 deletions openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ func TestServer_OutputOpenApiSpec(t *testing.T) {
s := NewServer(
WithOpenAPIConfig(
OpenAPIConfig{
JsonFilePath: docPath,
EngineOpenAPIConfig: EngineOpenAPIConfig{
JsonFilePath: docPath,
},
},
),
)
Expand All @@ -268,8 +270,10 @@ func TestServer_OutputOpenApiSpec(t *testing.T) {
s := NewServer(
WithOpenAPIConfig(
OpenAPIConfig{
JsonFilePath: docPath,
DisableLocalSave: true,
EngineOpenAPIConfig: EngineOpenAPIConfig{
JsonFilePath: docPath,
DisableLocalSave: true,
},
},
),
)
Expand All @@ -288,9 +292,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,
},
),
)
Expand All @@ -310,8 +316,10 @@ func TestServer_OutputOpenApiSpec(t *testing.T) {
s := NewServer(
WithOpenAPIConfig(
OpenAPIConfig{
JsonFilePath: docPath,
PrettyFormatJson: true,
EngineOpenAPIConfig: EngineOpenAPIConfig{
JsonFilePath: docPath,
PrettyFormatJson: true,
},
},
),
)
Expand Down Expand Up @@ -426,8 +434,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) {
Expand Down
6 changes: 6 additions & 0 deletions serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"net/http"
"reflect"
"time"

"github.com/getkin/kin-openapi/openapi3"
)

// Run starts the server.
Expand All @@ -29,6 +31,10 @@ func (s *Server) RunTLS(certFile, keyFile string) error {
}

func (s *Server) setup() {
s.OpenAPI.Description().Servers = append(s.OpenAPI.Description().Servers, &openapi3.Server{
URL: s.url(),
Description: "local server",
})
go s.OutputOpenAPISpec()
s.printStartupMessage()

Expand Down
Loading

0 comments on commit 72988f0

Please sign in to comment.