102 lines
2.7 KiB
Go
102 lines
2.7 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/internal/configdiag"
|
|
)
|
|
|
|
// 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 nested *configdiag.Error
|
|
if errors.As(err, &nested) {
|
|
return &Error{Field: nested.Field, Reason: nested.Reason, cause: err}
|
|
}
|
|
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
|
|
environments map[string][]string
|
|
}
|
|
|
|
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}
|
|
}
|