///|
/// A validation rule applied to a configuration value.
///
/// Rules can require a path to exist or require an existing value to have a
/// particular JSON-compatible kind.
pub(all) enum ConfigAuditRule {
Required(ConfigPath)
TypeIs(ConfigPath, ConfigValueKind)
} derive(Eq, Debug)
///|
/// Describes why a configuration audit reported an issue.
pub(all) enum ConfigAuditIssueKind {
MissingRequired
TypeMismatch
} derive(Eq, Debug)
///|
/// A single issue found while auditing a configuration value.
pub struct ConfigAuditIssue {
path : ConfigPath
kind : ConfigAuditIssueKind
expected_kind : ConfigValueKind?
actual_kind : ConfigValueKind?
} derive(Eq, Debug)
///|
/// Create a rule that requires one configuration path to be present.
pub fn ConfigAuditRule::required(path : ConfigPath) -> ConfigAuditRule {
Required(path)
}
///|
/// Create a rule that requires an existing value to have a particular kind.
pub fn ConfigAuditRule::type_is(
path : ConfigPath,
expected_kind : ConfigValueKind,
) -> ConfigAuditRule {
TypeIs(path, expected_kind)
}
///|
/// Return the path inspected by this rule.
pub fn ConfigAuditRule::path(self : ConfigAuditRule) -> ConfigPath {
match self {
Required(path) => path
TypeIs(path, _) => path
}
}
///|
/// Return the path associated with this audit issue.
pub fn ConfigAuditIssue::path(self : ConfigAuditIssue) -> ConfigPath {
self.path
}
///|
/// Return the category of this audit issue.
pub fn ConfigAuditIssue::kind(self : ConfigAuditIssue) -> ConfigAuditIssueKind {
self.kind
}
///|
/// Return the expected value kind for a type mismatch.
///
/// Other issue categories return `None`.
pub fn ConfigAuditIssue::expected_kind(
self : ConfigAuditIssue,
) -> ConfigValueKind? {
self.expected_kind
}
///|
/// Return the actual value kind for a type mismatch.
///
/// Other issue categories return `None`.
pub fn ConfigAuditIssue::actual_kind(
self : ConfigAuditIssue,
) -> ConfigValueKind? {
self.actual_kind
}
///|
/// Return a stable lowercase name for this audit issue category.
pub fn ConfigAuditIssueKind::to_string(self : ConfigAuditIssueKind) -> String {
match self {
MissingRequired => "missing_required"
TypeMismatch => "type_mismatch"
}
}
///|
/// Render a concise human-readable diagnostic for this audit issue.
pub fn ConfigAuditIssue::message(self : ConfigAuditIssue) -> String {
match self.kind {
MissingRequired =>
"required configuration path '\{self.path.to_string()}' is missing"
TypeMismatch => {
let expected = self.expected_kind.unwrap().to_string()
let actual = self.actual_kind.unwrap().to_string()
"configuration path '\{self.path.to_string()}' expected \{expected}, but found \{actual}"
}
}
}
///|
/// Apply validation rules to this configuration value.
///
/// Required paths count as present even when their value is `null`. Type rules
/// only inspect paths that exist, allowing a separate required rule to control
/// missing-path diagnostics. Issues are ordered lexicographically by path so
/// diagnostics remain deterministic.
pub fn ConfigValue::audit(
self : ConfigValue,
rules : Array[ConfigAuditRule],
) -> Array[ConfigAuditIssue] {
let issues : Array[ConfigAuditIssue] = []
for rule in rules {
match rule {
Required(path) =>
if self.get(path) is None {
issues.push({
path,
kind: MissingRequired,
expected_kind: None,
actual_kind: None,
})
}
TypeIs(path, expected_kind) =>
match self.get(path) {
Some(value) => {
let actual_kind = value.kind()
if actual_kind != expected_kind {
issues.push({
path,
kind: TypeMismatch,
expected_kind: Some(expected_kind),
actual_kind: Some(actual_kind),
})
}
}
None => ()
}
}
}
issues.sort_by((left, right) => {
left.path.to_string().lexical_compare(right.path.to_string())
})
issues
}