// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Why a configuration was refused, and where.
///
/// One error type for the whole repository's root package: reading a borrowed
/// format, merging layers, looking a path up, binding to a model. A caller
/// catches one thing and reads one message. The message names the source and
/// the position when the value came from text, and names the field path — `db.pool.size` —
/// whenever the refusal is about a place in the tree, because a message that
/// says "expected an integer" without saying where is a message that sends the
/// reader looking through the whole file.
pub(all) suberror Refused {
/// A value has the wrong shape. `path` is empty when the refusal is about a
/// value asked for on its own, with no place in the tree to name.
Type(
want~ : String,
got~ : String,
path~ : String,
source~ : String,
at~ : At
)
/// A field the model requires was nowhere in the sources.
Missing(path~ : String, want~ : String)
/// A member no field of the model describes, under a closed binding.
Unknown(path~ : String, got~ : String)
/// Two layers carried the same scalar and the clash policy refused both.
Clash(path~ : String, first~ : String, second~ : String)
/// A path that does not parse: an empty segment, a stray dot.
Path(got~ : String, at~ : Int32)
/// A borrowed reader refused the document, or a value it produced could not
/// be carried into the tree. `at` is the position the reader reported, when
/// it reported one.
Format(why~ : String, at~ : At?)
/// Nesting past the limit. A document of ten thousand opens must not take the
/// stack down with it.
TooDeep(at~ : At, limit~ : Int32)
}
///|
/// The refusal as a person reads it: what was wanted, what was there, and where.
///
/// The path comes first because that is what a person greps for; the source and
/// position follow, because that is where they look next.
fn Refused::message(self : Refused) -> String {
match self {
Type(want~, got~, path~, source~, at~) => {
let site = if path == "" {
spot(source, at)
} else {
path + " (" + spot(source, at) + ")"
}
site + ": want " + want + ", got " + got
}
Missing(path~, want~) => path + ": want " + want + ", got nothing"
Unknown(path~, got~) => path + ": no field describes this " + got
Clash(path~, first~, second~) =>
path + ": written twice, at " + first + " and " + second
Path(got~, at~) => "path " + got + ": empty segment at " + at.to_string()
Format(why~, at~) =>
match at {
Some(at) => spot("", at) + ": " + why
None => why
}
TooDeep(at~, limit~) =>
spot("", at) + ": nested deeper than " + limit.to_string()
}
}
///|
pub impl Show for Refused with fn to_string(self) {
self.message()
}
///|
pub extend Refused with Show::{to_string, output}
///|
/// The source and position part of a refusal, rendered as `name@line:column`.
pub fn spot(source : String, at : At) -> String {
let name = if source == "" { "" } else { source }
name + "@" + at.line.to_string() + ":" + at.column.to_string()
}