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
60 lines
1.9 KiB
Go
60 lines
1.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"music-server/internal/backend"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type uuidInput struct {
|
|
UUID string `path:"uuid" format:"uuid" doc:"UUID of the soundtrack or song"`
|
|
}
|
|
|
|
func (in *uuidInput) id() uuid.UUID {
|
|
// The format is validated by Huma before the handler runs.
|
|
return uuid.MustParse(in.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,
|
|
}
|
|
}
|
|
|
|
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()))
|
|
})
|
|
|
|
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()))
|
|
})
|
|
}
|