191 lines
5.8 KiB
Go
191 lines
5.8 KiB
Go
package configkit
|
|
|
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
|
|
// composedDocument is the private handoff between source assembly and later
|
|
// precedence layers. Claims remain ordered by source and declaration traversal;
|
|
// origins retain every source that contributes to a composite mapping.
|
|
type composedDocument struct {
|
|
root *document.Node
|
|
claims []compositionClaim
|
|
origins map[string]Origin
|
|
overlays []overlayRecord
|
|
applications []historyApplication
|
|
inputBytes int64
|
|
inputNodes int
|
|
}
|
|
|
|
// cloneComposedDocument detaches mutable composition and provenance state;
|
|
// history application trees are immutable and retain their existing sharing.
|
|
func cloneComposedDocument(base *composedDocument) *composedDocument {
|
|
return &composedDocument{
|
|
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
|
|
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
|
|
applications: cloneHistoryApplications(base.applications),
|
|
inputBytes: base.inputBytes, inputNodes: base.inputNodes,
|
|
}
|
|
}
|
|
|
|
type compositionClaim struct {
|
|
path Path
|
|
kind document.Kind
|
|
location Location
|
|
}
|
|
|
|
type compositionClaims struct {
|
|
ordered []compositionClaim
|
|
byPath map[string][]compositionClaim
|
|
}
|
|
|
|
// composeDocuments recursively joins disjoint mapping entries. Source order
|
|
// controls only output traversal and diagnostics: every non-mapping overlap is
|
|
// rejected before a result is assembled.
|
|
func composeDocuments(inputs ...*document.Node) (*composedDocument, []Diagnostic) {
|
|
if len(inputs) == 0 {
|
|
return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires at least one document", nil)}
|
|
}
|
|
|
|
collected := compositionClaims{byPath: make(map[string][]compositionClaim)}
|
|
for _, input := range inputs {
|
|
if input == nil || input.Kind != document.Mapping {
|
|
var location *Location
|
|
if input != nil {
|
|
converted := publicLocation(input.Location)
|
|
location = &converted
|
|
}
|
|
return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires mapping document roots", location)}
|
|
}
|
|
collected.append(input, Path{})
|
|
}
|
|
|
|
for _, claim := range collected.ordered {
|
|
claims := collected.byPath[claim.path.String()]
|
|
if len(claims) < 2 || allMappingClaims(claims) {
|
|
continue
|
|
}
|
|
return nil, []Diagnostic{compositionConflictDiagnostic(claim.path, claims)}
|
|
}
|
|
|
|
root := document.Clone(inputs[0])
|
|
for _, input := range inputs[1:] {
|
|
mergeDisjointMappings(root, input)
|
|
}
|
|
applications := make([]historyApplication, 0, len(inputs))
|
|
for _, input := range inputs {
|
|
applications = append(applications, historyApplication{layer: LayerBase, value: input})
|
|
}
|
|
return &composedDocument{
|
|
root: root,
|
|
claims: cloneCompositionClaims(collected.ordered),
|
|
origins: compositionOrigins(collected.ordered),
|
|
applications: applications,
|
|
}, nil
|
|
}
|
|
|
|
func (c *compositionClaims) append(node *document.Node, path Path) {
|
|
claim := compositionClaim{
|
|
path: path.clone(),
|
|
kind: node.Kind,
|
|
location: publicLocation(node.Location),
|
|
}
|
|
c.ordered = append(c.ordered, claim)
|
|
pointer := path.String()
|
|
c.byPath[pointer] = append(c.byPath[pointer], claim)
|
|
|
|
if node.Kind != document.Mapping {
|
|
return
|
|
}
|
|
for _, entry := range node.Mapping {
|
|
c.append(entry.Value, path.Key(entry.Key))
|
|
}
|
|
}
|
|
|
|
func allMappingClaims(claims []compositionClaim) bool {
|
|
for _, claim := range claims {
|
|
if claim.kind != document.Mapping {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func mergeDisjointMappings(target, incoming *document.Node) {
|
|
for _, incomingEntry := range incoming.Mapping {
|
|
index := documentEntryIndex(target.Mapping, incomingEntry.Key)
|
|
if index < 0 {
|
|
cloned := incomingEntry
|
|
cloned.Value = document.Clone(incomingEntry.Value)
|
|
target.Mapping = append(target.Mapping, cloned)
|
|
continue
|
|
}
|
|
// Claim validation guarantees that the only remaining overlap is a
|
|
// mapping pair. Empty mappings naturally contribute no child entries.
|
|
mergeDisjointMappings(target.Mapping[index].Value, incomingEntry.Value)
|
|
}
|
|
}
|
|
|
|
func documentEntryIndex(entries []document.Entry, key string) int {
|
|
for index := range entries {
|
|
if entries[index].Key == key {
|
|
return index
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func compositionOrigins(claims []compositionClaim) map[string]Origin {
|
|
origins := make(map[string]Origin, len(claims))
|
|
for _, claim := range claims {
|
|
pointer := claim.path.String()
|
|
origin := origins[pointer]
|
|
origin.Layer = LayerBase
|
|
origin.Locations = append(origin.Locations, claim.location)
|
|
origins[pointer] = origin
|
|
}
|
|
return origins
|
|
}
|
|
|
|
func applyCompositionOrigins(decoded *decodedConfiguration, origins map[string]Origin) {
|
|
for pointer, origin := range origins {
|
|
if _, present := decoded.presence[pointer]; present {
|
|
decoded.origins[pointer] = cloneOrigin(origin)
|
|
}
|
|
}
|
|
}
|
|
|
|
func cloneCompositionClaims(claims []compositionClaim) []compositionClaim {
|
|
cloned := make([]compositionClaim, len(claims))
|
|
for index, claim := range claims {
|
|
cloned[index] = claim
|
|
cloned[index].path = claim.path.clone()
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func compositionInvalidDiagnostic(message string, location *Location) Diagnostic {
|
|
return Diagnostic{
|
|
Code: CodeComposeInvalid, Severity: SeverityError, Message: message,
|
|
Primary: cloneLocation(location),
|
|
}
|
|
}
|
|
|
|
func compositionConflictDiagnostic(path Path, claims []compositionClaim) Diagnostic {
|
|
primary := claims[0].location
|
|
related := make([]Location, 0, len(claims)-1)
|
|
for _, claim := range claims[1:] {
|
|
related = append(related, claim.location)
|
|
}
|
|
return Diagnostic{
|
|
Code: CodeComposeConflict,
|
|
Severity: SeverityError,
|
|
Path: path.clone(),
|
|
Message: "configuration path is defined by multiple sources",
|
|
Primary: &primary,
|
|
Related: related,
|
|
}
|
|
}
|
|
|
|
func publicLocation(location document.Location) Location {
|
|
return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}
|
|
}
|