mdvault_core/domain/
creator.rs1use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9
10use chrono::Local;
11
12use super::NoteType;
13use super::context::CreationContext;
14use super::traits::{DomainError, DomainResult, NoteBehavior};
15use crate::templates::engine::render_with_ref_date as render_template;
16use crate::types::scaffolding::generate_scaffolding;
17
18#[derive(Debug)]
20pub struct CreationResult {
21 pub path: PathBuf,
23 pub generated_id: Option<String>,
25 pub type_name: String,
27 pub content: String,
29}
30
31pub struct NoteCreator {
33 note_type: NoteType,
34}
35
36impl NoteCreator {
37 pub fn new(note_type: NoteType) -> Self {
39 Self { note_type }
40 }
41
42 pub fn behavior(&self) -> &dyn NoteBehavior {
44 self.note_type.behavior()
45 }
46
47 pub fn create(&self, ctx: &mut CreationContext) -> DomainResult<CreationResult> {
59 let behavior = self.note_type.behavior();
60
61 behavior.before_create(ctx)?;
66
67 let output_path = if let Some(ref path) = ctx.output_path {
69 path.clone()
70 } else {
71 let path = behavior.output_path(ctx)?;
72 ctx.output_path = Some(path.clone());
73 path
74 };
75
76 if output_path.exists() {
78 return Err(DomainError::Other(format!(
79 "Refusing to overwrite existing file: {}",
80 output_path.display()
81 )));
82 }
83
84 let content = self.generate_content(ctx)?;
86
87 let order = ctx.typedef.as_ref().and_then(|td| td.frontmatter_order.as_deref());
89 let content =
90 ctx.core_metadata.apply_to_content(&content, order).map_err(|e| {
91 DomainError::Other(format!("Failed to apply core metadata: {}", e))
92 })?;
93
94 if let Some(parent) = output_path.parent() {
99 fs::create_dir_all(parent).map_err(DomainError::Io)?;
100 }
101 fs::write(&output_path, &content).map_err(DomainError::Io)?;
102
103 let generated_id = ctx
107 .core_metadata
108 .task_id
109 .clone()
110 .or_else(|| ctx.core_metadata.project_id.clone());
111
112 Ok(CreationResult {
113 path: output_path,
114 generated_id,
115 type_name: ctx.type_name.clone(),
116 content,
117 })
118 }
119
120 fn generate_content(&self, ctx: &CreationContext) -> DomainResult<String> {
125 if let Some(ref template) = ctx.template {
126 let render_ctx = self.build_render_context(ctx);
128 render_template(template, &render_ctx, ctx.reference_date).map_err(|e| {
129 DomainError::Other(format!("Failed to render template: {}", e))
130 })
131 } else {
132 let title_for_scaffolding =
136 ctx.core_metadata.title.as_ref().unwrap_or(&ctx.title);
137 Ok(generate_scaffolding(
138 &ctx.type_name,
139 ctx.typedef.as_deref(),
140 title_for_scaffolding,
141 &ctx.vars,
142 ))
143 }
144 }
145
146 fn build_render_context(&self, ctx: &CreationContext) -> HashMap<String, String> {
148 let mut render_ctx = HashMap::new();
149
150 let now = Local::now();
152 render_ctx.insert("date".into(), now.format("%Y-%m-%d").to_string());
153 render_ctx.insert("time".into(), now.format("%H:%M").to_string());
154 render_ctx.insert("datetime".into(), now.to_rfc3339());
155 render_ctx.insert("today".into(), now.format("%Y-%m-%d").to_string());
156 render_ctx.insert("now".into(), now.to_rfc3339());
157
158 render_ctx.extend(ctx.vars.clone());
160
161 render_ctx.insert(
163 "vault_root".into(),
164 ctx.config.vault_root.to_string_lossy().to_string(),
165 );
166 render_ctx.insert(
167 "templates_dir".into(),
168 ctx.config.templates_dir.to_string_lossy().to_string(),
169 );
170
171 if let Some(ref template) = ctx.template {
173 render_ctx.insert("template_name".into(), template.logical_name.clone());
174 render_ctx.insert(
175 "template_path".into(),
176 template.path.to_string_lossy().to_string(),
177 );
178 }
179
180 if let Some(ref output_path) = ctx.output_path {
182 render_ctx
183 .insert("output_path".into(), output_path.to_string_lossy().to_string());
184 if let Some(name) = output_path.file_name().and_then(|s| s.to_str()) {
185 render_ctx.insert("output_filename".into(), name.to_string());
186 }
187 if let Some(parent) = output_path.parent() {
188 render_ctx
189 .insert("output_dir".into(), parent.to_string_lossy().to_string());
190 }
191 }
192
193 if let Some(ref id) = ctx.core_metadata.task_id {
195 render_ctx.insert("task-id".into(), id.clone());
196 }
197 if let Some(ref id) = ctx.core_metadata.project_id {
198 render_ctx.insert("project-id".into(), id.clone());
199 }
200 if let Some(ref project) = ctx.core_metadata.project {
201 render_ctx.insert("project".into(), project.clone());
202 }
203
204 render_ctx
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use crate::domain::CoreMetadata;
211
212 #[test]
213 fn test_apply_core_metadata() {
214 let content =
215 "---\ntype: wrong\ntitle: Wrong Title\ncustom: value\n---\n# Body\n";
216
217 let core = CoreMetadata {
218 note_type: Some("task".into()),
219 title: Some("Correct Title".into()),
220 task_id: Some("TST-001".into()),
221 project: Some("TST".into()),
222 ..Default::default()
223 };
224
225 let result = core.apply_to_content(content, None).unwrap();
226
227 assert!(result.contains("type: task"));
228 assert!(result.contains("title: Correct Title"));
229 assert!(result.contains("task-id: TST-001"));
230 assert!(result.contains("project: TST"));
231 assert!(result.contains("custom: value")); }
233}