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
100 lines
3.9 KiB
Go
100 lines
3.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"music-server/internal/backend"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
)
|
|
|
|
type characterInput struct {
|
|
Name string `query:"name" required:"true" doc:"File name of the character image, from the character list"`
|
|
}
|
|
|
|
type redirectOutput struct {
|
|
Status int
|
|
Location string `header:"Location" doc:"Download URL of the asset"`
|
|
}
|
|
|
|
type healthOutput struct {
|
|
Body map[string]string
|
|
}
|
|
|
|
func (s *Server) registerMisc(a apis) {
|
|
get := func(id, path, tag, summary, description string, errs ...int) huma.Operation {
|
|
return huma.Operation{
|
|
OperationID: id,
|
|
Method: http.MethodGet,
|
|
Path: path,
|
|
Summary: summary,
|
|
Description: description,
|
|
Tags: []string{tag},
|
|
Errors: errs,
|
|
}
|
|
}
|
|
|
|
register(s, a, route{op: get("get-health", "/health", "server", "Health check",
|
|
"Returns {\"status\": \"up\"} when the server and database work, otherwise status \"down\" and an error."),
|
|
public: true, legacyPath: "/health"},
|
|
func(ctx context.Context, _ *struct{}) (*healthOutput, error) {
|
|
return &healthOutput{Body: s.db.Health(ctx)}, nil
|
|
})
|
|
register(s, a, route{op: get("get-version", "/version", "server", "Latest server version",
|
|
"Returns the server version and its changelog."), legacyPath: "/version"},
|
|
func(ctx context.Context, _ *struct{}) (*jsonBody[backend.VersionData], error) {
|
|
return ok(backend.GetLatestVersion(), nil)
|
|
})
|
|
register(s, a, route{op: get("get-version-history", "/version/history", "server", "Version history",
|
|
"Returns all server versions with their changelogs, newest first."), legacyPath: "/version/history"},
|
|
func(ctx context.Context, _ *struct{}) (*jsonBody[[]backend.VersionData], error) {
|
|
return ok(backend.GetVersionHistory(), nil)
|
|
})
|
|
|
|
register(s, a, route{op: get("list-characters", "/character/list", "characters", "List characters",
|
|
"Returns the file names of all character images."), legacyPath: "/characters"},
|
|
func(ctx context.Context, _ *struct{}) (*jsonBody[[]string], error) {
|
|
return ok(s.characters.List())
|
|
})
|
|
image := get("get-character", "/character", "characters", "Character image",
|
|
"Returns a character image.", http.StatusBadRequest, http.StatusNotFound)
|
|
image.Responses = fileResponses("The image", "image/png", "image/jpeg")
|
|
register(s, a, route{op: image, legacyPath: "/character"},
|
|
func(ctx context.Context, in *characterInput) (*huma.StreamResponse, error) {
|
|
f, err := s.characters.Open(in.Name)
|
|
if err != nil {
|
|
return nil, apiError(err)
|
|
}
|
|
return streamOpenFile(f, "", false)
|
|
})
|
|
|
|
r := s.releases
|
|
register(s, a, route{op: get("get-latest-client-version", "/download", "download", "Latest client version",
|
|
"Returns the name of the latest MusicPlayer release.", http.StatusBadGateway), legacyPath: "/download"},
|
|
func(ctx context.Context, _ *struct{}) (*jsonBody[string], error) {
|
|
return ok(r.LatestVersion(ctx))
|
|
})
|
|
register(s, a, route{op: get("list-client-assets", "/download/list", "download", "Latest client assets",
|
|
"Returns the file names of the latest MusicPlayer release.", http.StatusBadGateway), legacyPath: "/download/list"},
|
|
func(ctx context.Context, _ *struct{}) (*jsonBody[[]string], error) {
|
|
return ok(r.AssetNames(ctx))
|
|
})
|
|
download := func(id, path, platform, suffix string) {
|
|
op := get(id, path, "download", "Download the latest client for "+platform,
|
|
"Redirects to the download URL of the latest MusicPlayer release for "+platform+".",
|
|
http.StatusNotFound, http.StatusBadGateway)
|
|
op.DefaultStatus = http.StatusFound
|
|
register(s, a, route{op: op, legacyPath: path},
|
|
func(ctx context.Context, _ *struct{}) (*redirectOutput, error) {
|
|
url, err := r.AssetURL(ctx, suffix)
|
|
if err != nil {
|
|
return nil, apiError(err)
|
|
}
|
|
return &redirectOutput{Status: http.StatusFound, Location: url}, nil
|
|
})
|
|
}
|
|
download("download-client-windows", "/download/windows", "Windows", ".exe")
|
|
download("download-client-linux", "/download/linux", "Linux", ".x86_64")
|
|
}
|