use super::render::Renderer;
pub trait DocSource {
fn lookup(&self, name: &str) -> Option<&str>;
fn topics(&self) -> Vec<String>;
}
pub fn show(
name: &str,
source: &impl DocSource,
renderer: &impl Renderer,
) -> Result<String, UnknownTopic> {
match source.lookup(name) {
Some(md) => Ok(renderer.render(md)),
None => Err(UnknownTopic {
name: name.to_string(),
available: source.topics(),
}),
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct UnknownTopic {
pub name: String,
pub available: Vec<String>,
}
impl std::fmt::Display for UnknownTopic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "unknown topic '{}'", self.name)?;
if !self.available.is_empty() {
write!(f, "; available: {}", self.available.join(", "))?;
}
Ok(())
}
}
impl std::error::Error for UnknownTopic {}
#[cfg(test)]
mod tests {
use super::*;
struct StubSource {
entries: Vec<(&'static str, &'static str)>,
}
impl DocSource for StubSource {
fn lookup(&self, name: &str) -> Option<&str> {
self.entries
.iter()
.find(|(n, _)| *n == name)
.map(|(_, src)| *src)
}
fn topics(&self) -> Vec<String> {
let mut t: Vec<String> = self.entries.iter().map(|(n, _)| n.to_string()).collect();
t.sort();
t
}
}
struct IdentityRenderer;
impl Renderer for IdentityRenderer {
fn render(&self, source: &str) -> String {
source.to_string()
}
}
#[test]
fn show_returns_rendered_source_for_a_known_topic() {
let source = StubSource {
entries: vec![("issue", "# Issue\n\nA piece of work.")],
};
let out = show("issue", &source, &IdentityRenderer).unwrap();
assert_eq!(out, "# Issue\n\nA piece of work.");
}
#[test]
fn show_returns_unknown_topic_with_available_list() {
let source = StubSource {
entries: vec![("issue", "x"), ("status", "y")],
};
let err = show("decision-record", &source, &IdentityRenderer).unwrap_err();
assert_eq!(err.name, "decision-record");
assert_eq!(
err.available,
vec!["issue".to_string(), "status".to_string()]
);
}
#[test]
fn unknown_topic_display_lists_alternatives_when_available() {
let err = UnknownTopic {
name: "x".to_string(),
available: vec!["a".to_string(), "b".to_string()],
};
let s = err.to_string();
assert!(s.contains("unknown topic 'x'"));
assert!(s.contains("available: a, b"));
}
#[test]
fn unknown_topic_display_omits_available_section_when_empty() {
let err = UnknownTopic {
name: "x".to_string(),
available: vec![],
};
assert_eq!(err.to_string(), "unknown topic 'x'");
}
}