feat(17-01): menu item type registry and dependent dropdown binding

- pact: HasMenuItemTypes, MenuItemType, MenuItemReference, MenuItemResolve,
  ResolvedMenuItem, MenuItemAlternate, DependentOptionsProvider,
  DependentOptionSet; Option gains Depth (omitempty)
- signpost (new module): boot-time registry with fail-loud key and pattern
  checks, layered URL patterns incl. PatternFrom for list types, host
  overrides at menu_items.patterns, Resolve and ReferenceOptions
- party: signpost.Activate after postcard, before Boot
- cabana: dependsOn on dropdowns (boot checks, controller-only), GET
  fields/{field}/dependent-options with swag, inventories, conformance case
- companions fixture, TestSignpostSmoke, TestCompanionsDependentSmoke, docs
This commit is contained in:
2026-10-12 15:09:14 +02:00
parent 9016e2a6fd
commit 0cdff4c258
29 changed files with 1752 additions and 14 deletions

View File

@@ -438,7 +438,9 @@ func constrainFileID(g pact.Router) {
func constrainNested(g pact.Router) {
constrainController(g)
g.Where("segment", "[A-Za-z_][A-Za-z0-9_]*")
g.Where("name", "[A-Za-z_][A-Za-z0-9_]*")
// name admits a hyphen for the literal dependent-options segment; a
// relation or file field name with one matches nothing and answers 404.
g.Where("name", "[A-Za-z_][A-Za-z0-9_-]*")
}
// nestedGet serves the logical routes
@@ -446,6 +448,7 @@ func constrainNested(g pact.Router) {
// GET /{vendor}/{plugin}/{controller}/{id}/relations/{name}
// GET /{vendor}/{plugin}/{controller}/{id}/files/{field}
// GET /{vendor}/{plugin}/{controller}/fields/{field}/options
// GET /{vendor}/{plugin}/{controller}/fields/{field}/dependent-options
// GET /{vendor}/{plugin}/{controller}/filters/{scope}/options
//
// A numeric id never equals a literal segment, so the dispatch is unambiguous.
@@ -456,6 +459,9 @@ func (s *service) nestedGet(w http.ResponseWriter, r *http.Request) {
case id == "fields" && name == "options":
r.SetPathValue("field", segment)
s.fieldOptions(w, r)
case id == "fields" && name == "dependent-options":
r.SetPathValue("field", segment)
s.dependentOptionsRoute(w, r)
case id == "filters" && name == "options":
r.SetPathValue("scope", segment)
s.filterOptions(w, r)