From b04d4d269c17159384f6a13012990774fd5b8f69 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 2 Jun 2026 14:11:03 +0200 Subject: [PATCH] refactor(api/v2): self-register resource routes via init() registry Previously every new v2 resource appended an explicit RegisterXRoutes call (and the EnableAutoPatch line had to stay last) in registerAPIRoutesV2 in routes.go, causing recurring merge conflicts across in-flight PRs. Resources now self-register: each resource file calls AddRouteRegistrar from an init(), and registerAPIRoutesV2 just calls apiv2.RegisterAll, which runs every registrar and then EnableAutoPatch. New resources touch zero shared lines. --- pkg/routes/api/v2/admin_projects.go | 2 ++ pkg/routes/api/v2/avatar.go | 2 ++ pkg/routes/api/v2/avatar_upload.go | 2 ++ pkg/routes/api/v2/labels.go | 2 ++ pkg/routes/api/v2/project_views.go | 2 ++ pkg/routes/api/v2/registry.go | 39 +++++++++++++++++++++++++++++ pkg/routes/api/v2/task_duplicate.go | 2 ++ pkg/routes/routes.go | 13 ++-------- 8 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 pkg/routes/api/v2/registry.go diff --git a/pkg/routes/api/v2/admin_projects.go b/pkg/routes/api/v2/admin_projects.go index 8e7b51d3d..1fd52af63 100644 --- a/pkg/routes/api/v2/admin_projects.go +++ b/pkg/routes/api/v2/admin_projects.go @@ -31,6 +31,8 @@ type adminProjectListBody struct { Body Paginated[*models.Project] } +func init() { AddRouteRegistrar(RegisterAdminProjectRoutes) } + // Permissions are enforced by the gateV2AdminRoutes path middleware, not per-handler. func RegisterAdminProjectRoutes(api huma.API) { tags := []string{"admin"} diff --git a/pkg/routes/api/v2/avatar.go b/pkg/routes/api/v2/avatar.go index c2bd4c9bd..f8bf6326c 100644 --- a/pkg/routes/api/v2/avatar.go +++ b/pkg/routes/api/v2/avatar.go @@ -39,6 +39,8 @@ type avatarInput struct { Size int64 `query:"size" default:"250" minimum:"1" doc:"Desired avatar edge length in pixels. Clamped to the server's configured maximum if larger; providers that render fixed-size images may ignore it."` } +func init() { AddRouteRegistrar(RegisterAvatarRoutes) } + // RegisterAvatarRoutes wires the avatar binary endpoint onto the Huma API. func RegisterAvatarRoutes(api huma.API) { Register(api, huma.Operation{ diff --git a/pkg/routes/api/v2/avatar_upload.go b/pkg/routes/api/v2/avatar_upload.go index 6794cdb81..e2112f001 100644 --- a/pkg/routes/api/v2/avatar_upload.go +++ b/pkg/routes/api/v2/avatar_upload.go @@ -41,6 +41,8 @@ type avatarUploadBody struct { Body *models.Message } +func init() { AddRouteRegistrar(RegisterAvatarUploadRoutes) } + func RegisterAvatarUploadRoutes(api huma.API) { tags := []string{"user"} diff --git a/pkg/routes/api/v2/labels.go b/pkg/routes/api/v2/labels.go index 541ab1bb2..ca37af903 100644 --- a/pkg/routes/api/v2/labels.go +++ b/pkg/routes/api/v2/labels.go @@ -35,6 +35,8 @@ type labelListBody struct { Body Paginated[*models.LabelWithTaskID] } +func init() { AddRouteRegistrar(RegisterLabelRoutes) } + // RegisterLabelRoutes wires Label CRUD onto the Huma API. func RegisterLabelRoutes(api huma.API) { tags := []string{"labels"} diff --git a/pkg/routes/api/v2/project_views.go b/pkg/routes/api/v2/project_views.go index 907d305c9..c88bbc28a 100644 --- a/pkg/routes/api/v2/project_views.go +++ b/pkg/routes/api/v2/project_views.go @@ -34,6 +34,8 @@ type projectViewListBody struct { Body Paginated[*models.ProjectView] } +func init() { AddRouteRegistrar(RegisterProjectViewRoutes) } + // RegisterProjectViewRoutes wires the nested ProjectView CRUD onto the Huma API. // Every operation binds two path params: {project} → ProjectID and {view} → ID. // This is the reference shape every nested sub-resource copies. diff --git a/pkg/routes/api/v2/registry.go b/pkg/routes/api/v2/registry.go new file mode 100644 index 000000000..907f2ea22 --- /dev/null +++ b/pkg/routes/api/v2/registry.go @@ -0,0 +1,39 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package apiv2 + +import "github.com/danielgtaylor/huma/v2" + +var routeRegistrars []func(huma.API) + +// AddRouteRegistrar records a resource's route-registration function. Each +// resource file calls this from an init() so new resources never touch the +// central wiring. +func AddRouteRegistrar(f func(huma.API)) { + routeRegistrars = append(routeRegistrars, f) +} + +// RegisterAll runs every registrar collected via AddRouteRegistrar, then +// enables AutoPatch. Registrars run in init() order (filename order across the +// package); the order they register routes in is irrelevant. AutoPatch runs +// last so it can synthesise PATCH counterparts for all GET + PUT pairs. +func RegisterAll(api huma.API) { + for _, r := range routeRegistrars { + r(api) + } + EnableAutoPatch(api) +} diff --git a/pkg/routes/api/v2/task_duplicate.go b/pkg/routes/api/v2/task_duplicate.go index 3340994bd..83d3209b0 100644 --- a/pkg/routes/api/v2/task_duplicate.go +++ b/pkg/routes/api/v2/task_duplicate.go @@ -27,6 +27,8 @@ import ( ) // RegisterTaskDuplicateRoutes wires the task-duplicate action onto the Huma API. +func init() { AddRouteRegistrar(RegisterTaskDuplicateRoutes) } + // TaskDuplicate is a CRUDable Create, so the handler reuses handler.DoCreate // (its CanCreate enforces read-source + write-project); the only custom part is // taking TaskID from the path rather than a request body. diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 3fca382a1..159994724 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -415,17 +415,8 @@ func registerAPIRoutesV2(e *echo.Echo, a *echo.Group) { a.GET("/docs", apiv2.ScalarUI) a.GET("/docs/scalar.standalone.js", apiv2.ScalarJS) - // Resource registrations. - apiv2.RegisterLabelRoutes(api) - apiv2.RegisterTaskDuplicateRoutes(api) - apiv2.RegisterProjectViewRoutes(api) - apiv2.RegisterAdminProjectRoutes(api) - apiv2.RegisterAvatarRoutes(api) - apiv2.RegisterAvatarUploadRoutes(api) - - // AutoPatch must run AFTER all GET/PUT pairs are registered so it can - // synthesize their PATCH counterparts. - apiv2.EnableAutoPatch(api) + // Resources self-register via init(); RegisterAll runs them all + AutoPatch. + apiv2.RegisterAll(api) } func registerAPIRoutes(a *echo.Group) {