Skip to content

Commit

Permalink
Exposes the OutputOpenAPISpec method to generate directly the spec wi…
Browse files Browse the repository at this point in the history
…thout starting server
  • Loading branch information
EwenQuim committed Mar 14, 2024
1 parent feab1b3 commit 023b47d
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 49 deletions.
27 changes: 15 additions & 12 deletions openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ func NewOpenApiSpec() openapi3.T {
return spec
}

func (s *Server) generateOpenAPI() openapi3.T {
// OutputOpenAPISpec takes the OpenAPI spec and outputs it to a JSON file and/or serves it on a URL.
// Also serves a Swagger UI.
// To modify its behavior, use the [WithOpenAPIConfig] option.
func (s *Server) OutputOpenAPISpec() openapi3.T {
// Validate
err := s.OpenApiSpec.Validate(context.Background())
if err != nil {
Expand All @@ -48,21 +51,21 @@ func (s *Server) generateOpenAPI() openapi3.T {
slog.Error("Error marshalling spec to JSON", "error", err)
}

if !s.OpenapiConfig.DisableSwagger {
generateSwagger(s, jsonSpec)
if !s.OpenAPIConfig.DisableSwagger {
registerOpenAPIRoutes(s, jsonSpec)
}

if !s.OpenapiConfig.DisableLocalSave {
err := localSave(s.OpenapiConfig.JsonFilePath, jsonSpec)
if !s.OpenAPIConfig.DisableLocalSave {
err := saveOpenAPIToFile(s.OpenAPIConfig.JsonFilePath, jsonSpec)
if err != nil {
slog.Error("Error saving spec to local path", "error", err, "path", s.OpenapiConfig.JsonFilePath)
slog.Error("Error saving spec to local path", "error", err, "path", s.OpenAPIConfig.JsonFilePath)
}
}

return s.OpenApiSpec
}

func localSave(jsonSpecLocalPath string, jsonSpec []byte) error {
func saveOpenAPIToFile(jsonSpecLocalPath string, jsonSpec []byte) error {
jsonFolder := filepath.Dir(jsonSpecLocalPath)

err := os.MkdirAll(jsonFolder, 0o750)
Expand All @@ -86,16 +89,16 @@ func localSave(jsonSpecLocalPath string, jsonSpec []byte) error {
}

// Registers the routes to serve the OpenAPI spec and Swagger UI.
func generateSwagger(s *Server, jsonSpec []byte) {
GetStd(s, s.OpenapiConfig.JsonUrl, func(w http.ResponseWriter, r *http.Request) {
func registerOpenAPIRoutes(s *Server, jsonSpec []byte) {
GetStd(s, s.OpenAPIConfig.JsonUrl, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonSpec)
})

Handle(s, s.OpenapiConfig.SwaggerUrl+"/", s.UIHandler(s.OpenapiConfig.JsonUrl))
Handle(s, s.OpenAPIConfig.SwaggerUrl+"/", s.UIHandler(s.OpenAPIConfig.JsonUrl))

slog.Info(fmt.Sprintf("JSON spec: http://%s%s", s.Server.Addr, s.OpenapiConfig.JsonUrl))
slog.Info(fmt.Sprintf("OpenAPI UI: http://%s%s/index.html", s.Server.Addr, s.OpenapiConfig.SwaggerUrl))
slog.Info(fmt.Sprintf("JSON spec: http://%s%s", s.Server.Addr, s.OpenAPIConfig.JsonUrl))
slog.Info(fmt.Sprintf("OpenAPI UI: http://%s%s/index.html", s.Server.Addr, s.OpenAPIConfig.SwaggerUrl))
}

func validateJsonSpecLocalPath(jsonSpecLocalPath string) bool {
Expand Down
8 changes: 4 additions & 4 deletions openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestServer_generateOpenAPI(t *testing.T) {
Get(s, "/post/{id}", func(*ContextNoBody) (MyOutputStruct, error) {
return MyOutputStruct{}, nil
})
document := s.generateOpenAPI()
document := s.OutputOpenAPISpec()
require.NotNil(t, document)
require.NotNil(t, document.Paths.Find("/"))
require.Nil(t, document.Paths.Find("/unknown"))
Expand Down Expand Up @@ -119,7 +119,7 @@ func BenchmarkServer_generateOpenAPI(b *testing.B) {
})
}

s.generateOpenAPI()
s.OutputOpenAPISpec()
}
}

Expand Down Expand Up @@ -157,15 +157,15 @@ func TestValidateSwaggerUrl(t *testing.T) {

func TestLocalSave(t *testing.T) {
t.Run("with valid path", func(t *testing.T) {
err := localSave("/tmp/jsonSpec.json", []byte("test"))
err := saveOpenAPIToFile("/tmp/jsonSpec.json", []byte("test"))
require.NoError(t, err)

// cleanup
os.Remove("/tmp/jsonSpec.json")
})

t.Run("with invalid path", func(t *testing.T) {
err := localSave("///jsonSpec.json", []byte("test"))
err := saveOpenAPIToFile("///jsonSpec.json", []byte("test"))
require.Error(t, err)
})
}
36 changes: 18 additions & 18 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import (
"github.com/golang-jwt/jwt/v5"
)

type OpenapiConfig struct {
type OpenAPIConfig struct {
DisableSwagger bool // If true, the server will not serve the swagger ui nor the openapi json spec
DisableLocalSave bool // If true, the server will not save the openapi json spec locally
SwaggerUrl string // URL to serve the swagger ui
JsonUrl string // URL to serve the openapi json spec
JsonFilePath string // Local path to save the openapi json spec
}

var defaultOpenapiConfig = OpenapiConfig{
var defaultOpenAPIConfig = OpenAPIConfig{
SwaggerUrl: "/swagger",
JsonUrl: "/swagger/openapi.json",
JsonFilePath: "doc/openapi.json",
Expand Down Expand Up @@ -63,7 +63,7 @@ type Server struct {
ErrorHandler func(err error) error // Used to transform any error into a unified error type structure with status code. Defaults to [ErrorHandler]
startTime time.Time

OpenapiConfig OpenapiConfig
OpenAPIConfig OpenAPIConfig
}

// NewServer creates a new server with the given options.
Expand All @@ -87,7 +87,7 @@ func NewServer(options ...func(*Server)) *Server {
Mux: http.NewServeMux(),
OpenApiSpec: NewOpenApiSpec(),

OpenapiConfig: defaultOpenapiConfig,
OpenAPIConfig: defaultOpenAPIConfig,
UIHandler: defaultOpenAPIHandler,

Security: NewSecurity(),
Expand Down Expand Up @@ -264,34 +264,34 @@ func WithoutLogger() func(*Server) {
}
}

func WithOpenapiConfig(openapiConfig OpenapiConfig) func(*Server) {
func WithOpenAPIConfig(openapiConfig OpenAPIConfig) func(*Server) {
return func(s *Server) {
s.OpenapiConfig = openapiConfig
s.OpenAPIConfig = openapiConfig

if s.OpenapiConfig.JsonUrl == "" {
s.OpenapiConfig.JsonUrl = defaultOpenapiConfig.JsonUrl
if s.OpenAPIConfig.JsonUrl == "" {
s.OpenAPIConfig.JsonUrl = defaultOpenAPIConfig.JsonUrl
}

if s.OpenapiConfig.SwaggerUrl == "" {
s.OpenapiConfig.SwaggerUrl = defaultOpenapiConfig.SwaggerUrl
if s.OpenAPIConfig.SwaggerUrl == "" {
s.OpenAPIConfig.SwaggerUrl = defaultOpenAPIConfig.SwaggerUrl
}

if s.OpenapiConfig.JsonFilePath == "" {
s.OpenapiConfig.JsonFilePath = defaultOpenapiConfig.JsonFilePath
if s.OpenAPIConfig.JsonFilePath == "" {
s.OpenAPIConfig.JsonFilePath = defaultOpenAPIConfig.JsonFilePath
}

if !validateJsonSpecLocalPath(s.OpenapiConfig.JsonFilePath) {
slog.Error("Error writing json spec. Value of 'jsonSpecLocalPath' option is not valid", "file", s.OpenapiConfig.JsonFilePath)
if !validateJsonSpecLocalPath(s.OpenAPIConfig.JsonFilePath) {
slog.Error("Error writing json spec. Value of 'jsonSpecLocalPath' option is not valid", "file", s.OpenAPIConfig.JsonFilePath)
return
}

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)
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)
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
}
}
Expand Down
26 changes: 13 additions & 13 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,17 @@ func TestWithXML(t *testing.T) {
func TestWithOpenAPIConfig(t *testing.T) {
t.Run("with default values", func(t *testing.T) {
s := NewServer(
WithOpenapiConfig(OpenapiConfig{}),
WithOpenAPIConfig(OpenAPIConfig{}),
)

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.Equal(t, "/swagger", s.OpenAPIConfig.SwaggerUrl)
require.Equal(t, "/swagger/openapi.json", s.OpenAPIConfig.JsonUrl)
require.Equal(t, "doc/openapi.json", s.OpenAPIConfig.JsonFilePath)
})

t.Run("with custom values", func(t *testing.T) {
s := NewServer(
WithOpenapiConfig(OpenapiConfig{
WithOpenAPIConfig(OpenAPIConfig{
SwaggerUrl: "/api",
JsonUrl: "/api/openapi.json",
JsonFilePath: "openapi.json",
Expand All @@ -88,17 +88,17 @@ func TestWithOpenAPIConfig(t *testing.T) {
}),
)

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.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)
})

t.Run("with invalid local path values", func(t *testing.T) {
t.Run("with invalid path", func(t *testing.T) {
NewServer(
WithOpenapiConfig(OpenapiConfig{
WithOpenAPIConfig(OpenAPIConfig{
JsonFilePath: "path/to/jsonSpec",
SwaggerUrl: "p i",
JsonUrl: "pi/op enapi.json",
Expand All @@ -107,7 +107,7 @@ func TestWithOpenAPIConfig(t *testing.T) {
})
t.Run("with invalid url", func(t *testing.T) {
NewServer(
WithOpenapiConfig(OpenapiConfig{
WithOpenAPIConfig(OpenAPIConfig{
JsonFilePath: "path/to/jsonSpec.json",
JsonUrl: "pi/op enapi.json",
SwaggerUrl: "p i",
Expand All @@ -117,7 +117,7 @@ func TestWithOpenAPIConfig(t *testing.T) {

t.Run("with invalid url", func(t *testing.T) {
NewServer(
WithOpenapiConfig(OpenapiConfig{
WithOpenAPIConfig(OpenAPIConfig{
JsonFilePath: "path/to/jsonSpec.json",
JsonUrl: "/api/openapi.json",
SwaggerUrl: "invalid path",
Expand Down
5 changes: 3 additions & 2 deletions serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (

// Run starts the server.
// It is blocking.
// It returns an error if the server could not start (it could not bind to the port).
// It returns an error if the server could not start (it could not bind to the port for example).
// It also generates the OpenAPI spec and outputs it to a file, the UI, and a handler (if enabled).
func (s *Server) Run() error {
go s.generateOpenAPI()
go s.OutputOpenAPISpec()
elapsed := time.Since(s.startTime)
slog.Debug("Server started in "+elapsed.String(), "info", "time between since server creation (fuego.NewServer) and server startup (fuego.Run). Depending on your implementation, there might be things that do not depend on fuego slowing start time")
slog.Info("Server running ✅ on http://"+s.Server.Addr, "started in", elapsed.String())
Expand Down

0 comments on commit 023b47d

Please sign in to comment.