Complete single-source semantic validation

This commit is contained in:
2026-09-06 20:19:58 +00:00
parent 0da4260ad6
commit a0cd0977ee
14 changed files with 484 additions and 31 deletions

View File

@@ -1,6 +1,10 @@
package configkit
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
import (
"reflect"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
// Validation collects safe application diagnostics during a supported callback.
// A callback must not retain the context or use it concurrently.
@@ -12,6 +16,8 @@ type validationState struct {
active bool
errorCode Code
warningCode Code
invalidPath Code
schema *schemaDescriptor
document *document.Node
diagnostics []Diagnostic
}
@@ -38,17 +44,49 @@ func (v *Validation) report(severity Severity, path Path, message string, relate
}
diagnostic := Diagnostic{Code: code, Severity: severity, Path: path.clone(), Message: message}
if !path.valid() {
invalidCode := v.state.invalidPath
if invalidCode == "" {
invalidCode = v.state.errorCode
}
diagnostic.Severity = SeverityError
diagnostic.Code = v.state.errorCode
diagnostic.Code = invalidCode
diagnostic.Path = Path{}
diagnostic.Message = "application reported an invalid configuration path"
} else if v.state.schema != nil {
if _, ok := resolveSchemaPath(v.state.schema, path); !ok {
diagnostic.Severity = SeverityError
diagnostic.Code = v.state.invalidPath
diagnostic.Message = "application reported a path outside the configuration schema"
v.state.diagnostics = append(v.state.diagnostics, diagnostic)
return
}
if location, ok := nearestDocumentLocation(v.state.document, path); ok {
diagnostic.Primary = documentLocationPointer(location)
}
} else if location, ok := nearestDocumentLocation(v.state.document, path); ok {
diagnostic.Primary = documentLocationPointer(location)
}
for _, relatedPath := range related {
if !relatedPath.valid() {
invalidCode := v.state.invalidPath
if invalidCode == "" {
invalidCode = v.state.errorCode
}
v.state.diagnostics = append(v.state.diagnostics, Diagnostic{
Code: invalidCode, Severity: SeverityError,
Message: "application reported an invalid related configuration path",
})
continue
}
if v.state.schema != nil {
if _, ok := resolveSchemaPath(v.state.schema, relatedPath); !ok {
v.state.diagnostics = append(v.state.diagnostics, Diagnostic{
Code: v.state.invalidPath, Severity: SeverityError, Path: relatedPath.clone(),
Message: "application reported a related path outside the configuration schema",
})
continue
}
}
if location, ok := nearestDocumentLocation(v.state.document, relatedPath); ok {
diagnostic.Related = append(diagnostic.Related, Location{
SourceID: location.SourceID, Line: location.Line, Column: location.Column,
@@ -58,6 +96,32 @@ func (v *Validation) report(severity Severity, path Path, message string, relate
v.state.diagnostics = append(v.state.diagnostics, diagnostic)
}
func runValidation[T any](configuration *T, validator func(*T, *Validation), schema *schemaDescriptor, before, input *document.Node) []Diagnostic {
state := &validationState{
active: true, errorCode: CodeValidateError, warningCode: CodeValidateWarning,
invalidPath: CodeValidateInvalidPath, schema: schema, document: input,
}
context := &Validation{state: state}
validator(configuration, context)
state.active = false
after, issue := projectConfiguration(schema, reflect.ValueOf(*configuration), defaultProjectionLimits())
if issue != nil {
state.diagnostics = append(state.diagnostics, validationMutationDiagnostic(issue.path))
return canonicalDiagnostics(state.diagnostics)
}
for _, delta := range compareDocumentTrees(before, after) {
state.diagnostics = append(state.diagnostics, validationMutationDiagnostic(delta.path))
}
return canonicalDiagnostics(state.diagnostics)
}
func validationMutationDiagnostic(path Path) Diagnostic {
return Diagnostic{
Code: CodeValidateMutation, Severity: SeverityError, Path: path.clone(),
Message: "application validation mutated the configuration",
}
}
func nearestDocumentLocation(root *document.Node, path Path) (document.Location, bool) {
if root == nil || !path.valid() {
return document.Location{}, false