Skip to main content

mdvault_core/domain/
context.rs

1//! Context types for note creation.
2//!
3//! These types carry state through the note creation lifecycle.
4
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use chrono::NaiveDate;
10
11use crate::config::types::ResolvedConfig;
12use crate::frontmatter::{Frontmatter, ParsedDocument, parse, serialize_with_order};
13use crate::templates::repository::LoadedTemplate;
14use crate::types::{TypeDefinition, TypeRegistry};
15
16/// Core metadata fields managed by Rust.
17/// These fields are authoritative and survive template/hook modifications.
18#[derive(Debug, Clone, Default)]
19pub struct CoreMetadata {
20    pub note_type: Option<String>,
21    pub title: Option<String>,
22    pub project_id: Option<String>,
23    pub task_id: Option<String>,
24    pub meeting_id: Option<String>,
25    pub task_counter: Option<u32>,
26    pub project: Option<String>, // Parent project for tasks
27    pub date: Option<String>,    // For daily/meeting notes
28    pub week: Option<String>,    // For weekly notes
29}
30
31impl CoreMetadata {
32    /// Convert to HashMap for merging into frontmatter.
33    pub fn to_yaml_map(&self) -> HashMap<String, serde_yaml::Value> {
34        let mut map = HashMap::new();
35        if let Some(ref t) = self.note_type {
36            map.insert("type".into(), serde_yaml::Value::String(t.clone()));
37        }
38        if let Some(ref t) = self.title {
39            map.insert("title".into(), serde_yaml::Value::String(t.clone()));
40        }
41        if let Some(ref id) = self.project_id {
42            map.insert("project-id".into(), serde_yaml::Value::String(id.clone()));
43        }
44        if let Some(ref id) = self.task_id {
45            map.insert("task-id".into(), serde_yaml::Value::String(id.clone()));
46        }
47        if let Some(ref id) = self.meeting_id {
48            map.insert("meeting-id".into(), serde_yaml::Value::String(id.clone()));
49        }
50        if let Some(counter) = self.task_counter {
51            map.insert("task_counter".into(), serde_yaml::Value::Number(counter.into()));
52        }
53        if let Some(ref p) = self.project {
54            map.insert("project".into(), serde_yaml::Value::String(p.clone()));
55        }
56        if let Some(ref d) = self.date {
57            map.insert("date".into(), serde_yaml::Value::String(d.clone()));
58        }
59        if let Some(ref w) = self.week {
60            map.insert("week".into(), serde_yaml::Value::String(w.clone()));
61        }
62        map
63    }
64
65    /// Apply core metadata fields to note content, overwriting any existing values.
66    ///
67    /// Parses frontmatter, merges core fields (which are authoritative),
68    /// then re-serializes with optional field ordering.
69    pub fn apply_to_content(
70        &self,
71        content: &str,
72        field_order: Option<&[String]>,
73    ) -> Result<String, String> {
74        let parsed = parse(content).map_err(|e| e.to_string())?;
75        let mut fields = parsed.frontmatter.map(|fm| fm.fields).unwrap_or_default();
76        fields.extend(self.to_yaml_map());
77        let doc = ParsedDocument {
78            frontmatter: Some(Frontmatter { fields }),
79            body: parsed.body,
80        };
81        Ok(serialize_with_order(&doc, field_order))
82    }
83}
84
85/// Context available during note creation.
86pub struct CreationContext<'a> {
87    // Core inputs
88    pub title: String,
89    pub type_name: String,
90
91    // Configuration
92    pub config: &'a ResolvedConfig,
93    pub typedef: Option<Arc<TypeDefinition>>,
94    pub registry: &'a TypeRegistry,
95
96    // State (accumulated during creation)
97    pub vars: HashMap<String, String>,
98    pub core_metadata: CoreMetadata,
99
100    // Output state
101    pub output_path: Option<PathBuf>,
102
103    // Template (if using template-based creation)
104    pub template: Option<LoadedTemplate>,
105
106    // Mode flags
107    pub batch_mode: bool,
108
109    // Reference date for date expressions (overrides "today" in templates)
110    pub reference_date: Option<NaiveDate>,
111}
112
113impl<'a> CreationContext<'a> {
114    /// Create a new creation context.
115    pub fn new(
116        type_name: &str,
117        title: &str,
118        config: &'a ResolvedConfig,
119        registry: &'a TypeRegistry,
120    ) -> Self {
121        let typedef = registry.get(type_name);
122
123        let core_metadata = CoreMetadata {
124            note_type: Some(type_name.to_string()),
125            title: Some(title.to_string()),
126            ..Default::default()
127        };
128
129        let vars = HashMap::from([
130            ("title".to_string(), title.to_string()),
131            ("type".to_string(), type_name.to_string()),
132        ]);
133
134        Self {
135            title: title.to_string(),
136            type_name: type_name.to_string(),
137            config,
138            typedef,
139            registry,
140            vars,
141            core_metadata,
142            output_path: None,
143            template: None,
144            batch_mode: false,
145            reference_date: None,
146        }
147    }
148
149    /// Add CLI-provided variables.
150    pub fn with_vars(mut self, cli_vars: HashMap<String, String>) -> Self {
151        self.vars.extend(cli_vars);
152        self
153    }
154
155    /// Set a template to use for content generation.
156    pub fn with_template(mut self, template: LoadedTemplate) -> Self {
157        self.template = Some(template);
158        self
159    }
160
161    /// Set batch mode flag.
162    pub fn with_batch_mode(mut self, batch: bool) -> Self {
163        self.batch_mode = batch;
164        self
165    }
166
167    /// Get a variable value.
168    pub fn get_var(&self, key: &str) -> Option<&str> {
169        self.vars.get(key).map(|s| s.as_str())
170    }
171
172    /// Set a variable value.
173    pub fn set_var(&mut self, key: impl Into<String>, value: impl Into<String>) {
174        self.vars.insert(key.into(), value.into());
175    }
176
177    /// Create a PromptContext from this CreationContext.
178    pub fn to_prompt_context(&self) -> PromptContext<'_> {
179        PromptContext {
180            config: self.config,
181            type_name: &self.type_name,
182            title: &self.title,
183            provided_vars: &self.vars,
184            batch_mode: self.batch_mode,
185        }
186    }
187}
188
189/// Context for determining prompts.
190pub struct PromptContext<'a> {
191    pub config: &'a ResolvedConfig,
192    pub type_name: &'a str,
193    pub title: &'a str,
194    pub provided_vars: &'a HashMap<String, String>,
195    pub batch_mode: bool,
196}
197
198/// A single field prompt specification.
199#[derive(Debug, Clone)]
200pub struct FieldPrompt {
201    pub field_name: String,
202    pub prompt_text: String,
203    pub prompt_type: PromptType,
204    pub required: bool,
205    pub default_value: Option<String>,
206}
207
208/// Type of prompt to display.
209#[derive(Debug, Clone)]
210pub enum PromptType {
211    /// Single-line text input.
212    Text,
213    /// Multi-line text input.
214    Multiline,
215    /// Selection from a list of options.
216    Select(Vec<String>),
217    /// Special: pick from indexed projects.
218    ProjectSelector,
219}