Files
configkit/internal/engine/metadata.go

170 lines
5.3 KiB
Go

package engine
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
// Metadata provides detached effective presence/origin and chronological history
// queries for a load. Its zero value contains no paths.
type Metadata struct {
effectivePaths map[string]Path
fields map[string]FieldInfo
historyBuilder *historyBuilder
historyModel *historyModel
}
// Lookup returns information for an effective path. Unknown and invalid paths
// return false.
func (m Metadata) Lookup(path Path) (FieldInfo, bool) {
info, ok := m.effectiveField(path)
if !ok {
return FieldInfo{}, false
}
detached := cloneFieldInfo(info)
detached.Origin.Inheritance = projectInheritanceSteps(detached.Origin)
return detached, true
}
// IsExplicit reports whether a surviving input contribution, including one
// inherited from a sibling, explicitly defined path.
func (m Metadata) IsExplicit(path Path) bool {
info, ok := m.effectiveField(path)
return ok && info.Explicit
}
func (m Metadata) effectiveField(path Path) (FieldInfo, bool) {
if !path.valid() {
return FieldInfo{}, false
}
pointer := path.String()
candidate, ok := m.effectivePaths[pointer]
if !ok || !matchesPathShape(path, candidate) {
return FieldInfo{}, false
}
info, ok := m.fields[pointer]
return info, ok
}
// History returns detached definition, inheritance, and resolution records for path. A record's
// Path can be an ancestor when that container operation replaced or removed path.
// Removed historical paths can have history even though Lookup returns false.
// Unknown and invalid paths return nil.
func (m Metadata) History(path Path) []Change {
model := m.historyEvidence()
if model == nil || !model.hasObservedShape(path) {
return nil
}
selections := model.selectHistory(path)
if len(selections) == 0 {
return nil
}
changes := make([]Change, 0, len(selections))
for _, selection := range selections {
if !metadataHistoryRole(selection.role) || !selection.hasAttribution {
continue
}
var previous []Origin
if len(selection.previous) != 0 {
previous = make([]Origin, len(selection.previous))
for index, origin := range selection.previous {
previous[index] = cloneOrigin(origin)
}
}
changes = append(changes, Change{
Path: selection.reportedPath.clone(),
Origin: cloneOrigin(selection.attribution.origin),
Previous: previous,
Reasons: append([]string(nil), selection.attribution.reasons...),
Dependencies: clonePaths(selection.attribution.dependencies),
})
}
if len(changes) == 0 {
return nil
}
return changes
}
// HistoricalPaths returns the paths observed during loading, including effective
// paths and paths removed by overlays, inheritance, or recorded resolution. It includes default
// paths and the root, but not paths that never existed. The zero Metadata returns
// an empty slice.
//
// Paths are detached, unique by canonical JSON Pointer, and sorted by that
// spelling. Like ParsePath results, numeric tokens are resolved contextually;
// one pointer can identify a mapping key in an earlier layer and a sequence index
// in a later one. Use History to inspect definitions and Lookup to check whether
// a path remains effective. Enumeration exposes no configuration values and does
// not perform another load or resolve secrets.
func (m Metadata) HistoricalPaths() []Path {
return m.historyEvidence().historicalPaths()
}
func (m Metadata) historyEvidence() *historyModel {
if m.historyModel != nil {
return m.historyModel
}
if m.historyBuilder != nil {
return m.historyBuilder.model
}
return nil
}
// Textual pointers are lookup keys, but explicit key/index kinds remain part of
// the query. Candidate paths are constructed from actual document containers.
func matchesPathShape(query, candidate Path) bool {
if !query.valid() || len(query.segments) != len(candidate.segments) {
return false
}
for i, segment := range query.segments {
actual := candidate.segments[i]
if !pathTokenSpellingEqual(segment, actual) {
return false
}
if actual.kind == pathSegmentIndex {
switch segment.kind {
case pathSegmentIndex:
if segment.index != actual.index {
return false
}
case pathSegmentParsed:
index, ok := schemaIndex(segment)
if !ok || index != actual.index {
return false
}
default:
return false
}
} else if segment.kind == pathSegmentIndex {
return false
}
}
return true
}
func (m Metadata) hasHistoricalPath(path Path) bool {
return m.historyEvidence().hasObservedShape(path)
}
func freezeMetadataHistory(m *Metadata, final *document.Node) {
finalizeEffectiveMetadataPaths(m, final)
builder := m.historyBuilder
if builder == nil || !builder.prefixReady {
panic("configkit: metadata history prefix is not ready")
}
builder.model.final = final
m.historyModel = builder.freeze()
m.historyBuilder = nil
}
func finalizeEffectiveMetadataPaths(m *Metadata, final *document.Node) {
m.effectivePaths = effectivePathIndex(final)
}
// effectivePathIndex records document-derived paths with their contextual segment
// kinds. Callers retain no aliases to the walked tree or its paths.
func effectivePathIndex(final *document.Node) map[string]Path {
paths := make(map[string]Path)
walkDocument(final, Path{}, func(path Path, _ *document.Node) {
paths[path.String()] = path.clone()
})
return paths
}