Complete single-source semantic validation
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user