Files
jarl/internal/engine/errors.go
T
adamandClaude Opus 5.5 49b1c872c9 Changesets: staged deletes of gone records count as done, name stale ops
A web changeset with a staged record delete could not be applied after
the record was deleted elsewhere (e.g. with the CLI), and the error only
said "record N does not exist", so it was hard to find the operation to
remove. A delete of a record that is already gone is now a no-op with a
warning. Other operations that no longer apply fail with their number and
summary (op_summary in the API), and the Changes page marks that row and
offers to remove it from the error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:59:19 +01:00

68 lines
2.2 KiB
Go

package engine
import (
"errors"
"fmt"
"jarl/internal/bind"
"jarl/internal/validate"
)
// ErrorKind classifies engine errors (the API maps them to HTTP status
// codes).
type ErrorKind string
const (
KindBadRequest ErrorKind = "bad_request" // malformed or invalid input
KindForbidden ErrorKind = "forbidden" // permission denied
KindNotFound ErrorKind = "not_found" // object does not exist
KindConflict ErrorKind = "conflict" // concurrent change, wrong state
KindValidation ErrorKind = "validation_failed" // the resulting config has errors
KindBIND ErrorKind = "bind_failed" // named-checkconf or rndc failed
KindDrift ErrorKind = "drift" // files were changed outside Jarl
KindNotAdopted ErrorKind = "not_adopted" // named does not load Jarl's config yet
)
// Error is an engine error with details for the caller.
type Error struct {
Kind ErrorKind `json:"code"`
Message string `json:"message"`
OpSeq int `json:"op,omitempty"` // the operation that failed
// OpSummary describes that operation, so people can find it in the
// changeset (record IDs mean little to them).
OpSummary string `json:"op_summary,omitempty"`
Problems []validate.Problem `json:"problems,omitempty"`
Check []CheckMessage `json:"check,omitempty"`
Diff string `json:"diff,omitempty"`
}
func (e *Error) Error() string {
if e.OpSeq > 0 && e.OpSummary != "" {
return fmt.Sprintf("operation %d (%s): %s", e.OpSeq, e.OpSummary, e.Message)
}
if e.OpSeq > 0 {
return fmt.Sprintf("operation %d: %s", e.OpSeq, e.Message)
}
return e.Message
}
func errf(kind ErrorKind, format string, args ...any) *Error {
return &Error{Kind: kind, Message: fmt.Sprintf(format, args...)}
}
// AsError returns err as *Error, if it is one.
func AsError(err error) (*Error, bool) {
var e *Error
ok := errors.As(err, &e)
return e, ok
}
// CheckMessage is a named-checkconf message mapped back to the Jarl object
// that produced the offending line.
type CheckMessage struct {
bind.Message
ObjectType string `json:"object_type,omitempty"`
ObjectID int64 `json:"object_id,omitempty"`
Object string `json:"object,omitempty"`
}