#![allow(missing_docs)]
use crate::types::{Element, Layer, UnitId, World};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VocabCoord {
pub unit: UnitId,
pub layer: Layer,
pub element: Element,
}
impl VocabCoord {
pub fn world(&self) -> World {
World::from_unit(self.unit)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LexiconEntry {
pub key: String,
pub coordinate: VocabCoord,
pub default_en: String,
pub related: Vec<String>,
pub ordinal: Option<u32>,
pub invariant: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CoordinateError {
WrongUnit {
key: String,
expected: UnitId,
found: UnitId,
},
Collision {
coord: VocabCoord,
key: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TermDescription {
pub summary: String,
pub explanation: String,
pub examples: Vec<String>,
pub constraints: Vec<String>,
}
pub trait LexiconProvider: Send + Sync {
fn domain(&self) -> &str;
fn declared_unit(&self) -> UnitId;
fn primary_world(&self) -> World;
fn vocabulary(&self) -> Vec<LexiconEntry>;
fn describe(&self, key: &str) -> Option<TermDescription>;
fn renderings(&self) -> Vec<RenderingPack> {
Vec::new()
}
fn validate_coordinates(&self) -> Result<(), CoordinateError> {
let expected = self.declared_unit();
for entry in self.vocabulary() {
if entry.coordinate.unit != expected {
return Err(CoordinateError::WrongUnit {
key: entry.key.clone(),
expected,
found: entry.coordinate.unit,
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Medium {
Text,
Voice,
Icon,
Generative,
Terminal,
Structured,
}
impl Medium {
pub const ALL: [Medium; 6] = [
Medium::Text,
Medium::Voice,
Medium::Icon,
Medium::Generative,
Medium::Terminal,
Medium::Structured,
];
pub fn from_str_loose(s: &str) -> Option<Medium> {
match s.to_lowercase().as_str() {
"text" => Some(Medium::Text),
"voice" => Some(Medium::Voice),
"icon" => Some(Medium::Icon),
"generative" => Some(Medium::Generative),
"terminal" => Some(Medium::Terminal),
"structured" => Some(Medium::Structured),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Medium::Text => "text",
Medium::Voice => "voice",
Medium::Icon => "icon",
Medium::Generative => "generative",
Medium::Terminal => "terminal",
Medium::Structured => "structured",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rendering {
pub term_key: String,
pub content: String,
pub accessibility: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderingPack {
pub scope: String,
pub medium: Medium,
pub locale: String,
entries: std::collections::BTreeMap<String, Rendering>,
}
impl RenderingPack {
pub fn new(scope: impl Into<String>, medium: Medium, locale: impl Into<String>) -> Self {
Self {
scope: scope.into(),
medium,
locale: locale.into(),
entries: std::collections::BTreeMap::new(),
}
}
pub fn insert(&mut self, term_key: impl Into<String>, content: impl Into<String>) {
let key = term_key.into();
self.entries.insert(
key.clone(),
Rendering {
term_key: key,
content: content.into(),
accessibility: None,
},
);
}
pub fn insert_with_a11y(
&mut self,
term_key: impl Into<String>,
content: impl Into<String>,
accessibility: impl Into<String>,
) {
let key = term_key.into();
self.entries.insert(
key.clone(),
Rendering {
term_key: key,
content: content.into(),
accessibility: Some(accessibility.into()),
},
);
}
pub fn get(&self, term_key: &str) -> Option<&Rendering> {
self.entries.get(term_key)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRendering {
pub content: String,
pub medium: Medium,
pub locale: String,
pub pack_used: bool,
pub accessibility: Option<String>,
}
pub fn resolve_rendering(
term_key: &str,
medium: Medium,
locale: &str,
packs: &[RenderingPack],
vocabulary: &[LexiconEntry],
) -> ResolvedRendering {
let vocab_entry = vocabulary.iter().find(|e| e.key == term_key);
if let Some(entry) = vocab_entry {
if entry.invariant {
return ResolvedRendering {
content: entry.default_en.clone(),
medium,
locale: "en".to_string(),
pack_used: false,
accessibility: None,
};
}
}
let domain = term_key.split('.').next().unwrap_or("");
let organ_scope = format!("organ:{domain}");
let base_locale = base_language(locale);
let scopes_and_locales: [(&str, &str); 4] = [
(&organ_scope, locale), (&organ_scope, &base_locale), ("system", locale), ("system", &base_locale), ];
for (scope, loc) in &scopes_and_locales {
if let Some(pack) = packs.iter().find(|p| {
p.scope == *scope && p.medium == medium && p.locale == *loc
}) {
if let Some(rendering) = pack.get(term_key) {
return ResolvedRendering {
content: rendering.content.clone(),
medium,
locale: pack.locale.clone(),
pack_used: true,
accessibility: rendering.accessibility.clone(),
};
}
}
}
if let Some(entry) = vocab_entry {
return ResolvedRendering {
content: entry.default_en.clone(),
medium,
locale: "en".to_string(),
pack_used: false,
accessibility: None,
};
}
ResolvedRendering {
content: term_key.to_string(),
medium,
locale: locale.to_string(),
pack_used: false,
accessibility: None,
}
}
fn base_language(locale: &str) -> String {
locale.split('-').next().unwrap_or(locale).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
struct TestProvider;
impl LexiconProvider for TestProvider {
fn domain(&self) -> &str {
"test"
}
fn declared_unit(&self) -> UnitId {
UnitId::FU
}
fn primary_world(&self) -> World {
World::WAI
}
fn vocabulary(&self) -> Vec<LexiconEntry> {
vec![LexiconEntry {
key: "test.item.active".into(),
coordinate: VocabCoord { unit: UnitId::FU, layer: Layer::Data, element: Element::Pointer },
default_en: "Active".into(),
related: vec!["test.item.inactive".into()],
ordinal: Some(0),
invariant: false,
}]
}
fn describe(&self, key: &str) -> Option<TermDescription> {
if key == "test.item.active" {
Some(TermDescription {
summary: "An active test item".into(),
explanation: "Represents an item in the active state.".into(),
examples: vec!["test.item.active in FU.Data".into()],
constraints: vec!["Must be active or inactive".into()],
})
} else {
None
}
}
}
#[test]
fn lexicon_provider_is_object_safe() {
fn _assert(_: &dyn LexiconProvider) {}
}
#[test]
fn provider_domain() {
let p = TestProvider;
assert_eq!(p.domain(), "test");
}
#[test]
fn provider_vocabulary_returns_entries() {
let p = TestProvider;
let vocab = p.vocabulary();
assert_eq!(vocab.len(), 1);
assert_eq!(vocab[0].key, "test.item.active");
assert_eq!(vocab[0].default_en, "Active");
assert_eq!(vocab[0].coordinate.unit, UnitId::FU);
assert_eq!(vocab[0].coordinate.world(), World::WAI);
assert!(!vocab[0].invariant);
assert_eq!(vocab[0].ordinal, Some(0));
}
#[test]
fn provider_describe_known_term() {
let p = TestProvider;
let desc = p.describe("test.item.active").unwrap();
assert_eq!(desc.summary, "An active test item");
assert!(!desc.explanation.is_empty());
assert_eq!(desc.examples.len(), 1);
assert_eq!(desc.constraints.len(), 1);
}
#[test]
fn provider_describe_unknown_term_returns_none() {
let p = TestProvider;
assert!(p.describe("test.nonexistent").is_none());
}
#[test]
fn entry_keys_must_start_with_domain_prefix() {
let p = TestProvider;
let domain = p.domain();
for entry in p.vocabulary() {
assert!(
entry.key.starts_with(&format!("{domain}.")),
"entry key '{}' must start with '{domain}.'",
entry.key
);
}
}
#[test]
fn entry_related_terms_are_dot_notation() {
let p = TestProvider;
for entry in p.vocabulary() {
for related in &entry.related {
assert!(
related.contains('.'),
"related term '{related}' should use dot notation"
);
}
}
}
#[test]
fn lexicon_entry_clone_and_eq() {
let entry = LexiconEntry {
key: "test.a".into(),
coordinate: VocabCoord { unit: UnitId::FU, layer: Layer::Data, element: Element::Root },
default_en: "A".into(),
related: vec![],
ordinal: None,
invariant: false,
};
let cloned = entry.clone();
assert_eq!(entry, cloned);
}
#[test]
fn term_description_clone_and_eq() {
let desc = TermDescription {
summary: "s".into(),
explanation: "e".into(),
examples: vec!["x".into()],
constraints: vec![],
};
let cloned = desc.clone();
assert_eq!(desc, cloned);
}
#[test]
fn vocab_coord_world_derives_from_unit() {
let coord = VocabCoord { unit: UnitId::OU, layer: Layer::Data, element: Element::Root };
assert_eq!(coord.world(), World::WAY);
let coord_fu = VocabCoord { unit: UnitId::FU, layer: Layer::Server, element: Element::Pointer };
assert_eq!(coord_fu.world(), World::WAI);
}
#[test]
fn vocab_coord_clone_copy_and_eq() {
let c = VocabCoord { unit: UnitId::MU, layer: Layer::Client, element: Element::Tree };
let d = c;
assert_eq!(c, d);
}
#[test]
fn validate_coordinates_passes_for_correct_unit() {
let p = TestProvider;
assert!(p.validate_coordinates().is_ok());
}
#[test]
fn validate_coordinates_fails_for_wrong_unit() {
struct WrongUnitProvider;
impl LexiconProvider for WrongUnitProvider {
fn domain(&self) -> &str { "wrong" }
fn declared_unit(&self) -> UnitId { UnitId::MU }
fn primary_world(&self) -> World { World::WIU }
fn vocabulary(&self) -> Vec<LexiconEntry> {
vec![LexiconEntry {
key: "wrong.item".into(),
coordinate: VocabCoord {
unit: UnitId::FU, layer: Layer::Data,
element: Element::Root,
},
default_en: "Item".into(),
related: vec![],
ordinal: None,
invariant: false,
}]
}
fn describe(&self, _: &str) -> Option<TermDescription> { None }
}
let p = WrongUnitProvider;
let err = p.validate_coordinates().unwrap_err();
assert!(matches!(
err,
CoordinateError::WrongUnit { expected: UnitId::MU, found: UnitId::FU, .. }
));
}
#[test]
fn declared_unit_matches_primary_world() {
let p = TestProvider;
assert_eq!(World::from_unit(p.declared_unit()), p.primary_world());
}
#[test]
fn medium_has_six_variants() {
assert_eq!(Medium::ALL.len(), 6);
}
#[test]
fn medium_from_str_loose_all_variants() {
assert_eq!(Medium::from_str_loose("text"), Some(Medium::Text));
assert_eq!(Medium::from_str_loose("voice"), Some(Medium::Voice));
assert_eq!(Medium::from_str_loose("icon"), Some(Medium::Icon));
assert_eq!(Medium::from_str_loose("generative"), Some(Medium::Generative));
assert_eq!(Medium::from_str_loose("terminal"), Some(Medium::Terminal));
assert_eq!(Medium::from_str_loose("structured"), Some(Medium::Structured));
assert_eq!(Medium::from_str_loose("TEXT"), Some(Medium::Text));
assert_eq!(Medium::from_str_loose("unknown"), None);
}
#[test]
fn medium_as_str_round_trip() {
for m in Medium::ALL {
assert_eq!(Medium::from_str_loose(m.as_str()), Some(m));
}
}
#[test]
fn rendering_construction() {
let r = Rendering {
term_key: "organ-a.term.severity.critical".into(),
content: "<emphasis>Kritisch</emphasis>".into(),
accessibility: Some("Critical severity".into()),
};
assert_eq!(r.term_key, "organ-a.term.severity.critical");
assert_eq!(r.content, "<emphasis>Kritisch</emphasis>");
assert_eq!(r.accessibility.as_deref(), Some("Critical severity"));
}
#[test]
fn rendering_pack_insert_and_get() {
let mut pack = RenderingPack::new("organ:organ-a", Medium::Text, "de");
pack.insert("organ-a.term.severity.critical", "Kritisch");
pack.insert("organ-a.term.severity.high", "Hoch");
pack.insert("organ-a.term.severity.low", "Niedrig");
assert_eq!(pack.len(), 3);
assert!(!pack.is_empty());
let r = pack.get("organ-a.term.severity.critical").unwrap();
assert_eq!(r.content, "Kritisch");
assert!(pack.get("guard.nonexistent").is_none());
}
#[test]
fn rendering_pack_with_accessibility() {
let mut pack = RenderingPack::new("organ:organ-a", Medium::Icon, "en");
pack.insert_with_a11y(
"organ-a.term.severity.critical",
"icon:severity-critical-red",
"Critical severity indicator",
);
let r = pack.get("organ-a.term.severity.critical").unwrap();
assert_eq!(r.content, "icon:severity-critical-red");
assert_eq!(
r.accessibility.as_deref(),
Some("Critical severity indicator")
);
}
#[test]
fn rendering_pack_empty() {
let pack = RenderingPack::new("system", Medium::Text, "en");
assert!(pack.is_empty());
assert_eq!(pack.len(), 0);
}
fn test_vocab() -> Vec<LexiconEntry> {
vec![
LexiconEntry {
key: "organ-a.term.severity.critical".into(),
coordinate: VocabCoord { unit: UnitId::OU, layer: Layer::Data, element: Element::Root },
default_en: "Critical".into(),
related: vec![],
ordinal: Some(3),
invariant: false,
},
LexiconEntry {
key: "organ-b.unit.fu".into(),
coordinate: VocabCoord { unit: UnitId::FU, layer: Layer::Data, element: Element::Root },
default_en: "FU".into(),
related: vec![],
ordinal: None,
invariant: true,
},
]
}
#[test]
fn resolve_organ_exact_locale() {
let mut pack = RenderingPack::new("organ:organ-a", Medium::Text, "de");
pack.insert("organ-a.term.severity.critical", "Kritisch");
let packs = vec![pack];
let vocab = test_vocab();
let result = resolve_rendering(
"organ-a.term.severity.critical",
Medium::Text,
"de",
&packs,
&vocab,
);
assert_eq!(result.content, "Kritisch");
assert_eq!(result.locale, "de");
assert!(result.pack_used);
}
#[test]
fn resolve_regional_falls_back_to_base_language() {
let mut pack = RenderingPack::new("organ:organ-a", Medium::Text, "de");
pack.insert("organ-a.term.severity.critical", "Kritisch");
let packs = vec![pack];
let vocab = test_vocab();
let result = resolve_rendering(
"organ-a.term.severity.critical",
Medium::Text,
"de-AT",
&packs,
&vocab,
);
assert_eq!(result.content, "Kritisch");
assert_eq!(result.locale, "de");
assert!(result.pack_used);
}
#[test]
fn resolve_falls_back_to_system_pack() {
let mut sys_pack = RenderingPack::new("system", Medium::Text, "de");
sys_pack.insert("organ-a.term.severity.critical", "Kritisch (System)");
let packs = vec![sys_pack];
let vocab = test_vocab();
let result = resolve_rendering(
"organ-a.term.severity.critical",
Medium::Text,
"de",
&packs,
&vocab,
);
assert_eq!(result.content, "Kritisch (System)");
assert!(result.pack_used);
}
#[test]
fn resolve_falls_back_to_default_en() {
let packs: Vec<RenderingPack> = vec![];
let vocab = test_vocab();
let result = resolve_rendering(
"organ-a.term.severity.critical",
Medium::Text,
"ja",
&packs,
&vocab,
);
assert_eq!(result.content, "Critical");
assert_eq!(result.locale, "en");
assert!(!result.pack_used);
}
#[test]
fn resolve_falls_back_to_raw_key() {
let packs: Vec<RenderingPack> = vec![];
let vocab = test_vocab();
let result = resolve_rendering(
"unknown.term.here",
Medium::Text,
"de",
&packs,
&vocab,
);
assert_eq!(result.content, "unknown.term.here");
assert!(!result.pack_used);
}
#[test]
fn resolve_invariant_term_bypasses_packs() {
let mut pack = RenderingPack::new("organ:organ-b", Medium::Text, "de");
pack.insert("organ-b.unit.fu", "GE");
let packs = vec![pack];
let vocab = test_vocab();
let result = resolve_rendering(
"organ-b.unit.fu",
Medium::Text,
"de",
&packs,
&vocab,
);
assert_eq!(result.content, "FU");
assert!(!result.pack_used);
}
#[test]
fn resolve_voice_medium() {
let mut pack = RenderingPack::new("organ:organ-a", Medium::Voice, "de");
pack.insert(
"organ-a.term.severity.critical",
"<emphasis>Kritisch</emphasis>",
);
let packs = vec![pack];
let vocab = test_vocab();
let result = resolve_rendering(
"organ-a.term.severity.critical",
Medium::Voice,
"de",
&packs,
&vocab,
);
assert_eq!(result.content, "<emphasis>Kritisch</emphasis>");
assert_eq!(result.medium, Medium::Voice);
assert!(result.pack_used);
}
#[test]
fn resolve_organ_priority_over_system() {
let mut organ_pack = RenderingPack::new("organ:organ-a", Medium::Text, "de");
organ_pack.insert("organ-a.term.severity.critical", "Organ: Kritisch");
let mut sys_pack = RenderingPack::new("system", Medium::Text, "de");
sys_pack.insert("organ-a.term.severity.critical", "System: Kritisch");
let packs = vec![organ_pack, sys_pack];
let vocab = test_vocab();
let result = resolve_rendering(
"organ-a.term.severity.critical",
Medium::Text,
"de",
&packs,
&vocab,
);
assert_eq!(result.content, "Organ: Kritisch");
}
#[test]
fn base_language_extraction() {
assert_eq!(base_language("de-AT"), "de");
assert_eq!(base_language("en-US"), "en");
assert_eq!(base_language("ja"), "ja");
assert_eq!(base_language("zh-Hans-CN"), "zh");
}
#[test]
fn provider_default_renderings_empty() {
let p = TestProvider;
assert!(p.renderings().is_empty());
}
#[test]
fn rendering_clone_and_eq() {
let r = Rendering {
term_key: "a.b".into(),
content: "c".into(),
accessibility: None,
};
assert_eq!(r, r.clone());
}
#[test]
fn rendering_pack_clone_and_eq() {
let mut pack = RenderingPack::new("system", Medium::Text, "en");
pack.insert("a.b", "c");
assert_eq!(pack, pack.clone());
}
#[test]
fn resolved_rendering_clone_and_eq() {
let r = ResolvedRendering {
content: "x".into(),
medium: Medium::Text,
locale: "en".into(),
pack_used: false,
accessibility: None,
};
assert_eq!(r, r.clone());
}
}