use std::fmt::Write as _;
use crate::domain::marker;
use crate::gates::GATES;
const LANGUAGE: &str = "system";
#[derive(Debug, Clone)]
pub struct RenderOptions {
pub docs_root: String,
pub entry: String,
pub indent: String,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
docs_root: "_docs".to_string(),
entry: "sdd".to_string(),
indent: " ".to_string(),
}
}
}
fn quoted(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
#[allow(clippy::literal_string_with_formatting_args)]
fn substitute_root(pattern: &str, docs_root: &str) -> String {
pattern.replace("{docs_root}", docs_root)
}
fn render_gates(options: &RenderOptions) -> String {
let item = format!("{0} - ", options.indent);
let field = format!("{0} ", options.indent);
let mut out = String::new();
for gate in GATES {
let _ = writeln!(out, "{item}id: {}", gate.id);
let _ = writeln!(out, "{field}name: {}", quoted(gate.name));
let _ = writeln!(out, "{field}entry: {} gate {}", options.entry, gate.id);
let _ = writeln!(out, "{field}language: {LANGUAGE}");
if let Some(files) = gate.files {
let _ = writeln!(
out,
"{field}files: {}",
quoted(&substitute_root(files, &options.docs_root))
);
}
if let Some(types) = gate.types {
let _ = writeln!(out, "{field}types: [{types}]");
}
if let Some(exclude) = gate.exclude {
let _ = writeln!(
out,
"{field}exclude: {}",
quoted(&substitute_root(exclude, &options.docs_root))
);
}
if gate.always_run {
let _ = writeln!(out, "{field}always_run: true");
let _ = writeln!(out, "{field}pass_filenames: false");
}
}
out
}
#[must_use]
pub fn render_block(options: &RenderOptions) -> String {
let indent = &options.indent;
let mut out = String::new();
out.push_str(marker::BEGIN);
out.push('\n');
let _ = writeln!(out, "{indent}- repo: local");
let _ = writeln!(out, "{indent} hooks:");
let _ = writeln!(out, "{indent} - id: spec-driven-docs-verify");
let _ = writeln!(out, "{indent} name: verify spec-driven docs instance");
let _ = writeln!(out, "{indent} entry: {} verify", options.entry);
let _ = writeln!(out, "{indent} language: {LANGUAGE}");
let _ = writeln!(out, "{indent} always_run: true");
let _ = writeln!(out, "{indent} pass_filenames: false");
out.push_str(&render_gates(options));
out.push_str(marker::END);
out.push('\n');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_gate_renders_its_wiring_fields() {
let out = render_gates(&RenderOptions::default());
assert!(out.starts_with(" - id: adr-cites-a-live-rule\n"));
assert!(out.contains(" entry: sdd gate adr-filename-shape\n"));
assert!(out.contains(" language: system\n"));
assert!(out.contains(" types: [markdown]\n"));
assert_eq!(out.matches("- id: ").count(), crate::gates::GATES.len());
}
#[test]
fn the_docs_root_reaches_every_templated_pattern() {
let out = render_gates(&RenderOptions {
docs_root: "docs".to_string(),
..RenderOptions::default()
});
assert!(out.contains(" files: '^docs/decisions/.*\\.md$'\n"));
assert!(out.contains(" exclude: '^docs/decisions/'\n"));
assert!(!out.contains("{docs_root}"));
}
#[test]
fn block_style_carries_the_markers_and_the_verifier() {
let out = render_block(&RenderOptions::default());
assert!(out.starts_with("# BEGIN spec-driven-docs managed\n"));
assert!(out.ends_with("# END spec-driven-docs managed\n"));
assert!(out.contains(" - id: spec-driven-docs-verify\n"));
assert!(out.contains(" entry: sdd verify\n"));
assert!(out.contains(" - id: adr-filename-shape\n"));
assert!(out.contains(" files: '^_docs/decisions/.*\\.md$'\n"));
}
#[test]
fn always_run_gates_do_not_take_filenames() {
let out = render_gates(&RenderOptions::default());
assert_eq!(
out.matches("always_run: true").count(),
out.matches("pass_filenames: false").count()
);
}
#[test]
fn an_apostrophe_in_a_name_would_be_doubled() {
assert_eq!(quoted("it's"), "'it''s'");
}
#[test]
fn the_block_splices_into_a_plain_config() {
let block = render_block(&RenderOptions::default());
let spliced = crate::domain::marker::splice("repos:\n", &block).unwrap();
let (base, found) = crate::domain::marker::split_block(&spliced).unwrap();
assert_eq!(base, "repos:\n");
assert_eq!(found.as_deref(), Some(block.as_str()));
}
}