use std::sync::Arc; use std::collections::HashMap; 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 reqwest::header::{HeaderMap, HeaderValue}; 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"); } // TODO: Add the ability to hack into the code or add queuing pub type Result = std::result::Result; #[derive(Debug)] pub enum Error { Http(reqwest::Error), Deserialization(serde_json::Error, Option), Header(reqwest::header::InvalidHeaderValue), ChronoParse(chrono::ParseError), NoSuchProperty(String) } impl From for Error { fn from(error: reqwest::Error) -> Self { Error::Http(error) } } impl From for Error { fn from(error: reqwest::header::InvalidHeaderValue) -> Self { Error::Header(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) } } // 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() )? ) } const NOTION_VERSION: &str = "2022-06-28"; 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("Content-Type", HeaderValue::from_static("application/json")); reqwest::ClientBuilder::new() .default_headers(headers) .build() .expect("to build a valid client out of notion_api_key") } #[allow(unused)] pub struct Client { http_client: Arc, pub pages: Pages, pub blocks: Blocks, pub databases: Databases } #[allow(unused)] #[derive(Serialize)] pub struct SearchOptions<'a> { pub query: Option<&'a str>, pub filter: Option, pub sort: Option, pub start_cursor: Option<&'a str>, pub page_size: Option } impl<'a> Client { pub fn new(notion_api_key: &'a str) -> Self { let http_client = Arc::from(get_http_client(notion_api_key)); Client { http_client: http_client.clone(), pages: Pages { http_client: http_client.clone() }, blocks: Blocks { http_client: http_client.clone() }, databases: Databases { http_client: http_client.clone() } } } pub async fn search<'b>(self, options: SearchOptions<'b>) -> Result> { let url = "https://api.notion.com/v1/search"; let request = self.http_client .post(url) .json(&options) .send() .await?; match request.error_for_status_ref() { Ok(_) => Ok(request.json().await?), Err(error) => { println!("Error: {error:#?}"); println!("Body: {:#?}", request.json::().await?); Err(Error::Http(error)) } } } } pub struct PageOptions<'a> { pub page_id: &'a str } pub struct Pages { http_client: 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 request = self.http_client .get(url) .send() .await?; match request.error_for_status_ref() { Ok(_) => Ok(request.json().await?), Err(error) => { println!("Error: {error:#?}"); println!("Body: {:#?}", request.json::().await?); Err(Error::Http(error)) } } } } pub struct Blocks { http_client: Arc, } impl Blocks { pub fn children(&self) -> BlockChildren { BlockChildren { http_client: self.http_client.clone() } } } pub struct BlockChildren { http_client: Arc, } pub struct BlockChildrenListOptions<'a> { 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); let request = self.http_client .get(&url) .send() .await?; match request.error_for_status_ref() { Ok(_) => { Ok(request.json().await?) }, Err(error) => { println!("Error: {error:#?}"); println!("Body: {:#?}", request.json::().await?); Err(Error::Http(error)) } } } } pub struct Databases { http_client: 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); if let Some(filter) = options.filter { request = request.json(&json!({ "filter": filter })); } let request = request .send() .await?; match request.error_for_status_ref() { Ok(_) => Ok(request.json().await?), Err(error) => { println!("Error: {error:#?}"); println!("Body: {:#?}", request.json::().await?); Err(Error::Http(error)) } } } } pub struct DatabaseQueryOptions<'a> { pub database_id: &'a str, // TODO: Implement spec for filter? pub filter: Option } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(try_from = "Value")] 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, pub value: 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()) } } ) } } #[derive(Debug, Serialize, Deserialize, Clone)] #[allow(unused)] 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), // TODO: Implement Toggle, SyncedBlock, Template, Table, Bookmark, Divider, TableOfContents } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Heading { pub color: Color, pub rich_text: Vec, pub is_toggleable: bool } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PDF { pub pdf: File } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct FileBlock { pub file: File, 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)] pub struct ColumnList { pub children: Option> } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Column { pub children: Option> } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Callout { pub icon: Option, pub color: Color, pub rich_text: Vec, pub children: Option> } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Quote { pub color: Color, pub rich_text: Vec, pub children: Option> } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ToDoItem { pub color: Color, pub rich_text: Vec, pub checked: Option, pub children: Option> } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ListItem { pub color: Color, pub rich_text: Vec, pub children: Option> } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Paragraph { pub color: Color, pub rich_text: Vec } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Code { pub language: CodeLanguage, pub caption: Vec, pub rich_text: Vec } #[derive(Debug, Serialize, Deserialize, Clone, Default)] #[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)] pub struct ChildPage { pub title: String } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChildDatabase { pub title: String } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct User { pub id: String, pub name: String, pub person: Option, pub avatar_url: Option } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Workspace { pub workspace: bool } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Person { pub email: String } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Database { pub id: String, pub title: Vec, pub description: Vec, pub properties: Properties, 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 } // TODO: Paginate all possible responses #[derive(Debug, Serialize, Deserialize, Clone)] #[allow(unused)] pub struct QueryResponse { pub has_more: bool, pub next_cursor: Option, pub results: Vec } #[derive(Debug, Serialize, Deserialize, Clone)] #[allow(unused)] 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, pub properties: Properties, 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 } impl Properties { pub fn get(&self, key: &str) -> Option { match self.map.get(key) { Some(property) => Some(property.to_owned()), None => None } } pub fn keys(&self) -> Vec { self.map.keys() .map(|key| key.to_string()) .collect() } } impl TryFrom for Properties { type Error = Error; fn try_from(data: Value) -> Result { let mut map = HashMap::new(); for key in data.as_object().unwrap().keys() { map.insert(key.to_owned(), parse(key, &data)?); } Ok(Properties { map }) } } #[derive(Debug, Serialize, Deserialize, Clone)] pub enum PropertyResponse { List(Vec), PropertyItem(Box) } #[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(Date), Formula(Formula), Relation, Rollup, Title(Vec), People, Files, Checkbox(bool), Url, Email, PhoneNumber, CreatedTime, CreatedBy, LastEditedTime, LastEditedBy, Unknown } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(try_from = "Value")] pub enum Formula { String(Option), Number(Option), Boolean(Option), Date(Option), Unknown } 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)?), _ => Formula::Unknown } ) } } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(try_from = "Value")] pub struct Select(pub SelectOption); impl TryFrom for Select { type Error = Error; fn try_from(data: Value) -> Result