Skip to main content

distri_types/api/
notes.rs

1//! Shared note DTOs used by both distri-server (OSS) and distri-cloud.
2
3use chrono::{DateTime, Utc};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use utoipa::ToSchema;
7use uuid::Uuid;
8
9/// Query parameters for listing notes.
10#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, JsonSchema)]
11pub struct ListNotesQuery {
12    /// Filter by tag
13    pub tag: Option<String>,
14    /// Full-text search on title and content
15    pub search: Option<String>,
16}
17
18/// Request body for creating a note.
19#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
20#[schema(example = json!({"title": "My Note", "content": "Hello world", "tags": ["work", "ideas"]}))]
21pub struct CreateNoteRequest {
22    pub title: String,
23    pub content: String,
24    #[serde(default)]
25    pub tags: Vec<String>,
26}
27
28/// Request body for updating a note.
29#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, JsonSchema)]
30pub struct UpdateNoteRequest {
31    pub title: Option<String>,
32    pub content: Option<String>,
33    pub tags: Option<Vec<String>>,
34}
35
36/// A persisted note record.
37#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
38pub struct NoteRecord {
39    pub id: Uuid,
40    pub workspace_id: Uuid,
41    pub title: String,
42    pub content: String,
43    #[serde(default)]
44    pub tags: Vec<String>,
45    pub created_by: Option<Uuid>,
46    pub created_at: DateTime<Utc>,
47    pub updated_at: DateTime<Utc>,
48}
49
50/// Response wrapper for listing notes.
51#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
52pub struct ListNotesResponse {
53    pub notes: Vec<NoteRecord>,
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    /// `NoteRecord` is the exact type the cloud handler serializes and the
61    /// distri client deserializes. Pin its snake_case wire shape so a rename
62    /// on either field fails here rather than silently at runtime.
63    #[test]
64    fn note_record_round_trips_snake_case() {
65        let note = NoteRecord {
66            id: Uuid::nil(),
67            workspace_id: Uuid::nil(),
68            title: "t".into(),
69            content: "c".into(),
70            tags: vec!["a".into()],
71            created_by: None,
72            created_at: DateTime::<Utc>::from_timestamp(0, 0).unwrap(),
73            updated_at: DateTime::<Utc>::from_timestamp(0, 0).unwrap(),
74        };
75        let v = serde_json::to_value(&note).unwrap();
76        let obj = v.as_object().unwrap();
77        for key in [
78            "id",
79            "workspace_id",
80            "created_by",
81            "created_at",
82            "updated_at",
83        ] {
84            assert!(obj.contains_key(key), "missing key `{key}`");
85        }
86        let back: NoteRecord = serde_json::from_value(v).unwrap();
87        assert_eq!(back.title, "t");
88        assert_eq!(back.tags, vec!["a".to_string()]);
89    }
90
91    /// The client builds these request bodies; the server deserializes them.
92    #[test]
93    fn create_note_request_round_trips() {
94        let req = CreateNoteRequest {
95            title: "x".into(),
96            content: "y".into(),
97            tags: vec!["z".into()],
98        };
99        let back: CreateNoteRequest =
100            serde_json::from_value(serde_json::to_value(&req).unwrap()).unwrap();
101        assert_eq!(back.title, "x");
102        assert_eq!(back.tags, vec!["z".to_string()]);
103    }
104
105    #[test]
106    fn update_note_request_omits_none_friendly() {
107        // A bare update deserializes with all-None (default) — the partial-update
108        // contract the client relies on.
109        let req: UpdateNoteRequest = serde_json::from_value(serde_json::json!({})).unwrap();
110        assert!(req.title.is_none() && req.content.is_none() && req.tags.is_none());
111    }
112}