mdvault_core/domain/
creator.rs1use std::fs;
7use std::path::PathBuf;
8
9use super::NoteType;
10use super::context::CreationContext;
11use super::traits::{DomainError, DomainResult, NoteBehavior};
12use crate::types::scaffolding::generate_scaffolding;
13
14#[derive(Debug)]
16pub struct CreationResult {
17 pub path: PathBuf,
19 pub generated_id: Option<String>,
21 pub type_name: String,
23}
24
25pub struct NoteCreator {
27 note_type: NoteType,
28}
29
30impl NoteCreator {
31 pub fn new(note_type: NoteType) -> Self {
33 Self { note_type }
34 }
35
36 pub fn behavior(&self) -> &dyn NoteBehavior {
38 self.note_type.behavior()
39 }
40
41 pub fn create(&self, ctx: &mut CreationContext) -> DomainResult<CreationResult> {
53 let behavior = self.note_type.behavior();
54
55 behavior.before_create(ctx)?;
60
61 let output_path = if let Some(ref path) = ctx.output_path {
63 path.clone()
64 } else {
65 let path = behavior.output_path(ctx)?;
66 ctx.output_path = Some(path.clone());
67 path
68 };
69
70 if output_path.exists() {
72 return Err(DomainError::Other(format!(
73 "Refusing to overwrite existing file: {}",
74 output_path.display()
75 )));
76 }
77
78 let content = self.generate_content(ctx)?;
80
81 let content = ensure_core_metadata(&content, ctx)?;
83
84 if let Some(parent) = output_path.parent() {
89 fs::create_dir_all(parent).map_err(DomainError::Io)?;
90 }
91 fs::write(&output_path, &content).map_err(DomainError::Io)?;
92
93 behavior.after_create(ctx, &content)?;
95
96 let generated_id = ctx
98 .core_metadata
99 .task_id
100 .clone()
101 .or_else(|| ctx.core_metadata.project_id.clone());
102
103 Ok(CreationResult {
104 path: output_path,
105 generated_id,
106 type_name: ctx.type_name.clone(),
107 })
108 }
109
110 fn generate_content(&self, ctx: &CreationContext) -> DomainResult<String> {
112 Ok(generate_scaffolding(
115 &ctx.type_name,
116 ctx.typedef.as_deref(),
117 &ctx.title,
118 &ctx.vars,
119 ))
120 }
121}
122
123fn ensure_core_metadata(content: &str, ctx: &CreationContext) -> DomainResult<String> {
129 use crate::frontmatter::parse;
130
131 let parsed = parse(content)
132 .map_err(|e| DomainError::Other(format!("Failed to parse frontmatter: {}", e)))?;
133
134 let mut fields = parsed.frontmatter.map(|fm| fm.fields).unwrap_or_default();
135
136 let core = &ctx.core_metadata;
138
139 if let Some(ref t) = core.note_type {
140 fields.insert("type".into(), serde_yaml::Value::String(t.clone()));
141 }
142 if let Some(ref t) = core.title {
143 fields.insert("title".into(), serde_yaml::Value::String(t.clone()));
144 }
145 if let Some(ref id) = core.project_id {
146 fields.insert("project-id".into(), serde_yaml::Value::String(id.clone()));
147 }
148 if let Some(ref id) = core.task_id {
149 fields.insert("task-id".into(), serde_yaml::Value::String(id.clone()));
150 }
151 if let Some(counter) = core.task_counter {
152 fields.insert("task_counter".into(), serde_yaml::Value::Number(counter.into()));
153 }
154 if let Some(ref p) = core.project {
155 fields.insert("project".into(), serde_yaml::Value::String(p.clone()));
156 }
157 if let Some(ref d) = core.date {
158 fields.insert("date".into(), serde_yaml::Value::String(d.clone()));
159 }
160 if let Some(ref w) = core.week {
161 fields.insert("week".into(), serde_yaml::Value::String(w.clone()));
162 }
163
164 let yaml = serde_yaml::to_string(&fields).map_err(|e| {
166 DomainError::Other(format!("Failed to serialize frontmatter: {}", e))
167 })?;
168
169 Ok(format!("---\n{}---\n{}", yaml, parsed.body))
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::config::types::ResolvedConfig;
176 use crate::types::TypeRegistry;
177 use std::path::PathBuf;
178 use tempfile::tempdir;
179
180 fn make_test_config(vault_root: PathBuf) -> ResolvedConfig {
181 ResolvedConfig {
182 active_profile: "test".into(),
183 vault_root: vault_root.clone(),
184 templates_dir: vault_root.join(".mdvault/templates"),
185 captures_dir: vault_root.join(".mdvault/captures"),
186 macros_dir: vault_root.join(".mdvault/macros"),
187 typedefs_dir: vault_root.join(".mdvault/typedefs"),
188 security: Default::default(),
189 logging: Default::default(),
190 }
191 }
192
193 #[test]
194 fn test_ensure_core_metadata() {
195 let content =
196 "---\ntype: wrong\ntitle: Wrong Title\ncustom: value\n---\n# Body\n";
197
198 let tmp = tempdir().unwrap();
199 let config = make_test_config(tmp.path().to_path_buf());
200 let registry = TypeRegistry::new();
201
202 let mut ctx = CreationContext::new("task", "Correct Title", &config, ®istry);
203 ctx.core_metadata.task_id = Some("TST-001".into());
204 ctx.core_metadata.project = Some("TST".into());
205
206 let result = ensure_core_metadata(content, &ctx).unwrap();
207
208 assert!(result.contains("type: task"));
209 assert!(result.contains("title: Correct Title"));
210 assert!(result.contains("task-id: TST-001"));
211 assert!(result.contains("project: TST"));
212 assert!(result.contains("custom: value")); }
214}