Files
rdbms-playground/src/friendly/format.rs
T
claude@clouddev1 41b7e9a049 style: format the whole tree with cargo fmt (stock defaults, #35)
One-time, mechanical reformat — no functional changes. The tree was not
rustfmt-clean (~1800 hunks across ~100 files); this brings it to stock
`cargo fmt` defaults so a `cargo fmt --check` CI gate can follow.
Behaviour-preserving: 2509 pass / 0 fail / 1 ignored (unchanged baseline),
clippy clean. A .git-blame-ignore-revs entry follows so `git blame`
skips this commit.
2026-06-17 21:39:19 +00:00

245 lines
8.2 KiB
Rust

//! Catalog loader and `{name}` substitution (ADR-0019 §8.4).
//!
//! The catalog is an `include_str!`-embedded YAML file parsed
//! once on first access. Hierarchical keys flatten to
//! dot-separated paths so lookups are a single `HashMap` hit.
//!
//! Substitution rejects format specifiers (`{name:…}`),
//! unknown / missing names, unterminated `{`, and stray `}`.
//! `{{` and `}}` escape to literal `{` / `}`. Everything that
//! ends in a `panic!` should have been caught by the validator
//! unit test (Step 7) at build time.
use std::collections::HashMap;
use std::fmt::{Display, Write};
use std::sync::OnceLock;
const EN_US: &str = include_str!("strings/en-US.yaml");
/// Loaded catalog: a flat map from dot-separated key to template.
pub struct Catalog {
entries: HashMap<String, String>,
}
impl Catalog {
fn load() -> Self {
let value: serde_norway::Value = serde_norway::from_str(EN_US)
.expect("embedded en-US.yaml must parse (ADR-0019 §8.6 startup check)");
let mut entries = HashMap::new();
flatten(&value, String::new(), &mut entries);
Self { entries }
}
/// Look up a flat dot-separated key. Returns the template
/// string with placeholders un-substituted.
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
self.entries.get(key).map(String::as_str)
}
/// Iterate every catalog key; used by the validator test.
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.entries.keys().map(String::as_str)
}
}
fn flatten(value: &serde_norway::Value, prefix: String, out: &mut HashMap<String, String>) {
match value {
serde_norway::Value::Mapping(map) => {
for (k, v) in map {
let k_str = k.as_str().expect("catalog keys must be strings");
let next = if prefix.is_empty() {
k_str.to_string()
} else {
format!("{prefix}.{k_str}")
};
flatten(v, next, out);
}
}
serde_norway::Value::String(s) => {
out.insert(prefix, s.clone());
}
// Empty top-level (Null) is fine — an empty catalog
// loads as no entries. Anything else is a structure
// error worth panicking over since the catalog is
// shipped with the binary.
serde_norway::Value::Null if prefix.is_empty() => {}
other => panic!("catalog value at `{prefix}` is not a string: {other:?}"),
}
}
/// Singleton catalog access. Loads on first call; subsequent
/// calls are a `HashMap` lookup away.
pub fn catalog() -> &'static Catalog {
static C: OnceLock<Catalog> = OnceLock::new();
C.get_or_init(Catalog::load)
}
/// Look up `key` and substitute the supplied named arguments.
/// See module docs for failure modes.
pub fn translate(key: &str, args: &[(&str, &dyn Display)]) -> String {
let template = catalog().get(key).unwrap_or_else(|| {
panic!("missing catalog key: `{key}` (the validator should have caught this)");
});
substitute(template, args, key)
}
fn substitute(template: &str, args: &[(&str, &dyn Display)], key: &str) -> String {
let mut out = String::with_capacity(template.len());
let mut chars = template.chars().peekable();
while let Some(c) = chars.next() {
match c {
'{' => {
if chars.peek() == Some(&'{') {
chars.next();
out.push('{');
continue;
}
let mut name = String::new();
let mut closed = false;
while let Some(&nc) = chars.peek() {
match nc {
'}' => {
chars.next();
closed = true;
break;
}
':' => {
panic!(
"catalog key `{key}` uses a format specifier in \
`{{{name}:...}}` — specifiers are not allowed \
(ADR-0019 §8.4)"
);
}
_ => {
chars.next();
name.push(nc);
}
}
}
if !closed {
panic!("catalog key `{key}` has unterminated `{{`");
}
if name.is_empty() {
panic!("catalog key `{key}` has empty `{{}}` placeholder");
}
let value = args
.iter()
.find(|(n, _)| *n == name)
.map(|(_, v)| v)
.unwrap_or_else(|| {
panic!(
"catalog key `{key}` references `{{{name}}}` but that \
argument was not supplied"
);
});
write!(out, "{value}").expect("writing to String never fails");
}
'}' => {
if chars.peek() == Some(&'}') {
chars.next();
out.push('}');
} else {
panic!("catalog key `{key}` has stray `}}`");
}
}
_ => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_loads_at_least_the_test_entry() {
// Step 1 ships a single sanity entry under `_test.*`;
// its presence proves the loader walks the YAML and
// flattens hierarchical groups correctly.
assert!(
catalog().get("_test.hello").is_some(),
"expected `_test.hello` in catalog; entries: {:?}",
catalog().keys().collect::<Vec<_>>()
);
}
#[test]
fn substitute_replaces_named_placeholder() {
let s = substitute("Hello, {name}!", &[("name", &"World")], "_test.hello");
assert_eq!(s, "Hello, World!");
}
#[test]
fn substitute_handles_multiple_placeholders() {
let s = substitute(
"{a} + {b} = {sum}",
&[("a", &1), ("b", &2), ("sum", &3)],
"_test.math",
);
assert_eq!(s, "1 + 2 = 3");
}
#[test]
fn substitute_supports_doubled_braces_as_literals() {
let s = substitute(
"set with {{count}} = {count}",
&[("count", &5)],
"_test.literals",
);
assert_eq!(s, "set with {count} = 5");
}
#[test]
fn substitute_passes_display_values_unchanged() {
// Display for an i64 — no padding, no thousands separator.
// ADR-0019 §8.7: value formats stay invariant.
let s = substitute("n = {n}", &[("n", &1234567_i64)], "_test.display");
assert_eq!(s, "n = 1234567");
}
#[test]
#[should_panic(expected = "format specifier")]
#[allow(clippy::literal_string_with_formatting_args)]
fn substitute_rejects_format_specifier() {
// The literal `{n:08}` is intentional — that's the
// shape we want to refuse.
let _ = substitute("padded: {n:08}", &[("n", &5)], "_test.specifier");
}
#[test]
#[should_panic(expected = "not supplied")]
fn substitute_rejects_unknown_placeholder() {
let _ = substitute("Hello, {who}!", &[("name", &"World")], "_test.unknown");
}
#[test]
#[should_panic(expected = "unterminated")]
fn substitute_rejects_unterminated_brace() {
let _ = substitute("oh no {", &[], "_test.unterm");
}
#[test]
#[should_panic(expected = "stray")]
fn substitute_rejects_stray_close_brace() {
let _ = substitute("oh no }", &[], "_test.stray");
}
#[test]
#[should_panic(expected = "empty")]
fn substitute_rejects_empty_placeholder() {
let _ = substitute("oh no {}", &[], "_test.empty");
}
#[test]
fn t_macro_dispatches_to_translate() {
// Sanity check that the macro produces the same output
// as a direct call. Uses the `_test.hello` entry from
// the catalog.
let m = crate::t!("_test.hello", name = "World");
let d = translate("_test.hello", &[("name", &"World")]);
assert_eq!(m, d);
}
}