Files
ConfigKit/internal/engine/inheritance_identified.go

161 lines
5.6 KiB
Go

package engine
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
type identifiedInheritanceTemplate struct {
item *document.Node
path Path
tuple string
parent int
hasParent bool
cancelled bool
directive *document.Node
directivePath Path
}
type identifiedInheritanceGraph struct {
scope *document.Node
scopePath Path
fields []string
nodes []identifiedInheritanceTemplate
edges []dependencyEdge
byIdentity map[string]int
}
// analyzeIdentifiedInheritanceGraph validates relationships inside one
// identified sequence. It prepares a graph only; mapping-sibling expansion
// remains the active inheritance behavior until the reusable scope executor is
// activated.
func analyzeIdentifiedInheritanceGraph(sequence *document.Node, scopePath Path, identityFields []string) (*identifiedInheritanceGraph, []Diagnostic) {
if sequence == nil || sequence.Kind != document.Sequence {
var primary *Location
if sequence != nil {
location := publicLocation(sequence.Location)
primary = &location
}
return nil, []Diagnostic{{
Code: CodeSequenceInvalid, Severity: SeverityError, Path: scopePath.clone(),
Message: "identified inheritance scope must be a sequence", Primary: primary,
}}
}
fields, err := normalizeSequenceIdentityFields(identityFields)
if err != nil {
location := publicLocation(sequence.Location)
return nil, []Diagnostic{{
Code: CodeSequenceInvalid, Severity: SeverityError, Path: scopePath.clone(),
Message: "identified inheritance scope requires identity fields", Primary: &location,
}}
}
lookup, diagnostics := validateNormalizedSequenceIdentityFrom(
sequence, scopePath, fields, sequenceCombinationIdentityCodes, 0,
)
if len(diagnostics) != 0 {
return nil, diagnostics
}
graph := &identifiedInheritanceGraph{
scope: sequence, scopePath: scopePath.clone(), fields: cloneStrings(fields),
nodes: make([]identifiedInheritanceTemplate, len(sequence.Sequence)),
byIdentity: make(map[string]int, len(lookup)),
}
for tuple, record := range lookup {
graph.byIdentity[tuple] = record.index
graph.nodes[record.index] = identifiedInheritanceTemplate{
item: sequence.Sequence[record.index], path: scopePath.Index(record.index), tuple: tuple,
}
}
for index := range graph.nodes {
node := &graph.nodes[index]
directive, found := inheritanceDirective(node.item)
if !found {
continue
}
node.directive = directive.Value
node.directivePath = node.path.Key(inheritanceKey)
referenceLocation := publicLocation(directive.Value.Location)
switch directive.Value.Kind {
case document.Null:
node.cancelled = true
case document.Mapping:
reference, issues := identifiedInheritanceReference(directive.Value, node.directivePath, fields)
if len(issues) != 0 {
return nil, issues
}
parent, exists := graph.byIdentity[reference]
if !exists {
return nil, []Diagnostic{inheritanceDiagnostic(
CodeInheritanceMissing, node.directivePath, referenceLocation, nil,
"referenced record does not exist in this collection",
)}
}
node.parent = parent
node.hasParent = true
graph.edges = append(graph.edges, dependencyEdge{prerequisite: parent, dependent: index})
default:
return nil, []Diagnostic{inheritanceDiagnostic(
CodeInheritanceInvalid, node.directivePath, referenceLocation, nil,
"identified record inheritance reference must be a mapping or null",
)}
}
}
return graph, nil
}
func identifiedInheritanceReference(reference *document.Node, path Path, identityFields []string) (string, []Diagnostic) {
allowed := make(map[string]struct{}, len(identityFields))
for _, field := range identityFields {
allowed[field] = struct{}{}
}
seen := make(map[string]struct{}, len(reference.Mapping))
for _, entry := range reference.Mapping {
if _, ok := allowed[entry.Key]; !ok {
return "", []Diagnostic{inheritanceDiagnostic(
CodeInheritanceInvalid, path.Key(entry.Key), publicLocation(entry.Value.Location), nil,
"identified record inheritance reference has an unsupported field",
)}
}
seen[entry.Key] = struct{}{}
}
for _, field := range identityFields {
if _, ok := seen[field]; !ok {
return "", []Diagnostic{inheritanceDiagnostic(
CodeInheritanceInvalid, path.Key(field), publicLocation(reference.Location), nil,
"identified record inheritance reference is missing an identity field",
)}
}
}
index := newSequenceMappingIndex(reference)
tuple, _, missing, invalid := index.identity(identityFields)
if missing != "" || invalid >= 0 || invalid == -2 {
value := reference
fieldPath := path.Key(missing)
if invalid >= 0 && invalid < len(reference.Mapping) {
value = reference.Mapping[invalid].Value
fieldPath = path.Key(reference.Mapping[invalid].Key)
}
location := publicLocation(value.Location)
return "", []Diagnostic{inheritanceDiagnostic(
CodeInheritanceInvalid, fieldPath, location, nil,
"identified record inheritance identity fields must be scalar values",
)}
}
return tuple, nil
}
func (graph *identifiedInheritanceGraph) schedule() ([]int, []Diagnostic) {
keys := make([]dependencyKey, len(graph.nodes))
for index, node := range graph.nodes {
keys[index] = dependencyKey{"record", graph.scopePath.String(), node.path.String()}
}
order, cycle := scheduleDependencies(keys, graph.edges)
if len(cycle) == 0 {
return order, nil
}
parent, child := graph.nodes[cycle[0]], graph.nodes[cycle[1]]
return nil, []Diagnostic{inheritanceDiagnostic(
CodeInheritanceCycle, child.directivePath, publicLocation(child.directive.Location),
[]Location{publicLocation(parent.item.Location)},
"identified record inheritance contains a cycle",
)}
}