355 lines
10 KiB
Go
355 lines
10 KiB
Go
package engine
|
|
|
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
|
|
type sourceVersionMode uint8
|
|
|
|
const (
|
|
sourceVersionModeDefault sourceVersionMode = iota
|
|
sourceVersionModeIfPresent
|
|
sourceVersionModeRequired
|
|
)
|
|
|
|
type sourceVersionRole uint8
|
|
|
|
const (
|
|
sourceVersionMain sourceVersionRole = iota
|
|
sourceVersionContribution
|
|
)
|
|
|
|
type sourceVersionAncestor struct {
|
|
path Path
|
|
kind document.Kind
|
|
}
|
|
|
|
type sourceVersionEvidence struct {
|
|
path Path
|
|
resolvedPath Path
|
|
destination Path
|
|
version Version
|
|
location *Location
|
|
ancestors []sourceVersionAncestor
|
|
absence sourceVersionPathAbsence
|
|
checked bool
|
|
redacted bool
|
|
}
|
|
|
|
type sourceVersionProcessingResult struct {
|
|
payload *document.Node
|
|
imports []importDeclaration
|
|
evidence *sourceVersionEvidence
|
|
diagnostics []Diagnostic
|
|
}
|
|
|
|
func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule {
|
|
if local == nil {
|
|
return nil
|
|
}
|
|
if mode == sourceVersionModeDefault {
|
|
return local
|
|
}
|
|
effective := *local
|
|
effective.Path = Path{
|
|
segments: append(clonePathSegments(destination.segments, len(local.Path.segments)), local.Path.segments...),
|
|
invalid: destination.invalid || local.Path.invalid,
|
|
}
|
|
return &effective
|
|
}
|
|
|
|
type sourceVersionPathAbsence uint8
|
|
|
|
const (
|
|
sourceVersionPathPresent sourceVersionPathAbsence = iota
|
|
sourceVersionPathInvalid
|
|
sourceVersionMemberMissing
|
|
sourceVersionIndexUnavailable
|
|
sourceVersionPathObstructed
|
|
)
|
|
|
|
type sourceVersionPath struct {
|
|
node *document.Node
|
|
present bool
|
|
absence sourceVersionPathAbsence
|
|
nearest document.Location
|
|
resolvedPath Path
|
|
ancestors []sourceVersionAncestor
|
|
}
|
|
|
|
func processSourceVersion(root *document.Node, rule *VersionRule, role sourceVersionRole, destination Path, mode sourceVersionMode) sourceVersionProcessingResult {
|
|
if role != sourceVersionMain && role != sourceVersionContribution {
|
|
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source role is invalid")}}
|
|
}
|
|
if mode != sourceVersionModeDefault && mode != sourceVersionModeIfPresent && mode != sourceVersionModeRequired {
|
|
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version mode is invalid")}}
|
|
}
|
|
if mode != sourceVersionModeDefault && (rule == nil || rule.Check == nil || !rule.Path.valid()) {
|
|
return sourceVersionProcessingResult{diagnostics: []Diagnostic{sourceVersionOptionsDiagnostic("source-version checking requires a valid version rule and checker")}}
|
|
}
|
|
if root == nil {
|
|
if role == sourceVersionMain && mode != sourceVersionModeDefault {
|
|
return sourceVersionProcessingResult{diagnostics: canonicalDiagnostics(reportVersion(rule, Version{}, nil))}
|
|
}
|
|
return sourceVersionProcessingResult{}
|
|
}
|
|
|
|
payload, imports, diagnostics := splitImportDirectives(root)
|
|
if len(diagnostics) != 0 {
|
|
return sourceVersionProcessingResult{diagnostics: diagnostics}
|
|
}
|
|
result := sourceVersionProcessingResult{payload: payload, imports: imports}
|
|
if mode == sourceVersionModeDefault {
|
|
return result
|
|
}
|
|
|
|
resolved := resolveSourceVersionPath(root, rule.Path)
|
|
evidence := &sourceVersionEvidence{
|
|
path: rule.Path.clone(), resolvedPath: resolved.resolvedPath,
|
|
destination: destination.clone(), ancestors: resolved.ancestors,
|
|
absence: resolved.absence,
|
|
}
|
|
location := resolved.nearest
|
|
if resolved.present {
|
|
location = resolved.node.Location
|
|
}
|
|
if locationKnown(location) {
|
|
public := publicLocation(location)
|
|
evidence.location = &public
|
|
}
|
|
result.evidence = evidence
|
|
|
|
interpreted := interpretVersion(rule, root)
|
|
evidence.version = interpreted.version
|
|
if sourceVersionControlOverlap(root, evidence.resolvedPath) {
|
|
evidence.redactText()
|
|
result.payload = nil
|
|
result.diagnostics = []Diagnostic{sourceVersionOverlapDiagnostic(rule, evidence)}
|
|
return result
|
|
}
|
|
if hasErrors(interpreted.diagnostics) {
|
|
evidence.redactText()
|
|
result.payload = nil
|
|
result.diagnostics = canonicalDiagnostics(interpreted.diagnostics)
|
|
return result
|
|
}
|
|
if !interpreted.version.Present {
|
|
if mode == sourceVersionModeRequired {
|
|
evidence.redactText()
|
|
result.payload = nil
|
|
result.diagnostics = []Diagnostic{sourceVersionMissingDiagnostic(rule, evidence)}
|
|
return result
|
|
}
|
|
if role == sourceVersionContribution {
|
|
return result
|
|
}
|
|
}
|
|
|
|
evidence.checked = true
|
|
result.diagnostics = canonicalDiagnostics(reportVersion(rule, interpreted.version, root))
|
|
if hasErrors(result.diagnostics) {
|
|
evidence.redactText()
|
|
result.payload = nil
|
|
return result
|
|
}
|
|
if role == sourceVersionContribution && interpreted.version.Present {
|
|
if !removeSourceVersionNode(result.payload, evidence.resolvedPath) {
|
|
evidence.redactText()
|
|
result.payload = nil
|
|
result.diagnostics = []Diagnostic{sourceVersionOptionsDiagnostic("selected source version could not be removed from its owned payload")}
|
|
return result
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func sourceVersionOptionsDiagnostic(message string) Diagnostic {
|
|
return Diagnostic{Code: CodeOptionsInvalid, Severity: SeverityError, Message: message}
|
|
}
|
|
|
|
func sourceVersionMissingDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic {
|
|
return Diagnostic{
|
|
Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(),
|
|
Message: "required schema version is missing", Primary: cloneLocation(evidence.location),
|
|
}
|
|
}
|
|
|
|
func sourceVersionOverlapDiagnostic(rule *VersionRule, evidence *sourceVersionEvidence) Diagnostic {
|
|
return Diagnostic{
|
|
Code: CodeVersionError, Severity: SeverityError, Path: rule.Path.clone(),
|
|
Message: "schema version path overlaps a structural directive", Primary: cloneLocation(evidence.location),
|
|
}
|
|
}
|
|
|
|
func (evidence *sourceVersionEvidence) redactText() {
|
|
if evidence == nil {
|
|
return
|
|
}
|
|
evidence.version.Text = ""
|
|
evidence.redacted = true
|
|
}
|
|
|
|
func resolveSourceVersionPath(root *document.Node, path Path) sourceVersionPath {
|
|
resolved := sourceVersionPath{absence: sourceVersionPathInvalid}
|
|
if root == nil || !path.valid() {
|
|
return resolved
|
|
}
|
|
current := root
|
|
resolved.nearest = current.Location
|
|
resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{kind: current.Kind})
|
|
for _, segment := range path.segments {
|
|
switch current.Kind {
|
|
case document.Mapping:
|
|
if segment.kind == pathSegmentIndex {
|
|
resolved.absence = sourceVersionPathObstructed
|
|
return resolved
|
|
}
|
|
resolved.resolvedPath = resolved.resolvedPath.Key(segment.key)
|
|
found := false
|
|
for _, entry := range current.Mapping {
|
|
if entry.Key == segment.key {
|
|
current = entry.Value
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
resolved.absence = sourceVersionMemberMissing
|
|
return resolved
|
|
}
|
|
case document.Sequence:
|
|
index, ok := schemaIndex(segment)
|
|
if !ok {
|
|
resolved.absence = sourceVersionIndexUnavailable
|
|
return resolved
|
|
}
|
|
resolved.resolvedPath = resolved.resolvedPath.Index(index)
|
|
if index >= len(current.Sequence) {
|
|
resolved.absence = sourceVersionIndexUnavailable
|
|
return resolved
|
|
}
|
|
current = current.Sequence[index]
|
|
default:
|
|
resolved.absence = sourceVersionPathObstructed
|
|
return resolved
|
|
}
|
|
resolved.nearest = current.Location
|
|
resolved.ancestors = append(resolved.ancestors, sourceVersionAncestor{
|
|
path: resolved.resolvedPath.clone(), kind: current.Kind,
|
|
})
|
|
}
|
|
resolved.node = current
|
|
resolved.present = true
|
|
resolved.absence = sourceVersionPathPresent
|
|
return resolved
|
|
}
|
|
|
|
func sourceVersionControlOverlap(root *document.Node, path Path) bool {
|
|
if root == nil || !path.valid() {
|
|
return false
|
|
}
|
|
if root.Kind == document.Mapping {
|
|
for _, entry := range root.Mapping {
|
|
if entry.Key == directiveKey && pathHasPrefix(path, Keys(directiveKey)) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return sourceVersionInheritanceOverlap(root, Path{}, path)
|
|
}
|
|
|
|
func sourceVersionInheritanceOverlap(node *document.Node, current, path Path) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
if node.Kind == document.Mapping {
|
|
if _, found := inheritanceDirective(node); found {
|
|
controlPath := current.Key(inheritanceKey)
|
|
if pathHasPrefix(path, controlPath) {
|
|
return true
|
|
}
|
|
}
|
|
for _, entry := range node.Mapping {
|
|
if entry.Key == inheritanceKey {
|
|
continue
|
|
}
|
|
if sourceVersionInheritanceOverlap(entry.Value, current.Key(entry.Key), path) {
|
|
return true
|
|
}
|
|
}
|
|
} else if node.Kind == document.Sequence {
|
|
for index, child := range node.Sequence {
|
|
if sourceVersionInheritanceOverlap(child, current.Index(index), path) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func pathHasPrefix(path, prefix Path) bool {
|
|
if !path.valid() || !prefix.valid() || len(prefix.segments) > len(path.segments) {
|
|
return false
|
|
}
|
|
for index, segment := range prefix.segments {
|
|
candidate := path.segments[index]
|
|
if candidate.kind != segment.kind || candidate.key != segment.key || candidate.index != segment.index {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func removeSourceVersionNode(root *document.Node, path Path) bool {
|
|
if root == nil || !path.valid() || len(path.segments) == 0 {
|
|
return false
|
|
}
|
|
current := root
|
|
for _, segment := range path.segments[:len(path.segments)-1] {
|
|
switch current.Kind {
|
|
case document.Mapping:
|
|
if segment.kind == pathSegmentIndex {
|
|
return false
|
|
}
|
|
found := false
|
|
for _, entry := range current.Mapping {
|
|
if entry.Key == segment.key {
|
|
current = entry.Value
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
case document.Sequence:
|
|
index, ok := schemaIndex(segment)
|
|
if !ok || index >= len(current.Sequence) {
|
|
return false
|
|
}
|
|
current = current.Sequence[index]
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
last := path.segments[len(path.segments)-1]
|
|
switch current.Kind {
|
|
case document.Mapping:
|
|
if last.kind == pathSegmentIndex {
|
|
return false
|
|
}
|
|
for index, entry := range current.Mapping {
|
|
if entry.Key == last.key {
|
|
current.Mapping = append(current.Mapping[:index], current.Mapping[index+1:]...)
|
|
return true
|
|
}
|
|
}
|
|
case document.Sequence:
|
|
index, ok := schemaIndex(last)
|
|
if !ok || index >= len(current.Sequence) {
|
|
return false
|
|
}
|
|
current.Sequence = append(current.Sequence[:index], current.Sequence[index+1:]...)
|
|
return true
|
|
}
|
|
return false
|
|
}
|