272 lines
9.6 KiB
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
|
|
}
|