Skip to main content

cognee_http_server/dto/
forget.rs

1//! DTOs for `POST /api/v1/forget`.
2
3use serde::{Deserialize, Deserializer, Serialize};
4use utoipa::ToSchema;
5use uuid::Uuid;
6
7/// Request body for `POST /api/v1/forget`. Python `InDTO` (camelCase wire).
8///
9/// Snake_case `data_id` is accepted as an inbound alias for compatibility
10/// with Python's `populate_by_name=True`.
11#[derive(Debug, Clone, Deserialize, ToSchema)]
12#[serde(rename_all = "camelCase")]
13pub struct ForgetPayloadDTO {
14    /// UUID of a specific data item to remove. Requires `dataset` to be set
15    /// when used (mode 1). Ignored when `everything=true`.
16    #[serde(default, alias = "data_id")]
17    pub data_id: Option<Uuid>,
18
19    /// Dataset name OR UUID. Set alone (mode 2) deletes the whole dataset.
20    /// Set with `data_id` (mode 1) deletes one item. Ignored when
21    /// `everything=true`.
22    #[serde(default)]
23    pub dataset: Option<DatasetRef>,
24
25    /// If true, delete everything the user owns (mode 3). Other fields ignored.
26    #[serde(default)]
27    pub everything: bool,
28}
29
30/// Accept either a UUID or a free-form dataset name.
31///
32/// Serializes/deserializes as a plain string; the `ToSchema` impl presents it
33/// as `type: string` in OpenAPI.
34#[derive(Debug, Clone)]
35pub enum DatasetRef {
36    Id(Uuid),
37    Name(String),
38}
39
40impl utoipa::ToSchema for DatasetRef {
41    fn name() -> std::borrow::Cow<'static, str> {
42        std::borrow::Cow::Borrowed("DatasetRef")
43    }
44}
45
46impl utoipa::PartialSchema for DatasetRef {
47    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
48        utoipa::openapi::RefOr::T(utoipa::openapi::Schema::Object(
49            utoipa::openapi::ObjectBuilder::new()
50                .schema_type(utoipa::openapi::schema::Type::String)
51                .description(Some("Dataset name or UUID string. UUID is tried first."))
52                .build(),
53        ))
54    }
55}
56
57impl<'de> Deserialize<'de> for DatasetRef {
58    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
59        // Try as UUID first; if parsing fails treat as a free-form name.
60        let s = String::deserialize(d)?;
61        match Uuid::parse_str(&s) {
62            Ok(u) => Ok(DatasetRef::Id(u)),
63            Err(_) => Ok(DatasetRef::Name(s)),
64        }
65    }
66}
67
68/// Response variants. Wire is snake_case (Python returns plain dicts, not `OutDTO`).
69#[derive(Debug, Clone, Serialize, ToSchema)]
70#[serde(rename_all = "snake_case")]
71pub struct ForgetDataItemResponse {
72    pub data_id: Uuid,
73    pub dataset_id: Uuid,
74    pub status: String, // "success"
75}
76
77#[derive(Debug, Clone, Serialize, ToSchema)]
78#[serde(rename_all = "snake_case")]
79pub struct ForgetDatasetResponse {
80    pub dataset_id: Uuid,
81    pub status: String,
82}
83
84#[derive(Debug, Clone, Serialize, ToSchema)]
85#[serde(rename_all = "snake_case")]
86pub struct ForgetEverythingResponse {
87    pub datasets_removed: usize,
88    pub status: String,
89}
90
91#[derive(Debug, Clone, Serialize, ToSchema)]
92#[serde(untagged)]
93pub enum ForgetResponseDTO {
94    DataItem(ForgetDataItemResponse),
95    Dataset(ForgetDatasetResponse),
96    Everything(ForgetEverythingResponse),
97}
98
99/// `{error}` envelope for 422 / 500.
100#[derive(Debug, Serialize, ToSchema)]
101pub struct ForgetErrorResponseDTO {
102    pub error: String,
103}
104
105impl ForgetPayloadDTO {
106    /// Cross-field validation. Returns the resolved mode or an error suitable
107    /// for 422 mapping.
108    pub fn resolve_mode(&self) -> Result<ForgetMode, &'static str> {
109        if self.everything {
110            return Ok(ForgetMode::Everything);
111        }
112        match (&self.data_id, &self.dataset) {
113            (Some(_), Some(_)) => Ok(ForgetMode::DataItem),
114            (None, Some(_)) => Ok(ForgetMode::Dataset),
115            (Some(_), None) => Err("data_id requires dataset to be specified."),
116            (None, None) => Err("Specify dataset, data_id+dataset, or everything=True."),
117        }
118    }
119}
120
121#[derive(Debug, Clone, Copy)]
122pub enum ForgetMode {
123    DataItem,
124    Dataset,
125    Everything,
126}
127
128#[cfg(test)]
129#[allow(
130    clippy::unwrap_used,
131    clippy::expect_used,
132    reason = "test code — panics are acceptable failures"
133)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn resolve_mode_everything_ignores_other_fields() {
139        let dto = ForgetPayloadDTO {
140            data_id: Some(Uuid::new_v4()),
141            dataset: Some(DatasetRef::Name("foo".into())),
142            everything: true,
143        };
144        assert!(matches!(dto.resolve_mode(), Ok(ForgetMode::Everything)));
145    }
146
147    #[test]
148    fn resolve_mode_data_item() {
149        let dto = ForgetPayloadDTO {
150            data_id: Some(Uuid::new_v4()),
151            dataset: Some(DatasetRef::Name("foo".into())),
152            everything: false,
153        };
154        assert!(matches!(dto.resolve_mode(), Ok(ForgetMode::DataItem)));
155    }
156
157    #[test]
158    fn resolve_mode_dataset_only() {
159        let dto = ForgetPayloadDTO {
160            data_id: None,
161            dataset: Some(DatasetRef::Name("foo".into())),
162            everything: false,
163        };
164        assert!(matches!(dto.resolve_mode(), Ok(ForgetMode::Dataset)));
165    }
166
167    #[test]
168    fn resolve_mode_data_id_without_dataset_errors() {
169        let dto = ForgetPayloadDTO {
170            data_id: Some(Uuid::new_v4()),
171            dataset: None,
172            everything: false,
173        };
174        assert!(dto.resolve_mode().is_err());
175    }
176
177    #[test]
178    fn resolve_mode_nothing_errors() {
179        let dto = ForgetPayloadDTO {
180            data_id: None,
181            dataset: None,
182            everything: false,
183        };
184        assert!(dto.resolve_mode().is_err());
185    }
186
187    #[test]
188    fn dataset_ref_deserialize_uuid() {
189        let uuid = Uuid::new_v4();
190        let s = format!("\"{uuid}\"");
191        let parsed: DatasetRef = serde_json::from_str(&s).unwrap();
192        assert!(matches!(parsed, DatasetRef::Id(_)));
193    }
194
195    #[test]
196    fn dataset_ref_deserialize_name() {
197        let parsed: DatasetRef = serde_json::from_str("\"my_dataset\"").unwrap();
198        assert!(matches!(parsed, DatasetRef::Name(_)));
199    }
200}