kasl/db/templates.rs
1//! Reusable task templates: named blueprints tasks are created from.
2//!
3//! ```rust,no_run
4//! # fn main() -> anyhow::Result<()> {
5//! use kasl::db::templates::{Templates, TaskTemplate};
6//!
7//! let mut templates = Templates::new()?;
8//! let template = TaskTemplate::new(
9//! "daily-standup".to_string(),
10//! "Prepare for daily standup".to_string(),
11//! "Review yesterday's work and plan today".to_string(),
12//! 50
13//! );
14//! templates.create(&template)?;
15//! # Ok(())
16//! # }
17//! ```
18
19use crate::db::db::Db;
20use crate::libs::messages::Message;
21use crate::msg_error_anyhow;
22use anyhow::Result;
23use rusqlite::{Connection, params};
24use serde::{Deserialize, Serialize};
25
26const SCHEMA_TEMPLATES: &str = "CREATE TABLE IF NOT EXISTS task_templates (
27 id INTEGER PRIMARY KEY,
28 name TEXT NOT NULL UNIQUE,
29 task_name TEXT NOT NULL,
30 comment TEXT,
31 completeness INTEGER DEFAULT 100,
32 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
33)";
34
35const INSERT_TEMPLATE: &str = "INSERT INTO task_templates (name, task_name, comment, completeness) VALUES (?1, ?2, ?3, ?4)";
36const UPDATE_TEMPLATE: &str = "UPDATE task_templates SET task_name = ?2, comment = ?3, completeness = ?4 WHERE name = ?1";
37const DELETE_TEMPLATE: &str = "DELETE FROM task_templates WHERE name = ?1";
38const SELECT_ALL_TEMPLATES: &str = "SELECT * FROM task_templates ORDER BY name";
39const SELECT_TEMPLATE_BY_NAME: &str = "SELECT * FROM task_templates WHERE name = ?1";
40const SEARCH_TEMPLATES: &str = "SELECT * FROM task_templates WHERE name LIKE ?1 OR task_name LIKE ?1 ORDER BY name";
41
42/// A task blueprint. `name` identifies the template ("code-review");
43/// `task_name` becomes the created task's title ("Review PR #123").
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct TaskTemplate {
46 /// Database primary key; `None` until saved.
47 pub id: Option<i32>,
48
49 /// Unique template name users type to select it.
50 pub name: String,
51
52 /// Title given to tasks created from this template.
53 pub task_name: String,
54
55 /// Default notes for created tasks.
56 pub comment: String,
57
58 /// Default completeness for created tasks.
59 pub completeness: i32,
60
61 /// Set by the database on insert.
62 pub created_at: Option<String>,
63}
64
65impl TaskTemplate {
66 /// Builds an unsaved template; id and timestamp are assigned on `create`.
67 ///
68 /// ```rust
69 /// use kasl::db::templates::TaskTemplate;
70 ///
71 /// let template = TaskTemplate::new(
72 /// "morning-routine".to_string(),
73 /// "Complete morning routine".to_string(),
74 /// "Check emails, review calendar, plan day".to_string(),
75 /// 25 // Start at 25% since some prep is already done
76 /// );
77 /// # let _ = template;
78 /// ```
79 pub fn new(name: String, task_name: String, comment: String, completeness: i32) -> Self {
80 Self {
81 id: None,
82 name,
83 task_name,
84 comment,
85 completeness,
86 created_at: None,
87 }
88 }
89}
90
91/// Template table access.
92pub struct Templates {
93 conn: Connection,
94}
95
96impl Templates {
97 /// Opens the database and ensures the templates table exists
98 /// (migration v2 creates it officially).
99 ///
100 /// ```rust,no_run
101 /// # fn main() -> anyhow::Result<()> {
102 /// use kasl::db::templates::Templates;
103 ///
104 /// let mut templates = Templates::new()?;
105 /// # Ok(())
106 /// # }
107 /// ```
108 pub fn new() -> Result<Self> {
109 let db = Db::new()?;
110 db.conn.execute(SCHEMA_TEMPLATES, [])?;
111 Ok(Templates { conn: db.conn })
112 }
113
114 /// Inserts the template; the unique name constraint rejects duplicates.
115 ///
116 /// ```rust,no_run
117 /// # fn main() -> anyhow::Result<()> {
118 /// use kasl::db::templates::{Templates, TaskTemplate};
119 ///
120 /// let mut templates = Templates::new()?;
121 /// let template = TaskTemplate::new(
122 /// "weekly-review".to_string(),
123 /// "Weekly review and planning".to_string(),
124 /// "Review accomplishments and plan next week".to_string(),
125 /// 0
126 /// );
127 /// templates.create(&template)?;
128 /// # Ok(())
129 /// # }
130 /// ```
131 pub fn create(&mut self, template: &TaskTemplate) -> Result<()> {
132 let affected = self.conn.execute(
133 INSERT_TEMPLATE,
134 params![template.name, template.task_name, template.comment, template.completeness],
135 )?;
136
137 if affected == 0 {
138 return Err(msg_error_anyhow!(Message::TemplateCreateFailed));
139 }
140
141 Ok(())
142 }
143
144 /// Updates the template's content by name; errors if it does not exist.
145 pub fn update(&mut self, template: &TaskTemplate) -> Result<()> {
146 let affected = self.conn.execute(
147 UPDATE_TEMPLATE,
148 params![template.name, template.task_name, template.comment, template.completeness],
149 )?;
150
151 if affected == 0 {
152 return Err(msg_error_anyhow!(Message::TemplateNotFound(template.name.clone())));
153 }
154
155 Ok(())
156 }
157
158 /// Deletes the template by name; tasks created from it are unaffected.
159 ///
160 /// ```rust,no_run
161 /// # use kasl::db::templates::Templates;
162 /// # fn main() -> anyhow::Result<()> {
163 /// let mut templates = Templates::new()?;
164 /// templates.delete("obsolete-template")?;
165 /// # Ok(())
166 /// # }
167 /// ```
168 pub fn delete(&mut self, name: &str) -> Result<()> {
169 let affected = self.conn.execute(DELETE_TEMPLATE, params![name])?;
170
171 if affected == 0 {
172 return Err(msg_error_anyhow!(Message::TemplateNotFound(name.to_string())));
173 }
174
175 Ok(())
176 }
177
178 /// Returns every template, sorted by name.
179 ///
180 /// ```rust,no_run
181 /// # use kasl::db::templates::Templates;
182 /// # fn main() -> anyhow::Result<()> {
183 /// let mut templates = Templates::new()?;
184 /// let all_templates = templates.get_all()?;
185 /// for template in all_templates {
186 /// println!("Template: {} -> {}", template.name, template.task_name);
187 /// }
188 /// # Ok(())
189 /// # }
190 /// ```
191 pub fn get_all(&mut self) -> Result<Vec<TaskTemplate>> {
192 let mut stmt = self.conn.prepare(SELECT_ALL_TEMPLATES)?;
193 let template_iter = stmt.query_map([], |row| {
194 Ok(TaskTemplate {
195 id: row.get(0)?,
196 name: row.get(1)?,
197 task_name: row.get(2)?,
198 comment: row.get(3)?,
199 completeness: row.get(4)?,
200 created_at: row.get(5)?,
201 })
202 })?;
203
204 let mut templates = Vec::new();
205 for template in template_iter {
206 templates.push(template?);
207 }
208 Ok(templates)
209 }
210
211 /// Fetches one template by exact name; `search` is the fuzzy variant.
212 ///
213 /// ```rust,no_run
214 /// # use kasl::db::templates::Templates;
215 /// # fn main() -> anyhow::Result<()> {
216 /// let mut templates = Templates::new()?;
217 /// if let Some(template) = templates.get("daily-standup")? {
218 /// println!("Found template: {}", template.task_name);
219 /// } else {
220 /// println!("Template 'daily-standup' not found");
221 /// }
222 /// # Ok(())
223 /// # }
224 /// ```
225 pub fn get(&mut self, name: &str) -> Result<Option<TaskTemplate>> {
226 let mut stmt = self.conn.prepare(SELECT_TEMPLATE_BY_NAME)?;
227 let mut template_iter = stmt.query_map(params![name], |row| {
228 Ok(TaskTemplate {
229 id: row.get(0)?,
230 name: row.get(1)?,
231 task_name: row.get(2)?,
232 comment: row.get(3)?,
233 completeness: row.get(4)?,
234 created_at: row.get(5)?,
235 })
236 })?;
237
238 match template_iter.next() {
239 Some(Ok(template)) => Ok(Some(template)),
240 Some(Err(e)) => Err(e.into()),
241 None => Ok(None),
242 }
243 }
244
245 /// Substring search over template names and task names, sorted by name.
246 ///
247 /// ```rust,no_run
248 /// # use kasl::db::templates::Templates;
249 /// # fn main() -> anyhow::Result<()> {
250 /// let mut templates = Templates::new()?;
251 ///
252 /// let review_templates = templates.search("review")?;
253 /// let standup_templates = templates.search("standup")?;
254 ///
255 /// for template in review_templates {
256 /// println!("Found: {} -> {}", template.name, template.task_name);
257 /// }
258 /// # let _ = standup_templates;
259 /// # Ok(())
260 /// # }
261 /// ```
262 pub fn search(&mut self, query: &str) -> Result<Vec<TaskTemplate>> {
263 let search_pattern = format!("%{}%", query);
264 let mut stmt = self.conn.prepare(SEARCH_TEMPLATES)?;
265
266 let template_iter = stmt.query_map(params![search_pattern], |row| {
267 Ok(TaskTemplate {
268 id: row.get(0)?,
269 name: row.get(1)?,
270 task_name: row.get(2)?,
271 comment: row.get(3)?,
272 completeness: row.get(4)?,
273 created_at: row.get(5)?,
274 })
275 })?;
276
277 let mut templates = Vec::new();
278 for template in template_iter {
279 templates.push(template?);
280 }
281
282 Ok(templates)
283 }
284
285 /// True when a template with this exact name exists.
286 ///
287 /// ```rust,no_run
288 /// # use kasl::db::templates::{Templates, TaskTemplate};
289 /// # fn main() -> anyhow::Result<()> {
290 /// let mut templates = Templates::new()?;
291 /// if templates.exists("daily-standup")? {
292 /// println!("Template already exists");
293 /// } else {
294 /// let template = TaskTemplate::new(
295 /// "daily-standup".to_string(),
296 /// "Prepare for daily standup".to_string(),
297 /// "Review progress and plan".to_string(),
298 /// 0
299 /// );
300 /// templates.create(&template)?;
301 /// }
302 /// # Ok(())
303 /// # }
304 /// ```
305 pub fn exists(&mut self, name: &str) -> Result<bool> {
306 Ok(self.get(name)?.is_some())
307 }
308}