notion/models/
mod.rs

1pub mod block;
2pub mod error;
3pub mod paging;
4pub mod properties;
5pub mod search;
6#[cfg(test)]
7mod tests;
8pub mod text;
9pub mod users;
10
11use crate::models::properties::{PropertyConfiguration, PropertyValue};
12use crate::models::text::RichText;
13use crate::Error;
14use block::ExternalFileObject;
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::collections::HashMap;
18
19use crate::ids::{AsIdentifier, DatabaseId, PageId};
20use crate::models::block::{Block, CreateBlock, FileObject};
21use crate::models::error::ErrorResponse;
22use crate::models::paging::PagingCursor;
23use crate::models::users::User;
24pub use chrono::{DateTime, Utc};
25pub use serde_json::value::Number;
26
27#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)]
28#[serde(rename_all = "snake_case")]
29enum ObjectType {
30    Database,
31    List,
32}
33
34/// Represents a Notion Database
35/// See <https://developers.notion.com/reference/database>
36#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
37pub struct Database {
38    /// Unique identifier for the database.
39    pub id: DatabaseId,
40    /// Date and time when this database was created.
41    pub created_time: DateTime<Utc>,
42    /// Date and time when this database was updated.
43    pub last_edited_time: DateTime<Utc>,
44    /// Name of the database as it appears in Notion.
45    pub title: Vec<RichText>,
46    /// Schema of properties for the database as they appear in Notion.
47    //
48    // key string
49    // The name of the property as it appears in Notion.
50    //
51    // value object
52    // A Property object.
53    pub icon: Option<IconObject>,
54    pub properties: HashMap<String, PropertyConfiguration>,
55}
56
57#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
58#[serde(tag = "type")]
59#[serde(rename_all = "snake_case")]
60pub enum IconObject {
61    File {
62        #[serde(flatten)]
63        file: FileObject,
64    },
65    External {
66        external: ExternalFileObject,
67    },
68    Emoji {
69        emoji: String,
70    },
71}
72
73impl AsIdentifier<DatabaseId> for Database {
74    fn as_id(&self) -> &DatabaseId {
75        &self.id
76    }
77}
78
79impl Database {
80    pub fn title_plain_text(&self) -> String {
81        self.title
82            .iter()
83            .flat_map(|rich_text| rich_text.plain_text().chars())
84            .collect()
85    }
86}
87
88/// <https://developers.notion.com/reference/pagination#responses>
89#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
90pub struct ListResponse<T> {
91    pub results: Vec<T>,
92    pub next_cursor: Option<PagingCursor>,
93    pub has_more: bool,
94}
95
96impl<T> ListResponse<T> {
97    pub fn results(&self) -> &[T] {
98        &self.results
99    }
100}
101
102impl ListResponse<Object> {
103    pub fn only_databases(self) -> ListResponse<Database> {
104        let databases = self
105            .results
106            .into_iter()
107            .filter_map(|object| match object {
108                Object::Database { database } => Some(database),
109                _ => None,
110            })
111            .collect();
112
113        ListResponse {
114            results: databases,
115            has_more: self.has_more,
116            next_cursor: self.next_cursor,
117        }
118    }
119
120    pub(crate) fn expect_databases(self) -> Result<ListResponse<Database>, crate::Error> {
121        let databases: Result<Vec<_>, _> = self
122            .results
123            .into_iter()
124            .map(|object| match object {
125                Object::Database { database } => Ok(database),
126                response => Err(Error::UnexpectedResponse { response }),
127            })
128            .collect();
129
130        Ok(ListResponse {
131            results: databases?,
132            has_more: self.has_more,
133            next_cursor: self.next_cursor,
134        })
135    }
136
137    pub(crate) fn expect_pages(self) -> Result<ListResponse<Page>, crate::Error> {
138        let items: Result<Vec<_>, _> = self
139            .results
140            .into_iter()
141            .map(|object| match object {
142                Object::Page { page } => Ok(page),
143                response => Err(Error::UnexpectedResponse { response }),
144            })
145            .collect();
146
147        Ok(ListResponse {
148            results: items?,
149            has_more: self.has_more,
150            next_cursor: self.next_cursor,
151        })
152    }
153
154    pub(crate) fn expect_blocks(self) -> Result<ListResponse<Block>, crate::Error> {
155        let items: Result<Vec<_>, _> = self
156            .results
157            .into_iter()
158            .map(|object| match object {
159                Object::Block { block } => Ok(block),
160                response => Err(Error::UnexpectedResponse { response }),
161            })
162            .collect();
163
164        Ok(ListResponse {
165            results: items?,
166            has_more: self.has_more,
167            next_cursor: self.next_cursor,
168        })
169    }
170}
171
172#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
173#[serde(tag = "type")]
174#[serde(rename_all = "snake_case")]
175pub enum Parent {
176    #[serde(rename = "database_id")]
177    Database {
178        database_id: DatabaseId,
179    },
180    #[serde(rename = "page_id")]
181    Page {
182        page_id: PageId,
183    },
184    Workspace,
185}
186
187#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
188pub struct Properties {
189    #[serde(flatten)]
190    pub properties: HashMap<String, PropertyValue>,
191}
192
193impl Properties {
194    pub fn title(&self) -> Option<String> {
195        self.properties.values().find_map(|p| match p {
196            PropertyValue::Title { title, .. } => {
197                Some(title.iter().map(|t| t.plain_text()).collect())
198            }
199            _ => None,
200        })
201    }
202}
203
204#[derive(Serialize, Debug, Eq, PartialEq)]
205pub struct PageCreateRequest {
206    pub parent: Parent,
207    pub properties: Properties,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub children: Option<Vec<CreateBlock>>,
210}
211
212#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
213pub struct Page {
214    pub id: PageId,
215    /// Date and time when this page was created.
216    pub created_time: DateTime<Utc>,
217    /// Date and time when this page was updated.
218    pub last_edited_time: DateTime<Utc>,
219    /// The archived status of the page.
220    pub archived: bool,
221    pub properties: Properties,
222    pub icon: Option<IconObject>,
223    pub parent: Parent,
224}
225
226impl Page {
227    pub fn title(&self) -> Option<String> {
228        self.properties.title()
229    }
230}
231
232impl AsIdentifier<PageId> for Page {
233    fn as_id(&self) -> &PageId {
234        &self.id
235    }
236}
237
238#[derive(Eq, Serialize, Deserialize, Clone, Debug, PartialEq)]
239#[serde(tag = "object")]
240#[serde(rename_all = "snake_case")]
241pub enum Object {
242    Block {
243        #[serde(flatten)]
244        block: Block,
245    },
246    Database {
247        #[serde(flatten)]
248        database: Database,
249    },
250    Page {
251        #[serde(flatten)]
252        page: Page,
253    },
254    List {
255        #[serde(flatten)]
256        list: ListResponse<Object>,
257    },
258    User {
259        #[serde(flatten)]
260        user: User,
261    },
262    Error {
263        #[serde(flatten)]
264        error: ErrorResponse,
265    },
266}
267
268impl Object {
269    pub fn is_database(&self) -> bool {
270        matches!(self, Object::Database { .. })
271    }
272}