Generate the OpenAPI spec with Huma instead of swag
The spec is now built from the same calls that register the routes, so it
can't drift from the router like the swag comments did (all /api/v1
routes were missing).
- /api/v1 endpoints are typed Huma operations; the spec is OpenAPI 3.1 at
/openapi.json (3.0 at /openapi-3.0.json, YAML too), docs at /docs.
/openapi and /swagger/* redirect there
- Bearer token security is declared per operation; token creation and
the new GET /api/v1/health are public
- Legacy routes use the same handlers, are hidden from the spec and keep
the Deprecation header
- /sync/reset is hidden (debug only); the /sync/new, /sync/quick and
/sync/new/full aliases are removed (use /sync and /sync/full)
- Invalid parameters return 422 (validation); every error is
{"error": "..."}; empty lists are [] instead of null
- /sync/progress is documented as progress or result (oneOf)
- Tests validate the spec and check real responses of every documented
operation against it (kin-openapi)
- Removed swag annotations, cmd/docs, echo-swagger and the swag tool
This commit is contained in:
1 parent
0ec2425f78
commit
973ebf224b
37 files changed
+1245
-5967
No files matched your search
@@ -1,111 +1,59 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"music-server/internal/backend"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
type LibraryHandler struct {
|
||||
library *backend.LibraryService
|
||||
type uuidInput struct {
|
||||
UUID string `path:"uuid" format:"uuid" doc:"UUID of the soundtrack or song"`
|
||||
}
|
||||
|
||||
func NewLibraryHandler(library *backend.LibraryService) *LibraryHandler {
|
||||
return &LibraryHandler{library: library}
|
||||
func (in *uuidInput) id() uuid.UUID {
|
||||
// The format is validated by Huma before the handler runs.
|
||||
return uuid.MustParse(in.UUID)
|
||||
}
|
||||
|
||||
// uuidParam parses the :uuid path parameter.
|
||||
func uuidParam(ctx *echo.Context) (uuid.UUID, error) {
|
||||
id, err := uuid.Parse(ctx.Param("uuid"))
|
||||
if err != nil {
|
||||
return uuid.Nil, echo.NewHTTPError(http.StatusBadRequest, "invalid uuid")
|
||||
func (s *Server) registerLibrary(a apis) {
|
||||
l := s.library
|
||||
op := func(id, path, summary, description string, errs ...int) huma.Operation {
|
||||
return huma.Operation{
|
||||
OperationID: id,
|
||||
Method: http.MethodGet,
|
||||
Path: path,
|
||||
Summary: summary,
|
||||
Description: description,
|
||||
Tags: []string{"library"},
|
||||
Errors: errs,
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetSoundtracks godoc
|
||||
//
|
||||
// @Summary List soundtracks
|
||||
// @Description Returns all soundtracks with their UUIDs, sorted by name
|
||||
// @Tags library
|
||||
// @Produce json
|
||||
// @Success 200 {array} backend.Soundtrack
|
||||
// @Router /api/v1/soundtracks [get]
|
||||
func (l *LibraryHandler) GetSoundtracks(ctx *echo.Context) error {
|
||||
soundtracks, err := l.library.Soundtracks(ctx.Request().Context())
|
||||
if err != nil {
|
||||
return songError(err)
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, soundtracks)
|
||||
}
|
||||
register(s, a, route{op: op("list-soundtracks", "/soundtracks", "List soundtracks",
|
||||
"Returns all soundtracks, sorted by name.")},
|
||||
func(ctx context.Context, _ *struct{}) (*jsonBody[[]backend.Soundtrack], error) {
|
||||
return ok(l.Soundtracks(ctx))
|
||||
})
|
||||
register(s, a, route{op: op("get-soundtrack", "/soundtracks/{uuid}", "Get soundtrack",
|
||||
"Returns a soundtrack with its songs, sorted by name.", http.StatusNotFound)},
|
||||
func(ctx context.Context, in *uuidInput) (*jsonBody[backend.SoundtrackWithSongList], error) {
|
||||
return ok(l.Soundtrack(ctx, in.id()))
|
||||
})
|
||||
register(s, a, route{op: op("get-song", "/songs/{uuid}", "Get song",
|
||||
"Returns information about a song.", http.StatusNotFound)},
|
||||
func(ctx context.Context, in *uuidInput) (*jsonBody[backend.Song], error) {
|
||||
return ok(l.Song(ctx, in.id()))
|
||||
})
|
||||
|
||||
// GetSoundtrack godoc
|
||||
//
|
||||
// @Summary Get soundtrack
|
||||
// @Description Returns a soundtrack with its songs
|
||||
// @Tags library
|
||||
// @Produce json
|
||||
// @Param uuid path string true "Soundtrack UUID"
|
||||
// @Success 200 {object} backend.SoundtrackWithSongList
|
||||
// @Failure 400 {string} string "invalid uuid"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /api/v1/soundtracks/{uuid} [get]
|
||||
func (l *LibraryHandler) GetSoundtrack(ctx *echo.Context) error {
|
||||
id, err := uuidParam(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
soundtrack, err := l.library.Soundtrack(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
return songError(err)
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, soundtrack)
|
||||
}
|
||||
|
||||
// GetSong godoc
|
||||
//
|
||||
// @Summary Get song
|
||||
// @Description Returns information about a song
|
||||
// @Tags library
|
||||
// @Produce json
|
||||
// @Param uuid path string true "Song UUID"
|
||||
// @Success 200 {object} backend.Song
|
||||
// @Failure 400 {string} string "invalid uuid"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /api/v1/songs/{uuid} [get]
|
||||
func (l *LibraryHandler) GetSong(ctx *echo.Context) error {
|
||||
id, err := uuidParam(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
song, err := l.library.Song(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
return songError(err)
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, song)
|
||||
}
|
||||
|
||||
// GetSongFile godoc
|
||||
//
|
||||
// @Summary Stream song
|
||||
// @Description Returns the audio file of a song. Supports Range requests.
|
||||
// @Tags library
|
||||
// @Produce audio/mpeg
|
||||
// @Param uuid path string true "Song UUID"
|
||||
// @Success 200 {file} file
|
||||
// @Success 206 {file} file
|
||||
// @Failure 400 {string} string "invalid uuid"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 423 {string} string "Syncing is in progress"
|
||||
// @Router /api/v1/songs/{uuid}/file [get]
|
||||
func (l *LibraryHandler) GetSongFile(ctx *echo.Context) error {
|
||||
id, err := uuidParam(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := l.library.SongPath(ctx.Request().Context(), id)
|
||||
return serveSong(ctx, path, err)
|
||||
file := op("get-song-file", "/songs/{uuid}/file", "Stream song",
|
||||
"Returns the audio file of a song.", http.StatusNotFound)
|
||||
file.Responses = fileResponses("The song (MP3)", audioMPEG)
|
||||
register(s, a, route{op: file, locked: true},
|
||||
func(ctx context.Context, in *uuidInput) (*huma.StreamResponse, error) {
|
||||
return streamSong(l.SongPath(ctx, in.id()))
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user