A configuration with mistakes in it fails the operation that reads it, and the
error holds every problem found, not only the first. In Go it is an
*errors.ConfigError from the github.com/jumppad-labs/xcl/errors package; in
Python, Config operations raise xcl.ConfigError.
Take a block with a misspelled attribute:
agent "claude" { modls = ["opus"]}One problem, one diagnostic
Each problem in the configuration text is a diagnostic. It says how serious the
problem is, what it is and why, and where: the file and the exact range at
fault, the block it is in, as an id such as agent.claude, and the field
within that block, as a path such as modls or workspace[0].directory. It
also keeps the lines of source around the fault, read when the problem was
found, so showing them never opens the file again.
Every problem an operation finds is reported this way, one diagnostic per
problem: syntax errors, a block with the wrong labels for its type or
declaring an entity another block already declares, attributes and blocks
the schema does not allow,
values of the wrong type, references to something defined nowhere, a .vars
file that is not well formed, and a provider that fails during apply or
destroy, placed at the block of the entity it failed for. A problem with a
reference is placed at the reference as written.
import ( "errors"
xclerrors "github.com/jumppad-labs/xcl/errors")
err := c.Validate("./config")
var cfgErr *xclerrors.ConfigErrorif errors.As(err, &cfgErr) { for _, d := range cfgErr.Diagnostics() { fmt.Println(d.Entity, d.Field, d.Summary) }}try: cfg.validate("./config")except xcl.ConfigError as err: for problem in err.problems: print(problem.file, problem.line, problem.entity, problem.field, problem.message)In Go, xclerrors.Diagnostic embeds the hcl.Diagnostic it was made from, so
Severity, Summary, Detail, Subject and Context are all there, and adds
Entity, Field, EntityPos, where the block starts, Source, the lines
around the fault, and Cause, the error the problem was found through.
errors.Is and errors.As reach the cause, so a provider’s own error is still
matched through the ConfigError; a provider failure’s cause is an
*xclerrors.ProviderError naming the step and the address. Diagnostics()
returns the ConfigError’s diagnostics in the order they were found. A
diagnostic’s Error is a single line, made for logs:
main.xcl:2,3-8: Unsupported argument; An argument named "modls" is not expected here. (agent.claude, modls)In Python each problem has file, line, column and message, plus
detail, the why on its own, and entity and field when the engine knows
them.
Showing problems to a person
xcl.DiagnosticPieces lays diagnostics out as a report: the severity and
summary, where the problem is, the source around it and the detail. It returns
labelled pieces, the same as configuration text does, so any renderer from
Highlighting can print it:
highlight.Text for plain text, highlight.ANSI for a terminal, or a renderer
of your own.
report := xcl.DiagnosticPieces(cfgErr.Diagnostics(), 80)os.Stderr.Write(highlight.ANSI.Render(report))Error: Unsupported argument
on main.xcl line 2, in agent.claude, field modls: 2 | modls = ["opus"]
An argument named "modls" is not expected here.The severity, and the source at fault, are labelled highlight.ScopeError or
highlight.ScopeWarning; the rest of the source is labelled as configuration,
so a terminal shows it coloured as it is in the editor. The second argument
wraps the detail at that many columns, and 0 leaves it unwrapped. Source lines
are never wrapped.
The report is not yet available from Python, where each problem prints as its file, line, column and message. See the parity list for what the package offers.