From c2fde492ebb949fc002b225400218e0f7abb2e37 Mon Sep 17 00:00:00 2001 From: Bram Dingelstad Date: Mon, 3 Jul 2023 15:07:55 +0200 Subject: [PATCH] feat: updated everything for changes in notion.cafe --- Cargo.toml | 6 + src/lib.rs | 1404 +++++++++++++++++++++++++--------------------------- 2 files changed, 692 insertions(+), 718 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9788285..07496ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +request = [] +convert_from_notion = [] [dependencies] async-trait = "0.1.68" @@ -15,3 +18,6 @@ regex = "1.7.1" reqwest = { version = "0.11.14", features = ["json"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" + +[dev-dependencies] +tokio = { version = "1.28.1", features = ["macros"] } diff --git a/src/lib.rs b/src/lib.rs index 6f9597b..58948e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,23 +1,32 @@ -use std::sync::Arc; use std::collections::HashMap; +use std::sync::Arc; -use serde_json::json; -use regex::Regex; -use serde_json::Value; use chrono::{DateTime, Utc}; use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; +use regex::Regex; +#[cfg(feature = "request")] use reqwest::header::{HeaderMap, HeaderValue}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use serde_json::Value; 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"); + 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(&mut reqwest::RequestBuilder) -> BoxFuture<'_, std::result::Result> + 'static + Send + Sync; +pub type Callback = dyn Fn( + &mut reqwest::RequestBuilder, + ) -> BoxFuture<'_, std::result::Result> + + 'static + + Send + + Sync; #[derive(Debug)] pub enum Error { @@ -25,7 +34,6 @@ pub enum Error { Deserialization(serde_json::Error, Option), Header(reqwest::header::InvalidHeaderValue), ChronoParse(chrono::ParseError), - NoSuchProperty(String) } impl std::fmt::Display for Error { @@ -58,45 +66,40 @@ impl From for Error { } } -// TODO: Convert to macro? -// TODO: Investigate if I need to add a case for Some(Value::Null) instead of None -fn parse Deserialize<'de>>(key: &str, data: &Value) -> Result { - Ok( - serde_json::from_value::( - data.get(key).ok_or_else(|| Error::NoSuchProperty(key.to_string()))?.clone() - ) - .map_err(|error| Error::Deserialization(error, Some(data.clone())))? - ) -} - -async fn try_to_parse_response serde::Deserialize<'de>>(response: reqwest::Response) -> Result { +async fn try_to_parse_response serde::Deserialize<'de>>( + response: reqwest::Response, +) -> Result { let text = response.text().await?; match serde_json::from_str::(&text) { Ok(value) => Ok(value), - Err(error) => { - match serde_json::from_str::(&text) { - Ok(body) => { - println!("Error: {error:#?}\n\nBody: {body:#?}"); + Err(error) => match serde_json::from_str::(&text) { + Ok(body) => { + println!("Error: {error:#?}\n\nBody: {body:#?}"); - Err(Error::Deserialization(error, Some(body))) - }, - _ => { - println!("Error: {error:#?}\n\nBody: {text}"); - - Err(Error::Deserialization(error, None)) - } + Err(Error::Deserialization(error, None)) } - } + _ => { + println!("Error: {error:#?}\n\nBody: {text}"); + + Err(Error::Deserialization(error, None)) + } + }, } } -const NOTION_VERSION: &str = "2022-06-28"; - +#[cfg(feature = "request")] fn get_http_client(notion_api_key: &str) -> reqwest::Client { let mut headers = HeaderMap::new(); - headers.insert("Authorization", HeaderValue::from_str(&format!("Bearer {notion_api_key}")).expect("bearer token to be parsed into a header")); - headers.insert("Notion-Version", HeaderValue::from_str(NOTION_VERSION).expect("notion version to be parsed into a header")); + headers.insert( + "Authorization", + HeaderValue::from_str(&format!("Bearer {notion_api_key}")) + .expect("bearer token to be parsed into a header"), + ); + headers.insert( + "Notion-Version", + HeaderValue::from_str(NOTION_VERSION).expect("notion version to be parsed into a header"), + ); headers.insert("Content-Type", HeaderValue::from_static("application/json")); reqwest::ClientBuilder::new() @@ -105,10 +108,6 @@ fn get_http_client(notion_api_key: &str) -> reqwest::Client { .expect("to build a valid client out of notion_api_key") } - - - -#[allow(unused)] #[derive(Serialize)] pub struct SearchOptions<'a> { #[serde(skip_serializing_if = "Option::is_none")] @@ -120,13 +119,13 @@ pub struct SearchOptions<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub start_cursor: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - pub page_size: Option + pub page_size: Option, } #[derive(Default)] pub struct ClientBuilder { api_key: Option, - custom_request: Option> + custom_request: Option>, } impl ClientBuilder { @@ -136,61 +135,62 @@ impl ClientBuilder { self } - pub fn custom_request(mut self, callback: F) -> Self + pub fn custom_request(mut self, callback: F) -> Self where - for<'c> F: Fn(&'c mut reqwest::RequestBuilder) -> BoxFuture<'c, std::result::Result> + for<'c> F: Fn( + &'c mut reqwest::RequestBuilder, + ) -> BoxFuture<'c, std::result::Result> + 'static + Send - + Sync { + + 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 notion_api_key = self.api_key.expect("api_key to be set"); - let request_handler = self.custom_request - .unwrap_or( - Arc::new( - |request_builder: &mut reqwest::RequestBuilder| Box::pin(async move { - let request = request_builder.try_clone() - .expect("non-stream body request clone to succeed"); + let request_handler = self.custom_request.unwrap_or(Arc::new( + |request_builder: &mut reqwest::RequestBuilder| { + Box::pin(async move { + let request = request_builder + .try_clone() + .expect("non-stream body request clone to succeed"); - request.send().await - }) - ) - ); + request.send().await + }) + }, + )); 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 { + pages: Pages { http_client: http_client.clone(), - request_handler: request_handler.clone() + request_handler: request_handler.clone(), }, - databases: Databases { + blocks: Blocks { http_client: http_client.clone(), - request_handler: request_handler.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() - } + request_handler: request_handler.clone(), + }, } } - } -#[allow(unused)] pub struct Client { http_client: Arc, request_handler: Arc, @@ -198,7 +198,7 @@ pub struct Client { pub pages: Pages, pub blocks: Blocks, pub databases: Databases, - pub users: Users + pub users: Users, } impl<'a> Client { @@ -206,8 +206,12 @@ impl<'a> Client { ClientBuilder::default() } - pub async fn search<'b, T: std::fmt::Debug + for<'de> serde::Deserialize<'de>>(self, options: SearchOptions<'b>) -> Result> { - let mut request = self.http_client + pub async fn search<'b, T: std::fmt::Debug + for<'de> serde::Deserialize<'de>>( + self, + options: SearchOptions<'b>, + ) -> Result> { + let mut request = self + .http_client .post("https://api.notion.com/v1/search") .json(&options); @@ -223,23 +227,24 @@ impl<'a> Client { } } - pub struct PageOptions<'a> { - pub page_id: &'a str + pub page_id: &'a str, } #[derive(Clone)] pub struct Pages { http_client: Arc, - request_handler: Arc + request_handler: Arc, } impl Pages { pub async fn retrieve<'a>(self, options: PageOptions<'a>) -> Result { - let url = format!("https://api.notion.com/v1/pages/{page_id}", page_id = options.page_id); + let url = format!( + "https://api.notion.com/v1/pages/{page_id}", + page_id = options.page_id + ); - let mut request = self.http_client - .get(url); + let mut request = self.http_client.get(url); let response = (self.request_handler)(&mut request).await?; @@ -256,40 +261,43 @@ impl Pages { #[derive(Clone)] pub struct Blocks { http_client: Arc, - request_handler: Arc + request_handler: Arc, } impl Blocks { pub fn children(&self) -> BlockChildren { BlockChildren { http_client: self.http_client.clone(), - request_handler: self.request_handler.clone() + request_handler: self.request_handler.clone(), } } } pub struct BlockChildren { http_client: Arc, - request_handler: Arc + request_handler: Arc, } pub struct BlockChildrenListOptions<'a> { - pub block_id: &'a str + pub block_id: &'a str, } impl BlockChildren { - pub async fn list<'a>(self, options: BlockChildrenListOptions<'a>) -> Result> { - let url = format!("https://api.notion.com/v1/blocks/{block_id}/children", block_id = options.block_id); + pub async fn list<'a>( + self, + options: BlockChildrenListOptions<'a>, + ) -> Result> { + let url = format!( + "https://api.notion.com/v1/blocks/{block_id}/children", + block_id = options.block_id + ); - let mut request = self.http_client - .get(&url); + let mut request = self.http_client.get(&url); let response = (self.request_handler)(&mut request).await?; match response.error_for_status_ref() { - Ok(_) => { - Ok(response.json().await?) - }, + Ok(_) => Ok(response.json().await?), Err(error) => { let body = response.json::().await?; Err(Error::Http(error, Some(body))) @@ -301,15 +309,20 @@ impl BlockChildren { #[derive(Clone)] pub struct Databases { http_client: Arc, - request_handler: 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); + 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 mut request = self.http_client.post(url); if let Some(filter) = options.filter { request = request.json(&json!({ "filter": filter })); @@ -327,31 +340,71 @@ impl Databases { } } +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::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:#?}"); + } + + #[tokio::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 filter: Option, } #[derive(Clone)] pub struct Users { http_client: Arc, - request_handler: Arc + request_handler: Arc, } impl Users { pub async fn get(&self) -> Result> { let url = "https://api.notion.com/v1/users".to_owned(); - let mut request = self.http_client - .get(&url); + let mut request = self.http_client.get(&url); let response = (self.request_handler)(&mut request).await?; match response.error_for_status_ref() { - Ok(_) => { - Ok(response.json().await?) - }, + Ok(_) => Ok(response.json().await?), Err(error) => { let body = response.json::().await?; Err(Error::Http(error, Some(body))) @@ -362,8 +415,7 @@ impl Users { // Start of normal entities -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(try_from = "Value")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Block { pub id: String, pub parent: Parent, @@ -373,163 +425,171 @@ pub struct Block { pub last_edited_by: PartialUser, pub has_children: bool, pub archived: bool, - pub value: BlockType + #[serde(flatten)] + pub block: BlockType, } -impl TryFrom for Block { - type Error = Error; - - fn try_from(data: Value) -> Result { - Ok( - Block { - id: parse("id", &data)?, - parent: parse("parent", &data)?, - created_time: parse("created_time", &data)?, - last_edited_time: parse("last_edited_time", &data)?, - created_by: parse("created_by", &data)?, - last_edited_by: parse("last_edited_by", &data)?, - has_children: parse("has_children", &data)?, - archived: parse("archived", &data)?, - value: match parse::("type", &data)?.as_str() { - "heading_1" => BlockType::Heading1(parse("heading_1", &data)?), - "heading_2" => BlockType::Heading2(parse("heading_2", &data)?), - "heading_3" => BlockType::Heading3(parse("heading_3", &data)?), - "paragraph" => BlockType::Paragraph(parse("paragraph", &data)?), - "child_database" => BlockType::ChildDatabase(parse("child_database", &data)?), - "child_page" => BlockType::ChildPage(parse("child_page", &data)?), - "code" => BlockType::Code(parse("code", &data)?), - "bulleted_list_item" => BlockType::BulletedListItem(parse("bulleted_list_item", &data)?), - "numbered_list_item" => BlockType::NumberedListItem(parse("numbered_list_item", &data)?), - "quote" => BlockType::Quote(parse("quote", &data)?), - "callout" => BlockType::Callout(parse("callout", &data)?), - "to_do" => BlockType::ToDo(parse("to_do", &data)?), - "image" => BlockType::Image(serde_json::from_value(data)?), - "column_list" => BlockType::ColumnList(parse("column_list", &data)?), - "column" => BlockType::Column(parse("column", &data)?), - - string => BlockType::Unsupported(string.to_string(), data) - } - } - ) - } -} - - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[allow(unused)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] pub enum BlockType { - Paragraph(Paragraph), - BulletedListItem(ListItem), - NumberedListItem(ListItem), - ToDo(ToDoItem), - Quote(Quote), - Callout(Callout), - ChildPage(ChildPage), - ChildDatabase(ChildDatabase), - Heading1(Heading), - Heading2(Heading), - Heading3(Heading), - Code(Code), - Image(Image), - Video(Video), - File(FileBlock), - PDF(PDF), - ColumnList(ColumnList), - Column(Column), - Unsupported(String, Value), - - // TODO: Implement - Toggle, - SyncedBlock, - Template, - Table, - Bookmark, + Paragraph { + paragraph: Paragraph, + }, + Bookmark { + bookmark: Bookmark, + }, + Breadcrumb, + BulletedListItem { + bulleted_list_item: ListItem, + }, + Callout { + callout: Callout, + }, + ChildDatabase, + ChildPage, + Code { + code: Code, + }, + Column, + ColumnList, Divider, - TableOfContents + 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)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Embed { + url: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Bookmark { + caption: Vec, + url: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Heading { pub color: Color, pub rich_text: Vec, - pub is_toggleable: bool + pub is_toggleable: bool, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct PDF { - pub pdf: File + pub pdf: File, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct FileBlock { pub file: File, - pub caption: Vec + pub caption: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Image { - pub image: File -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Video { - pub video: File -} - -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ColumnList { - pub children: Option> + pub children: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Column { - pub children: Option> + pub children: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Callout { pub icon: Option, pub color: Color, pub rich_text: Vec, - pub children: Option> + pub children: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Quote { pub color: Color, pub rich_text: Vec, - pub children: Option> + pub children: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ToDoItem { pub color: Color, pub rich_text: Vec, pub checked: Option, - pub children: Option> + pub children: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ListItem { pub color: Color, pub rich_text: Vec, - pub children: Option> + pub children: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Paragraph { pub color: Color, - pub rich_text: Vec + pub rich_text: Vec, + pub children: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] pub struct Code { - pub language: CodeLanguage, - pub caption: Vec, - pub rich_text: Vec + caption: Vec, + rich_text: Vec, + language: CodeLanguage, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] #[serde(rename_all = "snake_case")] pub enum CodeLanguage { #[serde(rename = "abap")] @@ -632,43 +692,43 @@ pub enum CodeLanguage { #[serde(rename = "yaml")] YAML, #[serde(rename = "java/c/c++/c#")] - JavaCCppCSharp + JavaCCppCSharp, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ChildPage { - pub title: String + pub title: String, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ChildDatabase { - pub title: String + pub title: String, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct User { pub id: String, pub name: Option, pub person: Option, - pub avatar_url: Option + pub avatar_url: Option, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Workspace { - pub workspace: bool + pub workspace: bool, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Person { - pub email: String + pub email: String, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Database { pub id: String, pub title: Vec, pub description: Vec, - pub properties: DatabaseProperties, + pub properties: HashMap, pub url: String, pub parent: Parent, @@ -678,20 +738,190 @@ pub struct Database { pub icon: Option, pub cover: Option, pub archived: bool, - pub is_inline: 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, + }, + + // TODO: Implement Unsupported(Value) + #[serde(other)] + Unsupported, +} + +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, .. } + | 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, .. } + | Title { name, .. } + | Url { name, .. } => Some(name.to_owned()), + + Unsupported => None, + } + } +} + +#[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)] -#[allow(unused)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct QueryResponse { pub has_more: bool, pub next_cursor: Option, - pub results: Vec + pub results: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[allow(unused)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Page { pub id: String, pub created_by: PartialUser, @@ -704,214 +934,166 @@ pub struct Page { pub cover: Option, pub icon: Option, - pub properties: Properties, + pub properties: HashMap, - pub archived: bool + pub archived: bool, } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct PartialUser { - pub id: String -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(try_from = "Value")] -pub struct Properties { - pub map: HashMap, - pub id_map: HashMap -} - -impl Properties { - pub fn get(&self, key: &str) -> Option<&Property> { - self.map.get(key) - .or(self.id_map.get(key)) - } - - pub fn keys(&self) -> Vec { - self.map.keys() - .map(|key| key.to_string()) - .collect() +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 + }) } } -impl TryFrom for Properties { - type Error = Error; +#[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, + formula: Formula, + }, + LastEditedBy { + id: String, + }, // TODO: Implement LastEditedBy + LastEditedTime { + id: String, + last_edited_time: DateValue, + }, + Select { + id: String, + select: Option, + }, + MultiSelect { + id: String, + options: Option>, + }, + 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, + }, - fn try_from(data: Value) -> Result { - let mut map = HashMap::new(); - let mut id_map = HashMap::new(); - - for key in data.as_object().unwrap().keys() { - let property: Property = parse(key, &data)?; + // TODO: Implement Unsupported(Value) + #[serde(other)] + Unsupported, +} - map.insert(key.to_owned(), property.clone()); - id_map.insert(property.id.clone(), property); +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, .. } => Some(id.to_owned()), + + Unsupported => None, } - - Ok(Properties { map, id_map }) } } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(try_from = "Value")] -pub struct DatabaseProperties { - pub map: HashMap, - pub id_map: HashMap -} - -impl DatabaseProperties { - pub fn get(&self, key: &str) -> Option<&DatabaseProperty> { - self.map.get(key) - .or(self.id_map.get(key)) - } - - pub fn keys(&self) -> Vec { - self.map.keys() - .map(|key| key.to_string()) - .collect() - } -} - -impl TryFrom for DatabaseProperties { - type Error = Error; - - fn try_from(data: Value) -> Result { - let mut map = HashMap::new(); - let mut id_map = HashMap::new(); - - for key in data.as_object().unwrap().keys() { - let property: DatabaseProperty = parse(key, &data)?; - - map.insert(key.to_owned(), property.clone()); - id_map.insert(property.id.clone(), property); - } - - Ok(DatabaseProperties { map, id_map }) - } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[allow(unused)] -pub struct PartialProperty { - pub id: String -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[allow(unused)] -pub enum PropertyType { - RichText(Vec), - Number, - Select(Select), - MultiSelect(MultiSelect), - Date(Option), - Formula(Formula), - Relation, - Rollup, - Title(Vec), - People, - Files, - Checkbox(bool), - Url, - Email, - PhoneNumber, - CreatedTime, - CreatedBy, - LastEditedTime, - LastEditedBy, - Unsupported(String, Value) -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[allow(unused)] -pub enum DatabasePropertyType { - RichText, - Number, - Select(Vec), - MultiSelect(Vec), - Date, - Formula(DatabaseFormula), - Relation, - Rollup, - Title, - People, - Files, - Checkbox, - Url, - Email, - PhoneNumber, - CreatedTime, - CreatedBy, - LastEditedTime, - LastEditedBy, - Unsupported(String, Value) -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct DatabaseFormula { - pub expression: String -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(try_from = "Value")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] pub enum Formula { - String(Option), - Number(Option), - Boolean(Option), - Date(Option), - Unsupported(String, Value) + Boolean { boolean: Option }, + Date { date: Option }, + Number { number: Option }, + String { string: Option }, } -impl TryFrom for Formula { - type Error = Error; - - fn try_from(data: Value) -> Result { - Ok( - match parse::("type", &data)?.as_str() { - "string" => Formula::String(parse("string", &data)?), - "number" => Formula::Number(parse("number", &data)?), - "boolean" => Formula::Boolean(parse("boolean", &data)?), - "date" => Formula::Date(parse("date", &data)?), - key => Formula::Unsupported(key.to_string(), data) - } - ) - } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct PartialUser { + pub id: String, } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(try_from = "Value")] -pub struct Select(pub Option); - -impl TryFrom for Select { - type Error = Error; - - fn try_from(data: Value) -> Result