Files
ttstd-infinite-canvas/backend/internal/canvas/capability/registry.go

272 lines
9.6 KiB
Go

package capability
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
)
const SetVersion = "canvas-capabilities/v4"
type Registry struct{ descriptors map[string]Descriptor }
func NewRegistry(descriptors []Descriptor) (*Registry, error) {
items := make(map[string]Descriptor, len(descriptors))
generationModes := make(map[string]string, len(descriptors))
for _, descriptor := range descriptors {
descriptor.Type = normalizeType(descriptor.Type)
descriptor.Version = strings.TrimSpace(descriptor.Version)
descriptor.Label = strings.TrimSpace(descriptor.Label)
descriptor.Purpose = strings.TrimSpace(descriptor.Purpose)
descriptor.GoodFor = normalizeFields(descriptor.GoodFor)
descriptor.NotIdealFor = normalizeFields(descriptor.NotIdealFor)
descriptor.Tradeoffs = normalizeFields(descriptor.Tradeoffs)
descriptor.Actions = normalizeFields(descriptor.Actions)
if descriptor.Type == "" || descriptor.Version == "" || descriptor.Label == "" || descriptor.DefaultWidth <= 0 || descriptor.DefaultHeight <= 0 {
return nil, fmt.Errorf("invalid canvas capability descriptor %q", descriptor.Type)
}
inputKind, err := normalizeInputKind(descriptor.InputKind)
if err != nil {
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
}
descriptor.InputKind = inputKind
generationMode, err := normalizeGenerationMode(descriptor.GenerationMode)
if err != nil {
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
}
descriptor.GenerationMode = generationMode
if generationMode != "" {
if existing, exists := generationModes[generationMode]; exists {
return nil, fmt.Errorf("generation mode %q is registered by both %q and %q", generationMode, existing, descriptor.Type)
}
generationModes[generationMode] = descriptor.Type
}
projectionKind, err := normalizeProjectionKind(descriptor.ProjectionKind)
if err != nil {
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
}
descriptor.ProjectionKind = projectionKind
descriptor.ProjectionField = strings.TrimSpace(descriptor.ProjectionField)
if (descriptor.ProjectionKind == "") != (descriptor.ProjectionField == "") {
return nil, fmt.Errorf("capability %q must declare projection kind and field together", descriptor.Type)
}
if descriptor.ProjectionField != "" {
if err := validatePatchPath(descriptor.ProjectionField); err != nil {
return nil, fmt.Errorf("capability %q has invalid projection field: %w", descriptor.Type, err)
}
}
if descriptor.Connection.MaxInputCount < 0 {
return nil, fmt.Errorf("capability %q has invalid max input count", descriptor.Type)
}
accepted, err := normalizeConnectionKinds(descriptor.Connection.AcceptedInputKinds)
if err != nil {
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
}
rejected, err := normalizeConnectionKinds(descriptor.Connection.RejectedInputKinds)
if err != nil {
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
}
rejectedSet := make(map[string]struct{}, len(rejected))
for _, kind := range rejected {
rejectedSet[kind] = struct{}{}
}
for _, kind := range accepted {
if _, conflict := rejectedSet[kind]; conflict {
return nil, fmt.Errorf("capability %q accepts and rejects input kind %q", descriptor.Type, kind)
}
}
if descriptor.Connection.CanSource && descriptor.InputKind == "" {
return nil, fmt.Errorf("capability %q can source without an input kind", descriptor.Type)
}
descriptor.Connection.AcceptedInputKinds = accepted
descriptor.Connection.RejectedInputKinds = rejected
descriptor.PatchFields, err = normalizePatchFields(descriptor.PatchFields)
if err != nil {
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
}
if descriptor.CanUpdate != (len(descriptor.PatchFields) > 0) {
return nil, fmt.Errorf("capability %q must declare both canUpdate and patch fields, or neither", descriptor.Type)
}
descriptor.SummaryFields = normalizeFields(descriptor.SummaryFields)
descriptor.DetailFields = normalizeFields(descriptor.DetailFields)
if _, exists := items[descriptor.Type]; exists {
return nil, fmt.Errorf("duplicate canvas capability %q", descriptor.Type)
}
items[descriptor.Type] = cloneDescriptor(descriptor)
}
return &Registry{descriptors: items}, nil
}
func (r *Registry) Resolve(nodeType string) (Descriptor, bool) {
if r == nil {
return Descriptor{}, false
}
d, ok := r.descriptors[normalizeType(nodeType)]
if !ok {
return Descriptor{}, false
}
d = cloneDescriptor(d)
return d, ok
}
func (r *Registry) List() []Descriptor {
if r == nil {
return nil
}
out := make([]Descriptor, 0, len(r.descriptors))
for _, d := range r.descriptors {
out = append(out, cloneDescriptor(d))
}
sort.Slice(out, func(i, j int) bool { return out[i].Type < out[j].Type })
return out
}
func (r *Registry) Types() []string {
items := r.List()
out := make([]string, 0, len(items))
for _, d := range items {
out = append(out, d.Type)
}
return out
}
func (r *Registry) GenerationModeNames() []string {
seen := map[string]struct{}{}
for _, d := range r.List() {
if d.GenerationMode != "" {
seen[d.GenerationMode] = struct{}{}
}
}
out := make([]string, 0, len(seen))
for mode := range seen {
out = append(out, mode)
}
sort.Strings(out)
return out
}
func (r *Registry) SupportsGenerationMode(mode string) bool {
_, ok := r.ResolveGenerationMode(mode)
return ok
}
func (r *Registry) ResolveGenerationMode(mode string) (Descriptor, bool) {
mode = normalizeType(mode)
if mode == "" || r == nil {
return Descriptor{}, false
}
for _, descriptor := range r.descriptors {
if descriptor.GenerationMode == mode {
return cloneDescriptor(descriptor), true
}
}
return Descriptor{}, false
}
func (r *Registry) Hash() string {
data, _ := json.Marshal(registryHashItems(r.List()))
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func normalizeFields(fields []string) []string {
seen := make(map[string]struct{}, len(fields))
out := make([]string, 0, len(fields))
for _, field := range fields {
field = strings.TrimSpace(field)
if field == "" {
continue
}
if _, exists := seen[field]; exists {
continue
}
seen[field] = struct{}{}
out = append(out, field)
}
return out
}
func cloneDescriptor(descriptor Descriptor) Descriptor {
descriptor.Connection.AcceptedInputKinds = append([]string(nil), descriptor.Connection.AcceptedInputKinds...)
descriptor.Connection.RejectedInputKinds = append([]string(nil), descriptor.Connection.RejectedInputKinds...)
descriptor.GoodFor = append([]string(nil), descriptor.GoodFor...)
descriptor.NotIdealFor = append([]string(nil), descriptor.NotIdealFor...)
descriptor.Tradeoffs = append([]string(nil), descriptor.Tradeoffs...)
descriptor.Actions = append([]string(nil), descriptor.Actions...)
descriptor.SummaryFields = append([]string(nil), descriptor.SummaryFields...)
descriptor.DetailFields = append([]string(nil), descriptor.DetailFields...)
patchFields := make(map[string]PatchField, len(descriptor.PatchFields))
for key, field := range descriptor.PatchFields {
patchFields[key] = field
}
descriptor.PatchFields = patchFields
return descriptor
}
type hashDescriptor struct {
Type string
Version string
Label string
Purpose string
GoodFor []string
NotIdealFor []string
Tradeoffs []string
Actions []string
DefaultWidth float64
DefaultHeight float64
InputKind string
GenerationMode string
Connection hashConnectionPolicy
CanUpdate bool
SummaryFields []string
DetailFields []string
ProjectionKind string
ProjectionField string
PatchFields []hashPatchField
}
type hashConnectionPolicy struct {
CanSource bool
CanTarget bool
CanReference bool
AcceptedInputKinds []string
RejectedInputKinds []string
MaxInputCount int
}
type hashPatchField struct {
Key string
Path string
Kind string
Label string
Order int
Description string
MaxRunes int
}
func registryHashItems(descriptors []Descriptor) []hashDescriptor {
items := make([]hashDescriptor, 0, len(descriptors))
for _, descriptor := range descriptors {
keys := make([]string, 0, len(descriptor.PatchFields))
for key := range descriptor.PatchFields {
keys = append(keys, key)
}
sort.Strings(keys)
patchFields := make([]hashPatchField, 0, len(keys))
for _, key := range keys {
field := descriptor.PatchFields[key]
patchFields = append(patchFields, hashPatchField{Key: key, Path: field.Path, Kind: field.Kind, Label: field.Label, Order: field.Order, Description: field.Description, MaxRunes: field.MaxRunes})
}
items = append(items, hashDescriptor{
Type: descriptor.Type, Version: descriptor.Version, Label: descriptor.Label,
Purpose: descriptor.Purpose, GoodFor: descriptor.GoodFor, NotIdealFor: descriptor.NotIdealFor,
Tradeoffs: descriptor.Tradeoffs, Actions: descriptor.Actions,
DefaultWidth: descriptor.DefaultWidth, DefaultHeight: descriptor.DefaultHeight,
InputKind: descriptor.InputKind, GenerationMode: descriptor.GenerationMode,
ProjectionKind: descriptor.ProjectionKind, ProjectionField: descriptor.ProjectionField,
Connection: hashConnectionPolicy{CanSource: descriptor.Connection.CanSource, CanTarget: descriptor.Connection.CanTarget, CanReference: descriptor.Connection.CanReference, AcceptedInputKinds: descriptor.Connection.AcceptedInputKinds, RejectedInputKinds: descriptor.Connection.RejectedInputKinds, MaxInputCount: descriptor.Connection.MaxInputCount},
CanUpdate: descriptor.CanUpdate, SummaryFields: descriptor.SummaryFields, DetailFields: descriptor.DetailFields, PatchFields: patchFields,
})
}
return items
}