Add a command to write the OpenAPI spec without a server
`just openapi [dir]` (go run ./cmd/openapi) writes openapi.json (3.1) and openapi-3.0.json without a database or configuration. A test checks it matches the spec the server serves.
This commit is contained in:
1 parent
973ebf224b
commit
6da96f79e7
6 files changed
+108
-1
No files matched your search
@@ -10,3 +10,7 @@ main
|
|||||||
|
|
||||||
# Database dumps
|
# Database dumps
|
||||||
*_dump.sql
|
*_dump.sql
|
||||||
|
|
||||||
|
# Generated with `just openapi`
|
||||||
|
/openapi.json
|
||||||
|
/openapi-3.0.json
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,13 @@ func TestOpenAPISpecIsValid(t *testing.T) {
|
|||||||
assert.Len(t, spec31.Paths, spec.Paths.Len())
|
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) {
|
func TestOpenAPIDocumentsTheV1API(t *testing.T) {
|
||||||
spec := loadSpec(t, newTestEnv(t))
|
spec := loadSpec(t, newTestEnv(t))
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,25 @@ import (
|
|||||||
"music-server/internal/server/middleware"
|
"music-server/internal/server/middleware"
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
|
"github.com/danielgtaylor/huma/v2"
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
echoMiddleware "github.com/labstack/echo/v5/middleware"
|
echoMiddleware "github.com/labstack/echo/v5/middleware"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) RegisterRoutes() http.Handler {
|
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 := echo.New()
|
||||||
e.HTTPErrorHandler = httpErrorHandler
|
e.HTTPErrorHandler = httpErrorHandler
|
||||||
if len(s.cfg.TrustedProxies) > 0 {
|
if len(s.cfg.TrustedProxies) > 0 {
|
||||||
@@ -58,7 +71,7 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
for _, r := range e.Router().Routes() {
|
for _, r := range e.Router().Routes() {
|
||||||
logging.GetLogger().Debug("Registered route", zap.String("method", r.Method), zap.String("path", r.Path))
|
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 {
|
func redirect(to string) echo.HandlerFunc {
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ sqlc-generate:
|
|||||||
migrate-create name:
|
migrate-create name:
|
||||||
@migrate create -ext sql -dir internal/db/migrations -seq {{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
|
build: sqlc-generate templ-build
|
||||||
@echo "Building..."
|
@echo "Building..."
|
||||||
@go build -o main cmd/main.go
|
@go build -o main cmd/main.go
|
||||||
|
|||||||
Reference in new issue
Block a user