Generate the OpenAPI spec with Huma instead of swag
CI / integration (push) Successful in 49s
CI / docker (push) Successful in 1m0s
CI / test (push) Successful in 1m6s
CI / publish (push) Skipped

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:
Sansan committed 2026-10-06 16:31:06 +02:00
1 parent 0ec2425f78
commit 973ebf224b
37 files changed
+1245 -5967

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ func (c *CharacterService) List() ([]string, error) {
if err != nil {
return nil, err
}
var characters []string
characters := []string{}
for _, entry := range entries {
if !entry.IsDir() && isImage(entry.Name()) {
characters = append(characters, entry.Name())
+1 -1
View File
@@ -198,7 +198,7 @@ func (m *MusicService) SongInfo(ctx context.Context) SongInfo {
// PlayedSongs returns all songs in the queue.
func (m *MusicService) PlayedSongs(ctx context.Context) []SongInfo {
var songList []SongInfo
songList := []SongInfo{}
for i, song := range m.queue {
soundtrack := m.soundtrackFor(ctx, song)
songList = append(songList, SongInfo{
+3 -3
View File
@@ -66,7 +66,7 @@ func toSoundtracks[R soundtrackRow](rows []R, err error) ([]SoundtrackWithSongs,
if err != nil {
return nil, err
}
var result []SoundtrackWithSongs
result := []SoundtrackWithSongs{}
for _, r := range rows {
row := repository.GetMostPlayedSoundtracksWithSongsRow(r)
result = append(result, SoundtrackWithSongs{
@@ -89,7 +89,7 @@ func toSongs[R songRow](rows []R, err error) ([]SongInfoForStats, error) {
if err != nil {
return nil, err
}
var result []SongInfoForStats
result := []SongInfoForStats{}
for _, r := range rows {
result = append(result, SongInfoForStats(repository.GetMostPlayedSongsWithSoundtrackRow(r)))
}
@@ -132,7 +132,7 @@ func (s *StatisticsService) NeverPlayedSoundtracks(ctx context.Context) ([]Sound
if err != nil {
return nil, err
}
var result []SoundtrackWithSongs
result := []SoundtrackWithSongs{}
for _, row := range rows {
result = append(result, SoundtrackWithSongs{
SoundtrackID: row.SoundtrackID,
+14 -2
View File
@@ -136,11 +136,23 @@ func (s *SyncService) Progress() ProgressResponse {
}
}
// Result returns the result of the last finished sync.
// Result returns the result of the last finished sync. Lists are empty, not nil, before the first sync.
func (s *SyncService) Result() SyncResponse {
s.mu.Lock()
defer s.mu.Unlock()
return s.result
return s.result.withEmptyLists()
}
func (r SyncResponse) withEmptyLists() SyncResponse {
for _, list := range []*[]string{&r.SoundtracksAdded, &r.SoundtracksReAdded, &r.SoundtracksChangedContent, &r.SoundtracksRemoved, &r.CatchedErrors} {
if *list == nil {
*list = []string{}
}
}
if r.SoundtracksChangedTitle == nil {
r.SoundtracksChangedTitle = map[string]string{}
}
return r
}
// ResetDB deletes all songs and soundtracks.
+8 -1
View File
@@ -371,7 +371,14 @@ func TestSyncStartOnlyOnce(t *testing.T) {
func TestSyncProgressBeforeFirstSync(t *testing.T) {
s := NewSyncService(fakedb.New(), t.TempDir(), nil)
assert.Equal(t, "0", s.Progress().Progress)
assert.Equal(t, SyncResponse{}, s.Result())
assert.Equal(t, SyncResponse{
SoundtracksAdded: []string{},
SoundtracksReAdded: []string{},
SoundtracksChangedTitle: map[string]string{},
SoundtracksChangedContent: []string{},
SoundtracksRemoved: []string{},
CatchedErrors: []string{},
}, s.Result(), "lists are empty, not null, so the JSON matches the API schema")
}
func TestSyncCallsOnCompleteBeforeFinishing(t *testing.T) {