package meta import ( "cmp" "context" "slices" "nadir/internal/module" "github.com/danielgtaylor/huma/v2" ) // ModuleInfo describes a registered module: its stable ID, display name, and // the permissions it exposes. The frontend uses this to drive navigation and // render the role/permission matrix. type ModuleInfo struct { ID string `json:"id" example:"system" doc:"Stable module identifier"` Name string `json:"name" example:"System" doc:"Human-readable module name"` Permissions []string `json:"permissions" doc:"Permissions this module exposes (never includes the \"*\" wildcard)"` } type ModulesOutput struct { Body struct { Modules []ModuleInfo `json:"modules" doc:"Registered modules, sorted by ID"` } } // Register adds the read-only module-discovery endpoint. It is intentionally // public: it exposes only the API's static shape (module IDs and permission // vocabulary), the same information already served by /openapi.json. The module // list is fixed at startup, so the response is computed once here. func Register(api huma.API, mods []module.Module) { infos := make([]ModuleInfo, 0, len(mods)) for _, m := range mods { perms := m.Permissions() ps := make([]string, len(perms)) for i, p := range perms { ps[i] = string(p) } infos = append(infos, ModuleInfo{ID: m.ID(), Name: module.Title(m.ID()), Permissions: ps}) } slices.SortFunc(infos, func(a, b ModuleInfo) int { return cmp.Compare(a.ID, b.ID) }) huma.Register(api, huma.Operation{ OperationID: "list-modules", Method: "GET", Path: "/api/_modules", Summary: "List registered modules", Description: "Returns every registered module with its ID, display name, " + "and exported permissions. Public (same static shape as /openapi.json); " + "used by the frontend for navigation and the role/permission matrix.", Tags: []string{"Meta"}, }, func(ctx context.Context, _ *struct{}) (*ModulesOutput, error) { out := &ModulesOutput{} out.Body.Modules = infos return out, nil }) }