things_mcp/core/writer/operation/
update_project.rs1use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct UpdateProjectSpec {
9 pub id: String,
10 pub title: Option<String>,
11 pub notes: Option<String>,
12 pub when: Option<String>,
13 pub deadline: Option<String>,
14 pub tags: Option<Vec<String>>,
15 pub area_id: Option<String>,
18 pub completed: Option<bool>,
19 pub canceled: Option<bool>,
20}
21
22pub(crate) fn render_update_project(spec: &UpdateProjectSpec) -> Value {
23 let mut attributes = serde_json::Map::new();
24 if let Some(v) = spec.title.as_ref() {
25 attributes.insert("title".into(), Value::String(v.clone()));
26 }
27 if let Some(v) = spec.notes.as_ref() {
28 attributes.insert("notes".into(), Value::String(v.clone()));
29 }
30 if let Some(v) = spec.when.as_ref() {
31 attributes.insert("when".into(), Value::String(v.clone()));
32 }
33 if let Some(v) = spec.deadline.as_ref() {
34 attributes.insert("deadline".into(), Value::String(v.clone()));
35 }
36 if let Some(tags) = spec.tags.as_ref() {
37 attributes.insert(
38 "tags".into(),
39 Value::Array(tags.iter().map(|t| Value::String(t.clone())).collect()),
40 );
41 }
42 if let Some(v) = spec.area_id.as_ref() {
43 attributes.insert("area-id".into(), Value::String(v.clone()));
44 }
45 if let Some(v) = spec.completed {
46 attributes.insert("completed".into(), Value::Bool(v));
47 }
48 if let Some(v) = spec.canceled {
49 attributes.insert("canceled".into(), Value::Bool(v));
50 }
51
52 json!({
53 "type": "project",
54 "operation": "update",
55 "id": spec.id,
56 "attributes": Value::Object(attributes),
57 })
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use crate::core::writer::operation::Operation;
64
65 #[test]
66 fn update_project_minimal_only_id() {
67 let op = Operation::UpdateProject(UpdateProjectSpec {
68 id: "proj-1".into(),
69 ..Default::default()
70 });
71 let v = op.render_json();
72 assert_eq!(v["type"], "project");
73 assert_eq!(v["operation"], "update");
74 assert_eq!(v["id"], "proj-1");
75 assert_eq!(v["attributes"].as_object().unwrap().len(), 0);
76 }
77
78 #[test]
79 fn update_project_full_renders_all_populated_fields() {
80 let op = Operation::UpdateProject(UpdateProjectSpec {
81 id: "proj-1".into(),
82 title: Some("Renamed".into()),
83 notes: Some("Updated notes".into()),
84 when: Some("today".into()),
85 deadline: Some("2026-12-31".into()),
86 tags: Some(vec!["Work".into()]),
87 area_id: Some("area-2".into()),
88 completed: Some(true),
89 canceled: None,
90 });
91 let v = op.render_json();
92 assert_eq!(v["id"], "proj-1");
93 let attrs = v["attributes"].as_object().unwrap();
94 assert_eq!(attrs["title"], "Renamed");
95 assert_eq!(attrs["notes"], "Updated notes");
96 assert_eq!(attrs["when"], "today");
97 assert_eq!(attrs["deadline"], "2026-12-31");
98 assert_eq!(attrs["tags"], serde_json::json!(["Work"]));
99 assert_eq!(attrs["area-id"], "area-2");
100 assert_eq!(attrs["completed"], true);
101 assert!(!attrs.contains_key("canceled"), "None should not render");
102 }
103}