package config import ( "errors" "fmt" "strings" ) // Error is a value-free diagnostic. Cause may contain private parser details; // errors.Is/As remain available, but Error never formats Cause automatically. type Error struct { File, Field, Env, Reason string Line, Column int cause error } func (e *Error) Error() string { where := "config" if e.File != "" { where = fmt.Sprintf("config %q", e.File) } if e.Line > 0 { where += fmt.Sprintf(":%d:%d", e.Line, e.Column) } if e.Env != "" { where += fmt.Sprintf(" env %q", e.Env) } if e.Field != "" { where += fmt.Sprintf(" field %q", e.Field) } return where + ": " + e.Reason } func (e *Error) Unwrap() error { return e.cause } // DecodeError is retained for callers of errors.As from earlier releases. type DecodeError = Error // Invalid reports a public, value-free validation reason for a configuration field. func Invalid(field, reason string) error { return &ConfigError{Field: field, Message: reason} } // AtField prefixes a nested validation error without losing its original cause. // Unstructured validation errors are deliberately not echoed; use Invalid when // a custom validator needs a public reason rather than a private error message. func AtField(field string, err error) error { if err == nil { return nil } d := validationError(err) d.Field = joinField(field, d.Field) return d } func validationError(err error) *Error { var d *Error if errors.As(err, &d) { out := *d out.cause = err return &out } var c *ConfigError if errors.As(err, &c) { return &Error{Field: c.Field, Reason: c.Message, cause: err} } return &Error{Reason: "validation failed", cause: err} } func joinField(parent, child string) string { if parent == "" { return child } if child == "" { return parent } if strings.HasPrefix(child, "[") { return parent + child } return parent + "." + child } // Position uses one-based source line/column numbers. Zero means unavailable. type Position struct{ Line, Column int } // Document is the common decoder result. Values use JSON-compatible data: // string, bool, json.Number, nil, []any, map[string]any. Text is available for // formats such as XML whose scalar tokens have no inherent JSON type. type Document struct { Value any Positions map[string]Position } type Text string func (d Document) diagnostic(field, reason string, cause error) *Error { p := d.Positions[field] return &Error{Field: field, Reason: reason, Line: p.Line, Column: p.Column, cause: cause} }