Build identified record inheritance graphs
This commit is contained in:
160
internal/engine/inheritance_identified.go
Normal file
160
internal/engine/inheritance_identified.go
Normal file
@@ -0,0 +1,160 @@
|
||||
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",
|
||||
)}
|
||||
}
|
||||
Reference in New Issue
Block a user