Skip to content

Commit 1b6e4e2

Browse files
author
Marvin Zhang
committed
feat: Implement template loading for spec creation
- Added a default template for specs in the test helpers. - Updated `create_test_project`, `create_project_with_deps`, and `create_empty_project` functions to seed the template directory. - Introduced tests to verify that the created specs use the template content and allow for content overrides. - Updated existing specs to reflect the new template loading functionality. - Created a new spec for aligning the Rust distribution workflow with the LeanSpec model, detailing the necessary changes and phases for implementation.
1 parent eb8495c commit 1b6e4e2

File tree

9 files changed

+1387
-87
lines changed

9 files changed

+1387
-87
lines changed

rust/leanspec-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,5 @@ pub use validators::{FrontmatterValidator, StructureValidator, LineCountValidato
4444
pub use utils::{
4545
DependencyGraph, CompleteDependencyGraph, ImpactRadius,
4646
TokenCounter, TokenCount, TokenStatus,
47-
SpecLoader, SpecStats, Insights,
47+
SpecLoader, SpecStats, Insights, TemplateLoader, TemplateError,
4848
};

rust/leanspec-core/src/parsers/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
33
mod frontmatter;
44

5-
pub use frontmatter::FrontmatterParser;
5+
pub use frontmatter::{FrontmatterParser, ParseError};

rust/leanspec-core/src/utils/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ mod token_counter;
55
mod spec_loader;
66
mod stats;
77
mod insights;
8+
mod template_loader;
89

910
pub use dependency_graph::{DependencyGraph, CompleteDependencyGraph, ImpactRadius};
1011
pub use token_counter::{TokenCounter, TokenCount, TokenStatus};
1112
pub use spec_loader::SpecLoader;
1213
pub use stats::SpecStats;
1314
pub use insights::Insights;
15+
pub use template_loader::{TemplateLoader, TemplateError};
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//! Template loader for spec creation
2+
3+
use crate::types::LeanSpecConfig;
4+
use std::collections::HashSet;
5+
use std::fs;
6+
use std::path::{Path, PathBuf};
7+
use thiserror::Error;
8+
9+
#[derive(Debug, Error)]
10+
pub enum TemplateError {
11+
#[error("Templates directory not found: {0}")]
12+
TemplatesDirMissing(PathBuf),
13+
14+
#[error("Template not found. Tried: {0:?}")]
15+
NotFound(Vec<PathBuf>),
16+
17+
#[error("Failed to read template at {path}: {reason}")]
18+
ReadError { path: PathBuf, reason: String },
19+
20+
#[error("Template directory missing README.md: {0}")]
21+
MissingReadme(PathBuf),
22+
}
23+
24+
pub struct TemplateLoader {
25+
templates_dir: PathBuf,
26+
config: Option<LeanSpecConfig>,
27+
}
28+
29+
impl TemplateLoader {
30+
pub fn new<P: AsRef<Path>>(project_root: P) -> Self {
31+
Self {
32+
templates_dir: project_root.as_ref().join(".lean-spec").join("templates"),
33+
config: None,
34+
}
35+
}
36+
37+
pub fn with_config<P: AsRef<Path>>(project_root: P, config: LeanSpecConfig) -> Self {
38+
Self {
39+
templates_dir: project_root.as_ref().join(".lean-spec").join("templates"),
40+
config: Some(config),
41+
}
42+
}
43+
44+
pub fn templates_dir(&self) -> &Path {
45+
&self.templates_dir
46+
}
47+
48+
pub fn load(&self, template_name: Option<&str>) -> Result<String, TemplateError> {
49+
if !self.templates_dir.exists() {
50+
return Err(TemplateError::TemplatesDirMissing(self.templates_dir.clone()));
51+
}
52+
53+
let mut tried = Vec::new();
54+
55+
// Build candidate list with de-duplication
56+
let mut candidates: Vec<String> = Vec::new();
57+
let mut seen = HashSet::new();
58+
59+
if let Some(name) = template_name {
60+
if seen.insert(name.to_string()) {
61+
candidates.push(name.to_string());
62+
}
63+
} else if let Some(config) = &self.config {
64+
if let Some(default) = &config.default_template {
65+
if seen.insert(default.clone()) {
66+
candidates.push(default.clone());
67+
}
68+
}
69+
}
70+
71+
for fallback in ["spec-template.md", "README.md"] {
72+
if seen.insert(fallback.to_string()) {
73+
candidates.push(fallback.to_string());
74+
}
75+
}
76+
77+
for candidate in candidates {
78+
let path = self.templates_dir.join(&candidate);
79+
tried.push(path.clone());
80+
81+
if path.exists() {
82+
return self.read_template(&path);
83+
}
84+
}
85+
86+
Err(TemplateError::NotFound(tried))
87+
}
88+
89+
fn read_template(&self, path: &Path) -> Result<String, TemplateError> {
90+
if path.is_dir() {
91+
let readme = path.join("README.md");
92+
if !readme.exists() {
93+
return Err(TemplateError::MissingReadme(path.to_path_buf()));
94+
}
95+
fs::read_to_string(&readme).map_err(|e| TemplateError::ReadError {
96+
path: readme,
97+
reason: e.to_string(),
98+
})
99+
} else {
100+
fs::read_to_string(path).map_err(|e| TemplateError::ReadError {
101+
path: path.to_path_buf(),
102+
reason: e.to_string(),
103+
})
104+
}
105+
}
106+
}
107+
108+
#[cfg(test)]
109+
mod tests {
110+
use super::*;
111+
use tempfile::TempDir;
112+
113+
#[test]
114+
fn loads_file_template() {
115+
let temp = TempDir::new().unwrap();
116+
let templates_dir = temp.path().join(".lean-spec/templates");
117+
fs::create_dir_all(&templates_dir).unwrap();
118+
let template_path = templates_dir.join("spec-template.md");
119+
fs::write(&template_path, "# Test {name}").unwrap();
120+
121+
let loader = TemplateLoader::new(temp.path());
122+
let content = loader.load(None).unwrap();
123+
assert!(content.contains("Test"));
124+
}
125+
126+
#[test]
127+
fn falls_back_to_readme() {
128+
let temp = TempDir::new().unwrap();
129+
let template_dir = temp.path().join(".lean-spec/templates/custom");
130+
fs::create_dir_all(&template_dir).unwrap();
131+
fs::write(template_dir.join("README.md"), "# Readme Template").unwrap();
132+
133+
let loader = TemplateLoader::new(temp.path());
134+
let content = loader.load(Some("custom"));
135+
assert!(content.is_ok());
136+
}
137+
138+
#[test]
139+
fn returns_missing_dir_error() {
140+
let temp = TempDir::new().unwrap();
141+
let loader = TemplateLoader::new(temp.path());
142+
let err = loader.load(None).unwrap_err();
143+
matches!(err, TemplateError::TemplatesDirMissing(_));
144+
}
145+
}

0 commit comments

Comments
 (0)