use std::collections::HashMap; use std::sync::Arc; use chrono::{DateTime, NaiveTime, Utc}; use lazy_static::lazy_static; use regex::Regex; use serde::de::Error as SerdeError; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::json; use serde_json::Value; use surf::http::StatusCode; use futures_core::future::BoxFuture; lazy_static! { static ref ISO_8601_DATE: Regex = Regex::new(r"^\d{4}-\d{2}-\d{2}$").expect("ISO 8601 date regex to be parseable"); } #[cfg(feature = "request")] const NOTION_VERSION: &str = "2022-06-28"; pub type Result = std::result::Result; pub type Callback = dyn Fn(surf::RequestBuilder) -> BoxFuture<'static, std::result::Result> + 'static + Send + Sync; #[derive(Debug)] pub enum Error { Http(StatusCode, surf::Response), Surf(surf::Error), Deserialization(serde_json::Error, Option), ChronoParse(chrono::ParseError), UnexpectedType, } impl std::fmt::Display for Error { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { Ok(write!(formatter, "NotionError::{:?}", self)?) } } impl From for Error { fn from(error: surf::Error) -> Self { Error::Surf(error) } } impl From for Error { fn from(error: serde_json::Error) -> Self { Error::Deserialization(error, None) } } impl From for Error { fn from(error: chrono::ParseError) -> Self { Error::ChronoParse(error) } } // async fn try_to_parse_response serde::Deserialize<'de>>( // mut response: surf::Response, // ) -> Result { // let text = response.body_string().await?; // match serde_json::from_str::(&text) { // Ok(value) => Ok(value), // Err(error) => match serde_json::from_str::(&text) { // Ok(body) => Err(Error::Deserialization(error, Some(body))), // _ => Err(Error::Deserialization(error, Some(Value::String(text)))), // }, // } // } #[cfg(feature = "request")] fn get_http_client(notion_api_key: &str) -> surf::Client { log::trace!("Readying HTTP Client"); surf::Config::new() .add_header("Authorization", format!("Bearer {notion_api_key}")) .expect("to add Authorization header") .add_header("Notion-Version", NOTION_VERSION) .expect("to add Notion-Version header") .add_header("Content-Type", "application/json") .expect("to add Content-Type header") .try_into() .expect("to build a valid client out of notion_api_key") } #[derive(Serialize)] pub struct SearchOptions<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub query: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] pub filter: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sort: Option, #[serde(skip_serializing_if = "Option::is_none")] pub start_cursor: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] pub page_size: Option, } #[derive(Default)] pub struct ClientBuilder { api_key: Option, custom_request: Option>, } impl ClientBuilder { pub fn api_key(mut self, api_key: &str) -> Self { self.api_key = Some(api_key.to_owned()); self } pub fn custom_request(mut self, callback: F) -> Self where for<'c> F: Fn( surf::RequestBuilder, ) -> BoxFuture<'static, std::result::Result> + 'static + Send + Sync, { self.custom_request = Some(Arc::new(callback)); self } #[cfg(feature = "request")] pub fn build(self) -> Client { let notion_api_key = self.api_key.expect("api_key to be set"); let request_handler = self.custom_request .unwrap_or(Arc::new(|request_builder: surf::RequestBuilder| { Box::pin(request_builder) })); let http_client = Arc::from(get_http_client(¬ion_api_key)); Client { http_client: http_client.clone(), request_handler: request_handler.clone(), pages: Pages { http_client: http_client.clone(), request_handler: request_handler.clone(), }, blocks: Blocks { http_client: http_client.clone(), request_handler: request_handler.clone(), }, databases: Databases { http_client: http_client.clone(), request_handler: request_handler.clone(), }, users: Users { http_client: http_client.clone(), request_handler: request_handler.clone(), }, } } } pub struct Client { http_client: Arc, request_handler: Arc, pub pages: Pages, pub blocks: Blocks, pub databases: Databases, pub users: Users, } impl<'a> Client { #[allow(clippy::new_ret_no_self)] pub fn new() -> ClientBuilder { ClientBuilder::default() } pub async fn search<'b, T: std::fmt::Debug + for<'de> serde::Deserialize<'de>>( self, options: SearchOptions<'b>, ) -> Result> { let request = self .http_client .post("https://api.notion.com/v1/search") .body_json(&options) .expect("to parse JSON for doing `search`"); let mut response = (self.request_handler)(request) .await .expect("to request through a request handler"); match response.status() { StatusCode::Ok => Ok(response.body_json().await?), status => Err(Error::Http(status, response)), } } } pub struct PageOptions<'a> { pub page_id: &'a str, } #[derive(Clone)] pub struct Pages { http_client: Arc, request_handler: Arc, } impl Pages { pub async fn retrieve(self, options: PageOptions<'_>) -> Result { let url = format!( "https://api.notion.com/v1/pages/{page_id}", page_id = options.page_id ); let request = self.http_client.get(url); let mut response = (self.request_handler)(request).await?; match response.status() { StatusCode::Ok => Ok(response.body_json().await?), status => Err(Error::Http(status, response)), } } } #[derive(Clone)] pub struct Blocks { http_client: Arc, request_handler: Arc, } impl Blocks { pub fn children(&self) -> BlockChildren { BlockChildren { http_client: self.http_client.clone(), request_handler: self.request_handler.clone(), } } } pub struct BlockChildren { http_client: Arc, request_handler: Arc, } pub struct BlockChildrenListOptions<'a> { pub block_id: &'a str, } impl BlockChildren { pub async fn list(self, options: BlockChildrenListOptions<'_>) -> Result> { let url = format!( "https://api.notion.com/v1/blocks/{block_id}/children", block_id = options.block_id ); let request = self.http_client.get(&url); let mut response = (self.request_handler)(request).await?; match response.status() { StatusCode::Ok => Ok(response.body_json().await?), status => Err(Error::Http(status, response)), } } } #[derive(Clone)] pub struct Databases { http_client: Arc, request_handler: Arc, } impl Databases { pub async fn query<'a>( &self, options: DatabaseQueryOptions<'a>, ) -> Result> { let url = format!( "https://api.notion.com/v1/databases/{database_id}/query", database_id = options.database_id ); let mut request = self.http_client.post(url); let json = options.filter.map(|filter| json!({ "filter": filter })); let json = if let Some(sorts) = options.sorts { if let Some(mut json) = json { json.as_object_mut() .expect("Some object to be editable") .insert("sorts".to_string(), sorts); Some(json) } else { Some(json!({ "sorts": sorts })) } } else { json }; let json = if let Some(cursor) = options.start_cursor { if let Some(mut json) = json { json.as_object_mut() .expect("Some object to be editable") .insert("start_cursor".to_string(), Value::String(cursor)); Some(json) } else { Some(json!({ "start_cursor": cursor })) } } else { json }; if let Some(ref json) = json { request = request .body_json(json) .expect("to parse JSON for start_cursor"); } log::trace!("Querying database with request: {request:#?} and body: {json:#?}"); let mut response = (self.request_handler)(request).await?; match response.status() { StatusCode::Ok => Ok(response.body_json().await?), status => Err(Error::Http(status, response)), } } } #[cfg(test)] mod tests { use super::*; #[async_std::test] async fn check_database_query() { let databases = Client::new() .api_key("secret_FuhJkAoOVZlk8YUT9ZOeYqWBRRZN6OMISJwhb4dTnud") .build() .search::(SearchOptions { filter: Some(json!( { "value": "database", "property": "object" } )), query: None, page_size: None, sort: None, start_cursor: None, }) .await; println!("{databases:#?}"); } #[async_std::test] async fn test_blocks() { let blocks = Client::new() .api_key("secret_FuhJkAoOVZlk8YUT9ZOeYqWBRRZN6OMISJwhb4dTnud") .build() .blocks .children() .list(BlockChildrenListOptions { block_id: "0d253ab0f751443aafb9bcec14012897", }) .await; println!("{blocks:#?}") } } #[derive(Debug, Default)] pub struct DatabaseQueryOptions<'a> { pub database_id: &'a str, // TODO: Implement spec for filter? pub filter: Option, pub sorts: Option, pub start_cursor: Option, } #[derive(Clone)] pub struct Users { http_client: Arc, request_handler: Arc, } impl Users { pub async fn get(&self) -> Result> { let url = "https://api.notion.com/v1/users".to_owned(); let request = self.http_client.get(&url); let mut response = (self.request_handler)(request).await?; match response.status() { StatusCode::Ok => Ok(response.body_json().await?), status => Err(Error::Http(status, response)), } } } // Start of normal entities #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Block { pub id: String, pub parent: Parent, pub created_time: DateValue, pub last_edited_time: DateValue, pub created_by: PartialUser, pub last_edited_by: PartialUser, pub has_children: bool, pub archived: bool, #[serde(flatten)] pub block: BlockType, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum BlockType { Paragraph { paragraph: Paragraph, }, Bookmark { bookmark: Bookmark, }, Breadcrumb, BulletedListItem { bulleted_list_item: ListItem, }, Callout { callout: Callout, }, ChildDatabase { child_database: ChildDatabase, }, ChildPage { child_page: ChildPage, }, Code { code: Code, }, Column, ColumnList, Divider, Embed { embed: Embed, }, Equation { equation: Equation, }, File { file: File, }, Heading1 { heading: Heading, }, Heading2 { heading: Heading, }, Heading3 { heading: Heading, }, Image { image: File, }, LinkPreview { link_preview: LinkPreview, }, LinkToPage, NumberedListItem { numbered_list_item: ListItem, }, Pdf { pdf: File, }, Quote { quote: Quote, }, SyncedBlock, Table, TableOfContents, TableRow, Template, ToDo { to_do: ToDoItem, }, Toggle, Video { video: File, }, // TODO: Implement Unsupported(Value) #[serde(other)] Unsupported, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Embed { pub url: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Bookmark { pub caption: Vec, pub url: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Heading { pub color: Color, pub rich_text: Vec, pub is_toggleable: bool, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct PDF { pub pdf: File, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct FileBlock { pub file: File, pub caption: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ColumnList { pub children: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Column { pub children: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Callout { pub icon: Option, pub color: Color, pub rich_text: Vec, pub children: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Quote { pub color: Color, pub rich_text: Vec, pub children: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ToDoItem { pub color: Color, pub rich_text: Vec, pub checked: Option, pub children: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ListItem { pub color: Color, pub rich_text: Vec, pub children: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Paragraph { pub color: Color, pub rich_text: Vec, pub children: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] pub struct Code { pub caption: Vec, pub rich_text: Vec, pub language: CodeLanguage, } #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] #[serde(rename_all = "snake_case")] pub enum CodeLanguage { #[serde(rename = "abap")] ABAP, Arduino, Bash, Basic, C, Clojure, #[serde(rename = "coffeescript")] CoffeeScript, #[serde(rename = "c++")] Cpp, #[serde(rename = "c#")] CSharp, #[serde(rename = "css")] CSS, Dart, Diff, Docker, Elixer, Elm, Erlang, Flow, Fortan, #[serde(rename = "f#")] FSharp, Gherkin, #[serde(rename = "glsl")] GLSL, Go, #[serde(rename = "graphql")] GraphQL, Groovy, Haskell, #[serde(rename = "html")] HTML, Java, #[serde(rename = "javascript")] JavaScript, #[serde(rename = "json")] JSON, Julia, Kotlin, Latex, Less, Lisp, #[serde(rename = "livescript")] LiveScript, Lua, #[serde(rename = "makefile")] MakeFile, Markdown, Markup, Matlab, Mermaid, Nix, #[serde(rename = "objective-c")] ObjectiveC, #[serde(rename = "ocaml")] OCaml, Pascal, Perl, #[serde(rename = "php")] PHP, #[default] #[serde(rename = "plain text")] PlainText, #[serde(rename = "powershell")] PowerShell, Prolog, Protobuf, Python, R, Reason, Ruby, Rust, Sass, Scala, Scheme, #[serde(rename = "scss")] SCSS, Shell, #[serde(rename = "sql")] SQL, Swift, #[serde(rename = "typescript")] TypeScript, #[serde(rename = "vb.net")] VBNet, Verilog, #[serde(rename = "vhdl")] VHDL, #[serde(rename = "visual basic")] VisualBasic, #[serde(rename = "webassembly")] WebAssembly, #[serde(rename = "xml")] XML, #[serde(rename = "yaml")] YAML, #[serde(rename = "java/c/c++/c#")] JavaCCppCSharp, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ChildPage { pub title: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ChildDatabase { pub title: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct User { pub id: String, pub name: Option, pub person: Option, pub avatar_url: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Workspace { pub workspace: bool, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Person { pub email: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Database { pub id: String, pub title: Vec, pub description: Vec, #[serde(deserialize_with = "deserialize_database_properties")] pub properties: HashMap, pub url: String, pub parent: Parent, pub created_time: DateValue, pub last_edited_time: DateValue, pub last_edited_by: PartialUser, pub icon: Option, pub cover: Option, pub archived: bool, pub is_inline: bool, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub struct DatabaseSelectOptions { pub options: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum DatabaseProperty { Checkbox { id: String, name: String, }, CreatedTime { id: String, name: String, }, Date { id: String, name: String, }, Email { id: String, name: String, }, Files { id: String, name: String, }, Formula { id: String, name: String, formula: DatabaseFormula, }, LastEditedBy { id: String, name: String, }, LastEditedTime { id: String, name: String, }, MultiSelect { id: String, name: String, multi_select: DatabaseSelectOptions, }, Number { id: String, name: String, number: Number, }, People { id: String, name: String, }, PhoneNumber { id: String, name: String, }, Relation { id: String, name: String, // relation: Relation, }, RichText { id: String, name: String, }, Rollup { id: String, name: String, // TODO: Implement Rollup }, Select { id: String, name: String, select: DatabaseSelectOptions, }, Status { id: String, name: String, // TODO: Implement Status }, Title { id: String, name: String, }, Url { id: String, name: String, }, Button { id: String, name: String, }, Unsupported(Value), } impl DatabaseProperty { pub fn id(&self) -> Option { use DatabaseProperty::*; match self { Checkbox { id, .. } | CreatedTime { id, .. } | Date { id, .. } | Email { id, .. } | Files { id, .. } | Formula { id, .. } | LastEditedBy { id, .. } | LastEditedTime { id, .. } | MultiSelect { id, .. } | Number { id, .. } | People { id, .. } | PhoneNumber { id, .. } | Relation { id, .. } | RichText { id, .. } | Rollup { id, .. } | Select { id, .. } | Status { id, .. } | Title { id, .. } | Button { id, .. } | Url { id, .. } => Some(id.to_owned()), Unsupported(..) => None, } } pub fn name(&self) -> Option { use DatabaseProperty::*; match self { Checkbox { name, .. } | CreatedTime { name, .. } | Date { name, .. } | Email { name, .. } | Files { name, .. } | Formula { name, .. } | LastEditedBy { name, .. } | LastEditedTime { name, .. } | MultiSelect { name, .. } | Number { name, .. } | People { name, .. } | PhoneNumber { name, .. } | Relation { name, .. } | RichText { name, .. } | Rollup { name, .. } | Select { name, .. } | Status { name, .. } | Button { name, .. } | Title { name, .. } | Url { name, .. } => Some(name.to_owned()), Unsupported(..) => None, } } } fn deserialize_database_properties<'de, D>( deserializer: D, ) -> std::result::Result, D::Error> where D: Deserializer<'de>, { Ok(Value::deserialize(deserializer)? .as_object_mut() .ok_or(Error::UnexpectedType) .map_err(D::Error::custom)? .into_iter() .map(|(key, value)| { ( key.to_owned(), serde_json::from_value::(value.to_owned()).unwrap_or_else(|error| { log::warn!( "Could not parse value because of error, defaulting to DatabaseProperty::Unsupported:\n= ERROR:\n{error:#?}\n= JSON:\n{:#?}\n---", serde_json::to_string_pretty(&value).expect("to pretty print the database property error") ); DatabaseProperty::Unsupported(value.to_owned()) }), ) }) .collect::>()) } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Number { // TODO: Implement NumberFormat // pub format: NumberFormat } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Relation { // #[serde(alias = "database_id")] // id: String, // synced_property_name: String, // synced_property_id: String, } // TODO: Paginate all possible responses #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct QueryResponse { pub has_more: Option, pub next_cursor: Option, pub results: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Page { pub id: String, pub created_by: PartialUser, pub url: String, pub parent: Parent, pub created_time: DateValue, pub last_edited_time: DateValue, pub cover: Option, pub icon: Option, #[serde(deserialize_with = "deserialize_properties")] pub properties: HashMap, pub archived: bool, } impl Page { pub fn get_property_by_id(&self, id: &str) -> Option<(&String, &Property)> { self.properties.iter().find(|(_, property)| { property.id().is_some() && property.id().expect("id that is_some() to be unwrappable") == id }) } pub fn get_title(&self) -> &Vec { if let Property::Title { title, .. } = self .get_property_by_id("title") .expect("every page to have a title") .1 { title } else { unreachable!("Expected title to be of type title") } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum Property { Checkbox { id: String, checkbox: bool, }, CreatedBy { id: String, }, CreatedTime { id: String, created_time: DateValue, }, Date { id: String, date: Option, }, Email { id: String, email: Option, }, Files { id: String, files: Vec, }, Formula { id: String, name: Option, formula: Formula, }, LastEditedBy { id: String, }, // TODO: Implement LastEditedBy LastEditedTime { id: String, last_edited_time: DateValue, }, Select { id: String, select: Option, }, MultiSelect { id: String, multi_select: Vec, }, Number { id: String, number: Option, }, People { id: String, }, PhoneNumber { id: String, }, Relation { id: String, // relation: Vec, }, Rollup { id: String, }, // TODO: Implement Rollup RichText { id: String, rich_text: Vec, }, Status { id: String, }, // TODO: Implement Status Title { id: String, title: Vec, }, Url { id: String, url: Option, }, Verification { id: String, // TODO: Implement }, UniqueId { id: String, }, Button { id: String, }, Unsupported(Value), } impl Property { pub fn id(&self) -> Option { use Property::*; match self { Title { id, .. } | Checkbox { id, .. } | CreatedBy { id, .. } | CreatedTime { id, .. } | Date { id, .. } | Email { id, .. } | Files { id, .. } | LastEditedBy { id, .. } | MultiSelect { id, .. } | Number { id, .. } | People { id, .. } | LastEditedTime { id, .. } | PhoneNumber { id, .. } | Relation { id, .. } | Rollup { id, .. } | RichText { id, .. } | Select { id, .. } | Status { id, .. } | Url { id, .. } | Formula { id, .. } | Verification { id, .. } | Button { id, .. } | UniqueId { id, .. } => Some(id.to_owned()), Unsupported(_) => None, } } } fn deserialize_properties<'de, D>( deserializer: D, ) -> std::result::Result, D::Error> where D: Deserializer<'de>, { Ok(Value::deserialize(deserializer)? .as_object_mut() .ok_or(Error::UnexpectedType) .map_err(D::Error::custom)? .into_iter() .map(|(key, value)| { if let Value::Object(ref mut object) = value { // Notion sometimes sends an empty object when it means "null", so we gotta do it's homework for value in object.values_mut() { if value == &mut json!({}) { *value = Value::Null } } // Correcting missing values for (key, value) in object.iter_mut() { match key.as_ref() { // Notion forgets to set the formula type, so we're doing it's homework "formula" => { if let Value::Object(object) = value { if object.get("type").is_none() { object.insert("type".to_owned(), json!("string")); } } } // Notion sometimes just sets title to an empty object? Guess we have to set it to something? "title" => { if let Value::Null = value { *value = json!([{ "type": "text", "text": { "content": "Unknown title", "link": null }, "annotations": { "bold": false, "italic": false, "strikethrough": false, "underline": false, "code": false, "color": "default" }, "plain_text": "Unknown title", "href": null }]) } } // Notion sometimes forget to set a value for a checkbox?? "checkbox" => { if let Value::Null = value { *value = json!(false) } } _ => {} }; } } ( key.to_owned(), serde_json::from_value::(value.to_owned()).unwrap_or_else(|error| { log::warn!( "Could not parse value because of error, defaulting to Property::Unsupported:\n= ERROR:\n{error:#?}\n= JSON:\n{}\n---", serde_json::to_string_pretty(&value).expect("to pretty print Property errors") ); Property::Unsupported(value.to_owned()) }), ) }) .collect::>()) } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum Formula { Boolean { boolean: Option }, Date { date: Option }, Number { number: Option }, String { string: Option }, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct PartialUser { pub id: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct PartialProperty { pub id: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct DatabaseFormula { pub expression: String, pub suspected_type: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct SelectOption { pub id: String, pub name: String, pub color: Color, } impl std::fmt::Display for SelectOption { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { Ok(write!(formatter, "MultiSelectOption::{}", self.name)?) } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "lowercase")] pub enum RichText { Text { text: Text, plain_text: String, href: Option, annotations: Annotations, }, Mention { mention: Mention, plain_text: String, href: Option, annotations: Annotations, }, Equation { expression: Option, plain_text: String, href: Option, annotations: Annotations, }, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Text { pub content: String, pub link: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "lowercase")] pub enum Mention { Database { database: PartialDatabase }, Date { date: Date }, LinkPreview { link_preview: LinkPreview }, Page { page: PartialPage }, User { user: PartialUser }, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct LinkPreview { pub url: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct PartialPage { pub id: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct PartialDatabase { pub id: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct PartialBlock { pub id: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Date { pub start: DateValue, pub end: Option, // TODO: Implement for setting pub time_zone: Option, } impl std::fmt::Display for Date { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { let start = &self.start; if let Some(end) = &self.end { Ok(write!(formatter, "{start} - {end}")?) } else { Ok(write!(formatter, "{start}")?) } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(try_from = "String", into = "String")] pub enum DateValue { DateTime(DateTime), Date(chrono::NaiveDate), } impl TryFrom for DateValue { type Error = Error; fn try_from(string: String) -> Result { // NOTE: is either ISO 8601 Date or assumed to be ISO 8601 DateTime let value = if ISO_8601_DATE.is_match(&string) { DateValue::Date( DateTime::parse_from_rfc3339(&format!("{string}T00:00:00Z"))?.date_naive(), ) } else { DateValue::DateTime(DateTime::parse_from_rfc3339(&string)?.with_timezone(&Utc)) }; Ok(value) } } impl From for String { fn from(value: DateValue) -> String { value.to_string() } } impl std::fmt::Display for DateValue { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { let value = match self { DateValue::Date(date) => date.and_time(NaiveTime::MIN).and_utc().to_rfc3339(), DateValue::DateTime(date_time) => date_time.to_rfc3339(), }; Ok(write!(formatter, "{}", value)?) } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Equation { pub plain_text: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Link { pub url: String, } #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] pub struct Annotations { pub bold: bool, pub italic: bool, pub strikethrough: bool, pub underline: bool, pub code: bool, pub color: Color, } #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] #[serde(rename_all = "snake_case")] pub enum Color { #[default] Default, Gray, Brown, Orange, Yellow, Green, Blue, Purple, Pink, Red, GrayBackground, BrownBackground, OrangeBackground, YellowBackground, GreenBackground, BlueBackground, PurpleBackground, PinkBackground, RedBackground, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum Parent { PageId { page_id: String }, DatabaseId { database_id: String }, BlockId { block_id: String }, Workspace, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "lowercase")] pub enum File { File { file: NotionFile }, External { external: ExternalFile }, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[serde(rename_all = "lowercase")] pub enum Icon { Emoji { emoji: String }, File { file: NotionFile }, External { external: ExternalFile }, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct NotionFile { pub expiry_time: DateValue, pub url: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ExternalFile { pub url: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub enum DatabaseFormulaType { Boolean, Date, Number, String, }