diff --git a/.gitignore b/.gitignore index 9be0b89..20cf7b3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ main # Database dumps *_dump.sql + +# Generated with `just openapi` +/openapi.json +/openapi-3.0.json diff --git a/cmd/openapi/main.go b/cmd/openapi/main.go new file mode 100644 index 0000000..ba89422 --- /dev/null +++ b/cmd/openapi/main.go @@ -0,0 +1,51 @@ +// Command openapi writes the MusicServer API description without starting the server. +// +// go run ./cmd/openapi -out . +// +// It writes openapi.json (OpenAPI 3.1) and openapi-3.0.json (OpenAPI 3.0, for tools +// that don't support 3.1 yet). A running server serves the same files at /openapi.json +// and /openapi-3.0.json. +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "music-server/internal/server" +) + +func main() { + out := flag.String("out", ".", "directory to write openapi.json and openapi-3.0.json to") + flag.Parse() + + if err := run(*out); err != nil { + fmt.Fprintln(os.Stderr, "openapi:", err) + os.Exit(1) + } +} + +func run(out string) error { + spec := server.OpenAPISpec() + v31, err := spec.MarshalJSON() + if err != nil { + return err + } + v30, err := spec.Downgrade() + if err != nil { + return err + } + + if err := os.MkdirAll(out, 0o755); err != nil { + return err + } + for name, data := range map[string][]byte{"openapi.json": v31, "openapi-3.0.json": v30} { + path := filepath.Join(out, name) + if err := os.WriteFile(path, data, 0o644); err != nil { + return err + } + fmt.Println("Wrote", path) + } + return nil +} diff --git a/cmd/openapi/main_test.go b/cmd/openapi/main_test.go new file mode 100644 index 0000000..a962736 --- /dev/null +++ b/cmd/openapi/main_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunWritesBothVersions(t *testing.T) { + dir := t.TempDir() + require.NoError(t, run(dir)) + + for name, version := range map[string]string{"openapi.json": "3.1.0", "openapi-3.0.json": "3.0.3"} { + data, err := os.ReadFile(filepath.Join(dir, name)) + require.NoError(t, err) + var spec struct { + OpenAPI string `json:"openapi"` + Paths map[string]any `json:"paths"` + } + require.NoError(t, json.Unmarshal(data, &spec), name) + assert.Equal(t, version, spec.OpenAPI, name) + assert.Contains(t, spec.Paths, "/api/v1/music/info", name) + } +} diff --git a/internal/server/openapi_test.go b/internal/server/openapi_test.go index 6f63875..d241f2a 100644 --- a/internal/server/openapi_test.go +++ b/internal/server/openapi_test.go @@ -46,6 +46,13 @@ func TestOpenAPISpecIsValid(t *testing.T) { assert.Len(t, spec31.Paths, spec.Paths.Len()) } +func TestOpenAPISpecWithoutServerMatchesServedSpec(t *testing.T) { + served := newTestEnv(t).do(http.MethodGet, "/openapi.json").Body.Bytes() + offline, err := OpenAPISpec().MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, string(served), string(offline)) +} + func TestOpenAPIDocumentsTheV1API(t *testing.T) { spec := loadSpec(t, newTestEnv(t)) diff --git a/internal/server/routes.go b/internal/server/routes.go index 9d2f998..ba10f09 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -8,12 +8,25 @@ import ( "music-server/internal/server/middleware" "github.com/a-h/templ" + "github.com/danielgtaylor/huma/v2" "github.com/labstack/echo/v5" echoMiddleware "github.com/labstack/echo/v5/middleware" "go.uber.org/zap" ) func (s *Server) RegisterRoutes() http.Handler { + e, _ := s.routes() + return e +} + +// OpenAPISpec returns the API description. It needs no database or configuration: +// the handlers are registered but never called. +func OpenAPISpec() *huma.OpenAPI { + _, a := (&Server{}).routes() + return a.v1.OpenAPI() +} + +func (s *Server) routes() (*echo.Echo, apis) { e := echo.New() e.HTTPErrorHandler = httpErrorHandler if len(s.cfg.TrustedProxies) > 0 { @@ -58,7 +71,7 @@ func (s *Server) RegisterRoutes() http.Handler { for _, r := range e.Router().Routes() { logging.GetLogger().Debug("Registered route", zap.String("method", r.Method), zap.String("path", r.Path)) } - return e + return e, a } func redirect(to string) echo.HandlerFunc { diff --git a/justfile b/justfile index 289a3bc..9fcc620 100644 --- a/justfile +++ b/justfile @@ -15,6 +15,10 @@ sqlc-generate: migrate-create name: @migrate create -ext sql -dir internal/db/migrations -seq {{name}} +# Write openapi.json and openapi-3.0.json without starting the server, e.g. `just openapi ../MusicPlayer/api` +openapi out=".": + @go run ./cmd/openapi -out {{out}} + build: sqlc-generate templ-build @echo "Building..." @go build -o main cmd/main.go