homelabby/internal/inventory/quality_gate.go
Mikkel Georgsen 1f9621fcaa feat(01-04): quality gate state machine, tag sync, catalog updater
- CatalogStatus type with forward-only state machine (draft→indexed→…→complete)
- Transition() enforces valid transitions, returns error with 'invalid transition' message
- ParseCatalogStatus() validates known status strings
- HardwareRecord domain type composing netbox.Device with quality gate state
- CatalogUpdater.UpdateCatalogStatus() validates transition then PatchCustomFields
- SyncTags() normalizes tags (slug form) and ensures they exist in NetBox
- normalizeTags deduplicates across case/whitespace/space-vs-hyphen variants
- ensureTag uses go-netbox v4 NewTagRequest(name, slug) / ExtrasTagsCreate
- All 12 state machine table-driven tests pass
2026-04-10 05:22:22 +00:00

65 lines
2.1 KiB
Go

package inventory
import "fmt"
// CatalogStatus represents the lifecycle stage of a cataloged hardware item.
// Stored as the catalog_status custom field value in NetBox.
type CatalogStatus string
const (
StatusDraft CatalogStatus = "draft"
StatusIndexed CatalogStatus = "indexed"
StatusNeedsResearch CatalogStatus = "needs_research"
StatusResearched CatalogStatus = "researched"
StatusComplete CatalogStatus = "complete"
)
// validTransitions defines the allowed state machine transitions.
// No backward transitions are permitted (lifecycle is forward-only).
var validTransitions = map[CatalogStatus][]CatalogStatus{
StatusDraft: {StatusIndexed},
StatusIndexed: {StatusNeedsResearch, StatusResearched},
StatusNeedsResearch: {StatusResearched},
StatusResearched: {StatusComplete},
StatusComplete: {}, // terminal — no further transitions
}
// CanTransitionTo returns true if transitioning from s to next is permitted.
func (s CatalogStatus) CanTransitionTo(next CatalogStatus) bool {
allowed, ok := validTransitions[s]
if !ok {
return false
}
for _, a := range allowed {
if a == next {
return true
}
}
return false
}
// Transition attempts to move from current to next status.
// Returns the new status on success, or an error describing the invalid transition.
func Transition(current, next CatalogStatus) (CatalogStatus, error) {
if !current.CanTransitionTo(next) {
return "", fmt.Errorf("invalid transition: %s → %s (not in valid transitions map)", current, next)
}
return next, nil
}
// ParseCatalogStatus parses a string to a CatalogStatus.
// Returns error for unknown status values.
func ParseCatalogStatus(s string) (CatalogStatus, error) {
cs := CatalogStatus(s)
if _, ok := validTransitions[cs]; ok {
return cs, nil
}
return "", fmt.Errorf("unknown catalog status: %q (valid: draft, indexed, needs_research, researched, complete)", s)
}
// AllStatuses returns all valid catalog statuses in lifecycle order.
func AllStatuses() []CatalogStatus {
return []CatalogStatus{
StatusDraft, StatusIndexed, StatusNeedsResearch, StatusResearched, StatusComplete,
}
}