Preserve structured diagnostics and candidate error context

This commit is contained in:
2026-10-07 20:27:39 +00:00
parent 02050016c1
commit 9d2ca66a79
7 changed files with 133 additions and 34 deletions

View File

@@ -2,8 +2,7 @@ package releasekit
import (
"errors"
"fmt"
"strings"
"slices"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
)
@@ -32,21 +31,24 @@ type FileSizeMeasurement struct {
// ValidationError describes a failed shared validation check returned by
// [Check] or [Publish]. Cause preserves the contextual and underlying error chain.
type ValidationError struct {
Check ValidationCheck // failed shared check
Findings []ValidationFinding // actionable observations, when available
Check ValidationCheck // failed shared check
Findings []ValidationFinding // actionable observations, when available
Measurement *FileSizeMeasurement // measured file size, when applicable
Cause error // original contextual and underlying error chain
Cause error // original contextual and underlying error chain
}
// Error returns the contextual validation error summary.
// Error returns check details alongside the contextual and underlying causes.
func (failure *ValidationError) Error() string {
if failure == nil {
return "<nil>"
}
if failure.Cause != nil {
diagnostic := internalValidationFailure(failure)
var original *validation.Failure
if errors.As(failure.Cause, &original) && sameValidationDiagnostics(diagnostic, original) {
// Converted causes already render these diagnostics and candidate context.
return failure.Cause.Error()
}
return formatPublicValidationError(failure)
return diagnostic.Error()
}
// Unwrap returns the original validation error chain.
@@ -61,6 +63,10 @@ func convertValidationError(err error) error {
if err == nil {
return nil
}
var publicFailure *ValidationError
if errors.As(err, &publicFailure) {
return err
}
var failure *validation.Failure
if !errors.As(err, &failure) {
return err
@@ -91,31 +97,30 @@ func convertValidationError(err error) error {
return result
}
func formatPublicValidationError(failure *ValidationError) string {
var summary strings.Builder
if failure.Check == "" {
summary.WriteString("validation failed")
} else {
fmt.Fprintf(&summary, "%s validation failed", failure.Check)
}
func internalValidationFailure(failure *ValidationError) *validation.Failure {
result := &validation.Failure{Check: validation.Check(failure.Check), Cause: failure.Cause}
for _, finding := range failure.Findings {
summary.WriteString(": ")
if finding.Path != "" {
summary.WriteString(finding.Path)
if finding.Line > 0 {
fmt.Fprintf(&summary, ":%d", finding.Line)
if finding.Column > 0 {
fmt.Fprintf(&summary, ":%d", finding.Column)
}
}
summary.WriteString(": ")
}
summary.WriteString(finding.Message)
result.Findings = append(result.Findings, validation.Finding{
Path: finding.Path, Line: finding.Line, Column: finding.Column,
Message: finding.Message, Target: finding.Target, Directive: finding.Directive,
})
}
if failure.Measurement != nil {
fmt.Fprintf(&summary, ": measured %d bytes against a %d byte limit", failure.Measurement.Bytes, failure.Measurement.LimitBytes)
result.Measurement = &validation.FileSizeMeasurement{
Bytes: failure.Measurement.Bytes, LimitBytes: failure.Measurement.LimitBytes,
}
}
return summary.String()
return result
}
func sameValidationDiagnostics(a, b *validation.Failure) bool {
if b == nil || a.Check != b.Check || !slices.Equal(a.Findings, b.Findings) {
return false
}
if a.Measurement == nil || b.Measurement == nil {
return a.Measurement == b.Measurement
}
return *a.Measurement == *b.Measurement
}
var _ error = (*ValidationError)(nil)