cognee_http_server/dto/notebooks.rs
1//! DTOs for the `/api/v1/notebooks` router.
2//!
3//! Wire shape mirrors Python's `cognee.modules.notebooks.models.Notebook`
4//! and the inline Pydantic classes in `get_notebooks_router.py`.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use utoipa::ToSchema;
9use uuid::Uuid;
10
11/// Mirrors `cognee.modules.notebooks.models.Notebook` (one row).
12///
13/// Wire format matches Python's default SQLAlchemy → JSON serialization:
14/// every column is emitted, `cells` is a JSON array, `created_at` is ISO-8601.
15#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
16pub struct NotebookDTO {
17 pub id: Uuid,
18 pub owner_id: Uuid,
19 pub name: String,
20 pub cells: Vec<NotebookCellDTO>,
21 pub deletable: bool,
22 pub created_at: DateTime<Utc>,
23}
24
25/// Mirrors `cognee.modules.notebooks.models.NotebookCell`.
26#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
27pub struct NotebookCellDTO {
28 pub id: Uuid,
29 /// `"markdown"` or `"code"`. String for wire compat with Python's
30 /// `Literal["markdown", "code"]`; a closed Rust enum would reject
31 /// unknown values that Python tolerates silently.
32 #[serde(rename = "type")]
33 pub kind: String,
34 pub name: String,
35 pub content: String,
36}
37
38/// Mirrors the inline `NotebookData(InDTO)` Pydantic class in
39/// [`get_notebooks_router.py:24-26`](https://github.com/topoteretes/cognee/blob/main/cognee/api/v1/notebooks/routers/get_notebooks_router.py#L24-L26).
40///
41/// `name` is `Option<String>` because Python uses
42/// `Optional[str] = Field(...)` (required at validation time but allowed
43/// to be `None`). Handlers validate that `name.is_some()` before use.
44#[derive(Debug, Deserialize, ToSchema)]
45pub struct NotebookDataDTO {
46 pub name: Option<String>,
47 #[serde(default)]
48 pub cells: Vec<NotebookCellDTO>,
49}
50
51/// Mirrors `RunCodeData(InDTO)` from
52/// [`get_notebooks_router.py:63-64`](https://github.com/topoteretes/cognee/blob/main/cognee/api/v1/notebooks/routers/get_notebooks_router.py#L63-L64).
53#[derive(Debug, Deserialize, ToSchema)]
54pub struct RunCodeDataDTO {
55 pub content: String,
56}
57
58/// Stage B outcome placeholder. Wire shape: `{"result": [...], "error": null|str}`.
59/// Shipped in Stage A so the OpenAPI document is forward-compatible.
60#[derive(Debug, Serialize, ToSchema)]
61pub struct RunCodeOutcomeDTO {
62 pub result: Vec<serde_json::Value>,
63 pub error: Option<String>,
64}
65
66// ─── Conversion helpers ───────────────────────────────────────────────────────
67
68impl NotebookDTO {
69 /// Convert from the database `Notebook` model.
70 ///
71 /// `cells` is stored as a raw `serde_json::Value` (array of objects);
72 /// we attempt to parse each element into `NotebookCellDTO`, silently
73 /// skipping malformed entries.
74 pub fn from_db(nb: cognee_database::Notebook) -> Self {
75 let cells = nb
76 .cells
77 .as_array()
78 .map(|arr| {
79 arr.iter()
80 .filter_map(|v| serde_json::from_value(v.clone()).ok())
81 .collect()
82 })
83 .unwrap_or_default();
84
85 Self {
86 id: nb.id,
87 owner_id: nb.owner_id,
88 name: nb.name,
89 cells,
90 deletable: nb.deletable,
91 created_at: nb.created_at,
92 }
93 }
94}
95
96// ─── Tests ───────────────────────────────────────────────────────────────────
97
98#[cfg(test)]
99#[allow(
100 clippy::unwrap_used,
101 clippy::expect_used,
102 reason = "test code — panics are acceptable failures"
103)]
104mod tests {
105 use super::*;
106 use serde_json::json;
107
108 #[test]
109 fn round_trip_notebook_dto() {
110 let input = json!({
111 "id": "00000000-0000-0000-0000-000000000001",
112 "owner_id": "00000000-0000-0000-0000-000000000002",
113 "name": "My Notebook",
114 "cells": [
115 {
116 "id": "00000000-0000-0000-0000-000000000003",
117 "type": "markdown",
118 "name": "intro",
119 "content": "# hi"
120 }
121 ],
122 "deletable": true,
123 "created_at": "2024-01-01T00:00:00Z"
124 });
125
126 let dto: NotebookDTO =
127 serde_json::from_value(input.clone()).expect("deserialize NotebookDTO");
128 let serialized = serde_json::to_value(&dto).expect("serialize NotebookDTO");
129
130 assert_eq!(serialized["name"], "My Notebook");
131 assert_eq!(serialized["cells"][0]["type"], "markdown");
132 assert_eq!(serialized["deletable"], true);
133 }
134}