Add detailed validation evidence
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package ckconfig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"text/template"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type validationReport struct {
|
||||
@@ -12,11 +15,48 @@ type validationReport struct {
|
||||
}
|
||||
|
||||
type validationScope struct {
|
||||
Scope string `json:"scope"`
|
||||
Outcome string `json:"outcome"`
|
||||
Scope string `json:"scope"`
|
||||
Outcome string `json:"outcome"`
|
||||
Details *validationDetails `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type validationDetails struct {
|
||||
State string `json:"state"`
|
||||
Findings []validationFinding `json:"findings"`
|
||||
Sources []validationSource `json:"sources"`
|
||||
}
|
||||
|
||||
type validationFinding struct {
|
||||
Code string `json:"code"`
|
||||
Severity string `json:"severity"`
|
||||
Path string `json:"path"`
|
||||
Message string `json:"message"`
|
||||
Primary *locationDTO `json:"primary"`
|
||||
Related []locationDTO `json:"related"`
|
||||
}
|
||||
|
||||
type validationSource struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
var validationTextTemplate = template.Must(commandkit.ParseTextTemplate("validation", "{{range .Scopes}}{{.Scope}}: {{.Outcome}}\n{{end}}", nil))
|
||||
var validationDetailsTemplate = template.Must(commandkit.ParseTextTemplate("validation-details", `{{range .Scopes}}{{.Scope}}: {{.Outcome}} ({{.Details.State}})
|
||||
{{range .Details.Findings}} {{.Severity}} {{.Code}} {{printf "%q" .Path}}: {{.Message}}
|
||||
{{if .Primary}} {{.Primary.SourceID}}:{{.Primary.Line}}:{{.Primary.Column}}
|
||||
{{end}}{{range .Related}} {{.SourceID}}:{{.Line}}:{{.Column}}
|
||||
{{end}}{{end}}{{range .Details.Sources}} source {{printf "%q" .ID}}: {{.Label}} {{.Path}}
|
||||
{{end}}{{end}}`, nil))
|
||||
|
||||
func parseValidationSeverity(raw string) (configkit.Severity, error) {
|
||||
switch configkit.Severity(raw) {
|
||||
case configkit.SeverityError, configkit.SeverityWarning:
|
||||
return configkit.Severity(raw), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown validation severity %q", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
|
||||
reporter := invocation.Reporter()
|
||||
@@ -29,30 +69,88 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
|
||||
}
|
||||
name := selected.scope.scope.Name
|
||||
if selected.scope.scope.Acquire == nil {
|
||||
report.Scopes = append(report.Scopes, validationScope{Scope: name, Outcome: "failed"})
|
||||
entry := validationScope{Scope: name, Outcome: "failed"}
|
||||
if plan.details {
|
||||
entry.Details = projectValidationDetails(View{}, plan.severities, plan.codes)
|
||||
}
|
||||
report.Scopes = append(report.Scopes, entry)
|
||||
outcome = commandkit.Failure(fmt.Sprintf("Configuration scope %q has no acquisition callback.", name))
|
||||
break
|
||||
}
|
||||
acquired, err := selected.scope.scope.Acquire(invocation.Context, Request{Operation: Validate, Selection: selected.selection}, selected.input, state)
|
||||
if err != nil {
|
||||
status := "failed"
|
||||
if invocation.Context.Err() != nil {
|
||||
status = "canceled"
|
||||
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
||||
if !plan.details && acquired.Value.Findings != nil {
|
||||
queueConfigDiagnostics(reporter, acquired.Value.Findings.Messages)
|
||||
}
|
||||
entry := validationScope{Scope: name, Outcome: "valid"}
|
||||
if plan.details {
|
||||
entry.Details = projectValidationDetails(acquired.Value, plan.severities, plan.codes)
|
||||
}
|
||||
if canceled := invocation.Context.Err(); canceled != nil {
|
||||
entry.Outcome = "canceled"
|
||||
if err != nil && !errors.Is(err, canceled) {
|
||||
err = errors.Join(err, canceled)
|
||||
} else if err == nil {
|
||||
err = canceled
|
||||
}
|
||||
report.Scopes = append(report.Scopes, validationScope{Scope: name, Outcome: status})
|
||||
outcome = configAcquisitionFailure(reporter, "validate", name, acquired, err)
|
||||
} else if err != nil {
|
||||
entry.Outcome = "failed"
|
||||
}
|
||||
report.Scopes = append(report.Scopes, entry)
|
||||
if err != nil {
|
||||
message := fmt.Sprintf("Unable to validate configuration scope %q.", name)
|
||||
if entry.Outcome == "canceled" {
|
||||
message = "Configuration validation was canceled."
|
||||
}
|
||||
outcome = configFailure(message, acquired.Explanation, err)
|
||||
break
|
||||
}
|
||||
queueConfigAcquisition(reporter, acquired)
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
report.Scopes = append(report.Scopes, validationScope{Scope: name, Outcome: "canceled"})
|
||||
outcome = configFailure("Configuration validation was canceled.", acquired.Explanation, err)
|
||||
break
|
||||
}
|
||||
report.Scopes = append(report.Scopes, validationScope{Scope: name, Outcome: "valid"})
|
||||
}
|
||||
if len(report.Scopes) > 0 {
|
||||
_ = reporter.Report(commandkit.Report{Value: report, TextTemplate: validationTextTemplate})
|
||||
text := validationTextTemplate
|
||||
if plan.details {
|
||||
text = validationDetailsTemplate
|
||||
}
|
||||
_ = reporter.Report(commandkit.Report{Value: report, TextTemplate: text})
|
||||
return outcome
|
||||
}
|
||||
|
||||
func projectValidationDetails(view View, severities []configkit.Severity, codes []string) *validationDetails {
|
||||
details := &validationDetails{State: "unavailable", Findings: []validationFinding{}, Sources: []validationSource{}}
|
||||
if view.Findings == nil {
|
||||
return details
|
||||
}
|
||||
if len(view.Findings.Records) == 0 {
|
||||
details.State = "empty"
|
||||
return details
|
||||
}
|
||||
details.State = "filtered"
|
||||
references := make(map[string]bool)
|
||||
for _, record := range view.Findings.Records {
|
||||
if len(severities) > 0 && !slices.Contains(severities, record.Severity) {
|
||||
continue
|
||||
}
|
||||
if len(codes) > 0 && !slices.Contains(codes, string(record.Code)) {
|
||||
continue
|
||||
}
|
||||
finding := validationFinding{Code: string(record.Code), Severity: string(record.Severity), Path: record.Path.String(), Message: record.Message, Related: locationReports(record.Related)}
|
||||
if record.Primary != nil {
|
||||
finding.Primary = &locationDTO{SourceID: record.Primary.SourceID, Line: record.Primary.Line, Column: record.Primary.Column}
|
||||
references[record.Primary.SourceID] = true
|
||||
}
|
||||
for _, location := range record.Related {
|
||||
references[location.SourceID] = true
|
||||
}
|
||||
details.Findings = append(details.Findings, finding)
|
||||
}
|
||||
if len(details.Findings) > 0 {
|
||||
details.State = "present"
|
||||
}
|
||||
if view.Sources != nil {
|
||||
for _, source := range view.Sources.Sources() {
|
||||
if references[source.ID] {
|
||||
details.Sources = append(details.Sources, validationSource{ID: source.ID, Label: source.Label, Path: source.Path})
|
||||
}
|
||||
}
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user