#[cfg(feature = "unstable_plan_operations")]
use std::{collections::BTreeMap, sync::Arc};
#[cfg(feature = "unstable_plan_operations")]
use derive_more::{Display, From};
use schemars::JsonSchema;
#[cfg(feature = "unstable_plan_operations")]
use schemars::Schema;
use serde::{Deserialize, Serialize};
use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
use super::Meta;
use crate::{IntoOption, SkipListener};
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Plan {
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
pub entries: Vec<PlanEntry>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl Plan {
#[must_use]
pub fn new(entries: Vec<PlanEntry>) -> Self {
Self {
entries,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_plan_operations")]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct PlanId(pub Arc<str>);
#[cfg(feature = "unstable_plan_operations")]
impl PlanId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
}
#[cfg(feature = "unstable_plan_operations")]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PlanUpdate {
pub plan: PlanUpdateContent,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_plan_operations")]
impl PlanUpdate {
#[must_use]
pub fn new(plan: PlanUpdateContent) -> Self {
Self { plan, meta: None }
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_plan_operations")]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
#[schemars(extend("discriminator" = {"propertyName": "type"}))]
#[non_exhaustive]
pub enum PlanUpdateContent {
Items(PlanItems),
File(PlanFile),
Markdown(PlanMarkdown),
#[serde(untagged)]
Other(OtherPlanUpdateContent),
}
#[cfg(feature = "unstable_plan_operations")]
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
#[schemars(inline)]
#[schemars(transform = other_plan_update_content_schema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OtherPlanUpdateContent {
#[serde(rename = "type")]
pub type_: String,
#[serde(flatten)]
pub fields: BTreeMap<String, serde_json::Value>,
}
#[cfg(feature = "unstable_plan_operations")]
impl OtherPlanUpdateContent {
#[must_use]
pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
fields.remove("type");
Self {
type_: type_.into(),
fields,
}
}
}
#[cfg(feature = "unstable_plan_operations")]
impl<'de> Deserialize<'de> for OtherPlanUpdateContent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
let type_ = fields
.remove("type")
.ok_or_else(|| serde::de::Error::missing_field("type"))?;
let serde_json::Value::String(type_) = type_ else {
return Err(serde::de::Error::custom("`type` must be a string"));
};
if is_known_plan_update_content_type(&type_) {
return Err(serde::de::Error::custom(format!(
"known plan update content `{type_}` did not match its schema"
)));
}
Ok(Self { type_, fields })
}
}
#[cfg(feature = "unstable_plan_operations")]
fn is_known_plan_update_content_type(type_: &str) -> bool {
matches!(type_, "items" | "file" | "markdown")
}
#[cfg(feature = "unstable_plan_operations")]
fn other_plan_update_content_schema(schema: &mut Schema) {
super::schema_util::reject_known_string_discriminators(
schema,
"type",
&["items", "file", "markdown"],
);
}
#[cfg(feature = "unstable_plan_operations")]
impl PlanUpdateContent {
#[must_use]
pub fn items(id: impl Into<PlanId>, entries: Vec<PlanEntry>) -> Self {
Self::Items(PlanItems::new(id, entries))
}
#[must_use]
pub fn file(id: impl Into<PlanId>, uri: impl Into<String>) -> Self {
Self::File(PlanFile::new(id, uri))
}
#[must_use]
pub fn markdown(id: impl Into<PlanId>, content: impl Into<String>) -> Self {
Self::Markdown(PlanMarkdown::new(id, content))
}
}
#[cfg(feature = "unstable_plan_operations")]
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PlanItems {
pub id: PlanId,
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
pub entries: Vec<PlanEntry>,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_plan_operations")]
impl PlanItems {
#[must_use]
pub fn new(id: impl Into<PlanId>, entries: Vec<PlanEntry>) -> Self {
Self {
id: id.into(),
entries,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_plan_operations")]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PlanFile {
pub id: PlanId,
pub uri: String,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_plan_operations")]
impl PlanFile {
#[must_use]
pub fn new(id: impl Into<PlanId>, uri: impl Into<String>) -> Self {
Self {
id: id.into(),
uri: uri.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_plan_operations")]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PlanMarkdown {
pub id: PlanId,
pub content: String,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_plan_operations")]
impl PlanMarkdown {
#[must_use]
pub fn new(id: impl Into<PlanId>, content: impl Into<String>) -> Self {
Self {
id: id.into(),
content: content.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_plan_operations")]
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PlanRemoved {
pub id: PlanId,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_plan_operations")]
impl PlanRemoved {
#[must_use]
pub fn new(id: impl Into<PlanId>) -> Self {
Self {
id: id.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[cfg(feature = "unstable_plan_operations")]
#[skip_serializing_none]
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PlanCapabilities {
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
#[cfg(feature = "unstable_plan_operations")]
impl PlanCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PlanEntry {
pub content: String,
pub priority: PlanEntryPriority,
pub status: PlanEntryStatus,
#[serde(rename = "_meta")]
pub meta: Option<Meta>,
}
impl PlanEntry {
#[must_use]
pub fn new(
content: impl Into<String>,
priority: PlanEntryPriority,
status: PlanEntryStatus,
) -> Self {
Self {
content: content.into(),
priority,
status,
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PlanEntryPriority {
High,
Medium,
Low,
#[serde(untagged)]
Other(String),
}
#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PlanEntryStatus {
Pending,
InProgress,
Completed,
#[serde(untagged)]
Other(String),
}
#[cfg(all(test, feature = "unstable_plan_operations"))]
mod tests {
use super::*;
#[test]
fn plan_entry_priority_preserves_unknown_variant() {
let priority: PlanEntryPriority = serde_json::from_str("\"urgent\"").unwrap();
assert_eq!(priority, PlanEntryPriority::Other("urgent".to_string()));
assert_eq!(serde_json::to_value(&priority).unwrap(), "urgent");
}
#[test]
fn plan_entry_status_preserves_unknown_variant() {
let status: PlanEntryStatus = serde_json::from_str("\"blocked\"").unwrap();
assert_eq!(status, PlanEntryStatus::Other("blocked".to_string()));
assert_eq!(serde_json::to_value(&status).unwrap(), "blocked");
}
#[test]
fn plan_update_content_preserves_unknown_variant() {
let content: PlanUpdateContent = serde_json::from_value(serde_json::json!({
"type": "_timeline",
"id": "plan-1",
"events": []
}))
.unwrap();
let PlanUpdateContent::Other(unknown) = content else {
panic!("expected unknown plan update content");
};
assert_eq!(unknown.type_, "_timeline");
assert_eq!(unknown.fields.get("id"), Some(&serde_json::json!("plan-1")));
assert_eq!(
serde_json::to_value(PlanUpdateContent::Other(unknown)).unwrap(),
serde_json::json!({
"type": "_timeline",
"id": "plan-1",
"events": []
})
);
}
#[test]
fn plan_update_content_does_not_hide_malformed_known_variant() {
assert!(
serde_json::from_value::<PlanUpdateContent>(serde_json::json!({
"type": "items"
}))
.is_err()
);
}
}