1use std::{collections::BTreeMap, sync::Arc};
9
10use derive_more::{Display, From};
11#[cfg(feature = "schemars")]
12use schemars::Schema;
13use serde::{Deserialize, Serialize};
14use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
15
16use super::Meta;
17use crate::IntoOption;
18
19#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
22#[serde(transparent)]
23#[from(forward)]
24#[non_exhaustive]
25pub struct PlanId(pub Arc<str>);
26
27impl PlanId {
28 #[must_use]
30 pub fn new(id: impl Into<Self>) -> Self {
31 id.into()
32 }
33}
34
35#[serde_as]
37#[skip_serializing_none]
38#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40#[serde(rename_all = "camelCase")]
41#[non_exhaustive]
42pub struct PlanUpdate {
43 pub plan: PlanUpdateContent,
45 #[serde_as(deserialize_as = "DefaultOnError")]
51 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
52 #[serde(default)]
53 #[serde(rename = "_meta")]
54 pub meta: Option<Meta>,
55}
56
57impl PlanUpdate {
58 #[must_use]
60 pub fn new(plan: PlanUpdateContent) -> Self {
61 Self { plan, meta: None }
62 }
63
64 #[must_use]
70 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
71 self.meta = meta.into_option();
72 self
73 }
74}
75
76#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79#[serde(tag = "type", rename_all = "snake_case")]
80#[non_exhaustive]
81pub enum PlanUpdateContent {
82 Items(PlanItems),
84 #[cfg(feature = "unstable_plan_operations")]
90 File(PlanFile),
91 #[cfg(feature = "unstable_plan_operations")]
97 Markdown(PlanMarkdown),
98 #[serde(untagged)]
109 Other(OtherPlanUpdateContent),
110}
111
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
114#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
115#[cfg_attr(feature = "schemars", schemars(inline))]
116#[cfg_attr(feature = "schemars", schemars(transform = other_plan_update_content_schema))]
117#[serde(rename_all = "camelCase")]
118#[non_exhaustive]
119pub struct OtherPlanUpdateContent {
120 #[serde(rename = "type")]
126 pub type_: String,
127 pub plan_id: PlanId,
129 #[serde(flatten)]
131 pub fields: BTreeMap<String, serde_json::Value>,
132}
133
134impl OtherPlanUpdateContent {
135 #[must_use]
137 pub fn new(
138 type_: impl Into<String>,
139 plan_id: impl Into<PlanId>,
140 mut fields: BTreeMap<String, serde_json::Value>,
141 ) -> Self {
142 fields.remove("type");
143 fields.remove("planId");
144 Self {
145 type_: type_.into(),
146 plan_id: plan_id.into(),
147 fields,
148 }
149 }
150}
151
152impl<'de> Deserialize<'de> for OtherPlanUpdateContent {
153 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
154 where
155 D: serde::Deserializer<'de>,
156 {
157 let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
158 let type_ = fields
159 .remove("type")
160 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
161 let serde_json::Value::String(type_) = type_ else {
162 return Err(serde::de::Error::custom("`type` must be a string"));
163 };
164 let plan_id = fields
165 .remove("planId")
166 .ok_or_else(|| serde::de::Error::missing_field("planId"))?;
167 let serde_json::Value::String(plan_id) = plan_id else {
168 return Err(serde::de::Error::custom("`planId` must be a string"));
169 };
170
171 if is_known_plan_update_content_type(&type_) {
172 return Err(serde::de::Error::custom(format!(
173 "known plan update content `{type_}` did not match its schema"
174 )));
175 }
176
177 Ok(Self {
178 type_,
179 plan_id: PlanId::new(plan_id),
180 fields,
181 })
182 }
183}
184
185fn is_known_plan_update_content_type(type_: &str) -> bool {
186 KNOWN_PLAN_UPDATE_CONTENT_TYPES.contains(&type_)
187}
188
189#[cfg(feature = "schemars")]
190fn other_plan_update_content_schema(schema: &mut Schema) {
191 super::schema_util::reject_known_string_discriminators(
192 schema,
193 "type",
194 KNOWN_PLAN_UPDATE_CONTENT_TYPES,
195 );
196}
197
198const KNOWN_PLAN_UPDATE_CONTENT_TYPES: &[&str] = &[
199 "items",
200 #[cfg(feature = "unstable_plan_operations")]
201 "file",
202 #[cfg(feature = "unstable_plan_operations")]
203 "markdown",
204];
205
206impl PlanUpdateContent {
207 #[must_use]
209 pub fn items(plan_id: impl Into<PlanId>, entries: Vec<PlanEntry>) -> Self {
210 Self::Items(PlanItems::new(plan_id, entries))
211 }
212
213 #[cfg(feature = "unstable_plan_operations")]
215 #[must_use]
216 pub fn file(plan_id: impl Into<PlanId>, uri: impl Into<String>) -> Self {
217 Self::File(PlanFile::new(plan_id, uri))
218 }
219
220 #[cfg(feature = "unstable_plan_operations")]
222 #[must_use]
223 pub fn markdown(plan_id: impl Into<PlanId>, content: impl Into<String>) -> Self {
224 Self::Markdown(PlanMarkdown::new(plan_id, content))
225 }
226}
227
228#[serde_as]
230#[skip_serializing_none]
231#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
233#[serde(rename_all = "camelCase")]
234#[non_exhaustive]
235pub struct PlanItems {
236 pub plan_id: PlanId,
238 #[serde_as(deserialize_as = "VecSkipError<_>")]
243 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-skip-invalid-items" = true)))]
244 pub entries: Vec<PlanEntry>,
245 #[serde_as(deserialize_as = "DefaultOnError")]
251 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
252 #[serde(default)]
253 #[serde(rename = "_meta")]
254 pub meta: Option<Meta>,
255}
256
257impl PlanItems {
258 #[must_use]
260 pub fn new(plan_id: impl Into<PlanId>, entries: Vec<PlanEntry>) -> Self {
261 Self {
262 plan_id: plan_id.into(),
263 entries,
264 meta: None,
265 }
266 }
267
268 #[must_use]
274 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
275 self.meta = meta.into_option();
276 self
277 }
278}
279
280#[cfg(feature = "unstable_plan_operations")]
286#[serde_as]
287#[skip_serializing_none]
288#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
289#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
290#[serde(rename_all = "camelCase")]
291#[non_exhaustive]
292pub struct PlanFile {
293 pub plan_id: PlanId,
295 #[cfg_attr(feature = "schemars", schemars(url))]
297 pub uri: String,
298 #[serde_as(deserialize_as = "DefaultOnError")]
304 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
305 #[serde(default)]
306 #[serde(rename = "_meta")]
307 pub meta: Option<Meta>,
308}
309
310#[cfg(feature = "unstable_plan_operations")]
311impl PlanFile {
312 #[must_use]
314 pub fn new(plan_id: impl Into<PlanId>, uri: impl Into<String>) -> Self {
315 Self {
316 plan_id: plan_id.into(),
317 uri: uri.into(),
318 meta: None,
319 }
320 }
321
322 #[must_use]
328 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
329 self.meta = meta.into_option();
330 self
331 }
332}
333
334#[cfg(feature = "unstable_plan_operations")]
340#[serde_as]
341#[skip_serializing_none]
342#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
343#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
344#[serde(rename_all = "camelCase")]
345#[non_exhaustive]
346pub struct PlanMarkdown {
347 pub plan_id: PlanId,
349 pub content: String,
351 #[serde_as(deserialize_as = "DefaultOnError")]
357 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
358 #[serde(default)]
359 #[serde(rename = "_meta")]
360 pub meta: Option<Meta>,
361}
362
363#[cfg(feature = "unstable_plan_operations")]
364impl PlanMarkdown {
365 #[must_use]
367 pub fn new(plan_id: impl Into<PlanId>, content: impl Into<String>) -> Self {
368 Self {
369 plan_id: plan_id.into(),
370 content: content.into(),
371 meta: None,
372 }
373 }
374
375 #[must_use]
381 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
382 self.meta = meta.into_option();
383 self
384 }
385}
386
387#[cfg(feature = "unstable_plan_operations")]
393#[serde_as]
394#[skip_serializing_none]
395#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
397#[serde(rename_all = "camelCase")]
398#[non_exhaustive]
399pub struct PlanRemoved {
400 pub plan_id: PlanId,
402 #[serde_as(deserialize_as = "DefaultOnError")]
408 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
409 #[serde(default)]
410 #[serde(rename = "_meta")]
411 pub meta: Option<Meta>,
412}
413
414#[cfg(feature = "unstable_plan_operations")]
415impl PlanRemoved {
416 #[must_use]
418 pub fn new(plan_id: impl Into<PlanId>) -> Self {
419 Self {
420 plan_id: plan_id.into(),
421 meta: None,
422 }
423 }
424
425 #[must_use]
431 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
432 self.meta = meta.into_option();
433 self
434 }
435}
436
437#[serde_as]
443#[skip_serializing_none]
444#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
445#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
446#[serde(rename_all = "camelCase")]
447#[non_exhaustive]
448pub struct PlanEntry {
449 pub content: String,
451 pub priority: PlanEntryPriority,
454 pub status: PlanEntryStatus,
456 #[serde_as(deserialize_as = "DefaultOnError")]
462 #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
463 #[serde(default)]
464 #[serde(rename = "_meta")]
465 pub meta: Option<Meta>,
466}
467
468impl PlanEntry {
469 #[must_use]
471 pub fn new(
472 content: impl Into<String>,
473 priority: PlanEntryPriority,
474 status: PlanEntryStatus,
475 ) -> Self {
476 Self {
477 content: content.into(),
478 priority,
479 status,
480 meta: None,
481 }
482 }
483
484 #[must_use]
490 pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
491 self.meta = meta.into_option();
492 self
493 }
494}
495
496#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
502#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
503#[serde(rename_all = "snake_case")]
504#[non_exhaustive]
505pub enum PlanEntryPriority {
506 High,
508 Medium,
510 Low,
512 #[serde(untagged)]
518 Other(String),
519}
520
521#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
526#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
527#[serde(rename_all = "snake_case")]
528#[non_exhaustive]
529pub enum PlanEntryStatus {
530 Pending,
532 InProgress,
534 Completed,
536 Cancelled,
538 #[serde(untagged)]
544 Other(String),
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 #[test]
552 fn plan_entry_priority_preserves_unknown_variant() {
553 let priority: PlanEntryPriority = serde_json::from_str("\"urgent\"").unwrap();
554 assert_eq!(priority, PlanEntryPriority::Other("urgent".to_string()));
555 assert_eq!(serde_json::to_value(&priority).unwrap(), "urgent");
556 }
557
558 #[test]
559 fn plan_entry_status_preserves_unknown_variant() {
560 let status: PlanEntryStatus = serde_json::from_str("\"blocked\"").unwrap();
561 assert_eq!(status, PlanEntryStatus::Other("blocked".to_string()));
562 assert_eq!(serde_json::to_value(&status).unwrap(), "blocked");
563 }
564
565 #[test]
566 fn plan_entry_status_recognizes_cancelled_variant() {
567 let status: PlanEntryStatus = serde_json::from_str("\"cancelled\"").unwrap();
568 assert_eq!(status, PlanEntryStatus::Cancelled);
569 assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
570 }
571
572 #[test]
573 fn plan_update_content_preserves_unknown_variant() {
574 let content: PlanUpdateContent = serde_json::from_value(serde_json::json!({
575 "type": "_timeline",
576 "planId": "plan-1",
577 "events": []
578 }))
579 .unwrap();
580
581 let PlanUpdateContent::Other(unknown) = content else {
582 panic!("expected unknown plan update content");
583 };
584
585 assert_eq!(unknown.type_, "_timeline");
586 assert_eq!(unknown.plan_id.to_string(), "plan-1");
587 assert!(!unknown.fields.contains_key("planId"));
588 assert_eq!(
589 serde_json::to_value(PlanUpdateContent::Other(unknown)).unwrap(),
590 serde_json::json!({
591 "type": "_timeline",
592 "planId": "plan-1",
593 "events": []
594 })
595 );
596 }
597
598 #[test]
599 fn plan_update_content_does_not_hide_malformed_known_variant() {
600 assert!(
601 serde_json::from_value::<PlanUpdateContent>(serde_json::json!({
602 "type": "items"
603 }))
604 .is_err()
605 );
606 }
607
608 #[cfg(feature = "unstable_plan_operations")]
609 #[test]
610 fn plan_update_content_does_not_hide_malformed_known_experimental_variant() {
611 assert!(
612 serde_json::from_value::<PlanUpdateContent>(serde_json::json!({
613 "type": "file",
614 "planId": "plan-1"
615 }))
616 .is_err()
617 );
618 assert!(
619 serde_json::from_value::<PlanUpdateContent>(serde_json::json!({
620 "type": "markdown",
621 "planId": "plan-1"
622 }))
623 .is_err()
624 );
625 }
626
627 #[test]
628 fn plan_update_content_requires_id_for_unknown_variant() {
629 assert!(
630 serde_json::from_value::<PlanUpdateContent>(serde_json::json!({
631 "type": "_timeline"
632 }))
633 .is_err()
634 );
635 }
636}