Add single-source loading and version checks
This commit is contained in:
101
validation.go
Normal file
101
validation.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package configkit
|
||||
|
||||
import "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.
|
||||
type Validation struct {
|
||||
state *validationState
|
||||
}
|
||||
|
||||
type validationState struct {
|
||||
active bool
|
||||
errorCode Code
|
||||
warningCode Code
|
||||
document *document.Node
|
||||
diagnostics []Diagnostic
|
||||
}
|
||||
|
||||
// Error reports a value-free application error at path. Related paths are
|
||||
// resolved to source locations when possible.
|
||||
func (v *Validation) Error(path Path, message string, related ...Path) {
|
||||
v.report(SeverityError, path, message, related)
|
||||
}
|
||||
|
||||
// Warning reports a value-free application warning at path. Related paths are
|
||||
// resolved to source locations when possible.
|
||||
func (v *Validation) Warning(path Path, message string, related ...Path) {
|
||||
v.report(SeverityWarning, path, message, related)
|
||||
}
|
||||
|
||||
func (v *Validation) report(severity Severity, path Path, message string, related []Path) {
|
||||
if v == nil || v.state == nil || !v.state.active {
|
||||
return
|
||||
}
|
||||
code := v.state.errorCode
|
||||
if severity == SeverityWarning {
|
||||
code = v.state.warningCode
|
||||
}
|
||||
diagnostic := Diagnostic{Code: code, Severity: severity, Path: path.clone(), Message: message}
|
||||
if !path.valid() {
|
||||
diagnostic.Severity = SeverityError
|
||||
diagnostic.Code = v.state.errorCode
|
||||
diagnostic.Path = Path{}
|
||||
diagnostic.Message = "application reported an invalid configuration path"
|
||||
} else if location, ok := nearestDocumentLocation(v.state.document, path); ok {
|
||||
diagnostic.Primary = documentLocationPointer(location)
|
||||
}
|
||||
for _, relatedPath := range related {
|
||||
if !relatedPath.valid() {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
v.state.diagnostics = append(v.state.diagnostics, diagnostic)
|
||||
}
|
||||
|
||||
func nearestDocumentLocation(root *document.Node, path Path) (document.Location, bool) {
|
||||
if root == nil || !path.valid() {
|
||||
return document.Location{}, false
|
||||
}
|
||||
current := root
|
||||
nearest := current.Location
|
||||
for _, segment := range path.segments {
|
||||
switch current.Kind {
|
||||
case document.Mapping:
|
||||
if segment.kind == pathSegmentIndex {
|
||||
return nearest, locationKnown(nearest)
|
||||
}
|
||||
found := false
|
||||
for _, entry := range current.Mapping {
|
||||
if entry.Key == segment.key {
|
||||
current = entry.Value
|
||||
nearest = current.Location
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nearest, locationKnown(nearest)
|
||||
}
|
||||
case document.Sequence:
|
||||
index, ok := schemaIndex(segment)
|
||||
if !ok || index >= len(current.Sequence) {
|
||||
return nearest, locationKnown(nearest)
|
||||
}
|
||||
current = current.Sequence[index]
|
||||
nearest = current.Location
|
||||
default:
|
||||
return nearest, locationKnown(nearest)
|
||||
}
|
||||
}
|
||||
return nearest, locationKnown(nearest)
|
||||
}
|
||||
|
||||
func locationKnown(location document.Location) bool {
|
||||
return location.SourceID != "" || location.Line != 0 || location.Column != 0
|
||||
}
|
||||
Reference in New Issue
Block a user