Compare commits

...

7 commits

2 changed files with 166 additions and 144 deletions

View file

@ -5,10 +5,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
request = [] request = ["surf/h1-client-rustls"]
convert_from_notion = [] convert_from_notion = []
[dependencies] [dependencies]
async-std = "1.12.0"
async-trait = "0.1.68" async-trait = "0.1.68"
base64 = "0.21.0" base64 = "0.21.0"
chrono = "0.4.31" chrono = "0.4.31"
@ -16,9 +17,6 @@ futures-core = "0.3.28"
lazy_static = "1.4.0" lazy_static = "1.4.0"
log = "0.4.20" log = "0.4.20"
regex = "1.7.1" regex = "1.7.1"
reqwest = { version = "0.11.14", features = ["json"] }
serde = { version = "^1.0", features = ["derive"], default-features = false } serde = { version = "^1.0", features = ["derive"], default-features = false }
serde_json = { version = "^1.0", features = ["raw_value"], default-features = false } serde_json = { version = "^1.0", features = ["raw_value"], default-features = false }
surf = { version = "2.3.2", default-features = false }
[dev-dependencies]
tokio = { version = "1.28.1", features = ["macros"] }

View file

@ -4,12 +4,11 @@ use std::sync::Arc;
use chrono::{DateTime, NaiveTime, Utc}; use chrono::{DateTime, NaiveTime, Utc};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
#[cfg(feature = "request")]
use reqwest::header::{HeaderMap, HeaderValue};
use serde::de::Error as SerdeError; use serde::de::Error as SerdeError;
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use serde_json::json; use serde_json::json;
use serde_json::Value; use serde_json::Value;
use surf::http::StatusCode;
use futures_core::future::BoxFuture; use futures_core::future::BoxFuture;
@ -22,18 +21,16 @@ lazy_static! {
const NOTION_VERSION: &str = "2022-06-28"; const NOTION_VERSION: &str = "2022-06-28";
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
pub type Callback = dyn Fn( pub type Callback = dyn Fn(surf::RequestBuilder) -> BoxFuture<'static, std::result::Result<surf::Response, surf::Error>>
&mut reqwest::RequestBuilder,
) -> BoxFuture<'_, std::result::Result<reqwest::Response, reqwest::Error>>
+ 'static + 'static
+ Send + Send
+ Sync; + Sync;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Http(reqwest::Error, Option<Value>), Http(StatusCode, surf::Response),
Surf(surf::Error),
Deserialization(serde_json::Error, Option<Value>), Deserialization(serde_json::Error, Option<Value>),
Header(reqwest::header::InvalidHeaderValue),
ChronoParse(chrono::ParseError), ChronoParse(chrono::ParseError),
UnexpectedType, UnexpectedType,
} }
@ -44,15 +41,9 @@ impl std::fmt::Display for Error {
} }
} }
impl From<reqwest::Error> for Error { impl From<surf::Error> for Error {
fn from(error: reqwest::Error) -> Self { fn from(error: surf::Error) -> Self {
Error::Http(error, None) Error::Surf(error)
}
}
impl From<reqwest::header::InvalidHeaderValue> for Error {
fn from(error: reqwest::header::InvalidHeaderValue) -> Self {
Error::Header(error)
} }
} }
@ -68,37 +59,31 @@ impl From<chrono::ParseError> for Error {
} }
} }
async fn try_to_parse_response<T: std::fmt::Debug + for<'de> serde::Deserialize<'de>>( // async fn try_to_parse_response<T: std::fmt::Debug + for<'de> serde::Deserialize<'de>>(
response: reqwest::Response, // mut response: surf::Response,
) -> Result<T> { // ) -> Result<T> {
let text = response.text().await?; // let text = response.body_string().await?;
match serde_json::from_str::<T>(&text) { // match serde_json::from_str::<T>(&text) {
Ok(value) => Ok(value), // Ok(value) => Ok(value),
Err(error) => match serde_json::from_str::<Value>(&text) { // Err(error) => match serde_json::from_str::<Value>(&text) {
Ok(body) => Err(Error::Deserialization(error, Some(body))), // Ok(body) => Err(Error::Deserialization(error, Some(body))),
_ => Err(Error::Deserialization(error, Some(Value::String(text)))), // _ => Err(Error::Deserialization(error, Some(Value::String(text)))),
}, // },
} // }
} // }
#[cfg(feature = "request")] #[cfg(feature = "request")]
fn get_http_client(notion_api_key: &str) -> reqwest::Client { fn get_http_client(notion_api_key: &str) -> surf::Client {
let mut headers = HeaderMap::new(); log::trace!("Readying HTTP Client");
headers.insert( surf::Config::new()
"Authorization", .add_header("Authorization", format!("Bearer {notion_api_key}"))
HeaderValue::from_str(&format!("Bearer {notion_api_key}")) .expect("to add Authorization header")
.expect("bearer token to be parsed into a header"), .add_header("Notion-Version", NOTION_VERSION)
); .expect("to add Notion-Version header")
headers.insert( .add_header("Content-Type", "application/json")
"Notion-Version", .expect("to add Content-Type header")
HeaderValue::from_str(NOTION_VERSION).expect("notion version to be parsed into a header"), .try_into()
);
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") .expect("to build a valid client out of notion_api_key")
} }
@ -132,8 +117,8 @@ impl ClientBuilder {
pub fn custom_request<F>(mut self, callback: F) -> Self pub fn custom_request<F>(mut self, callback: F) -> Self
where where
for<'c> F: Fn( for<'c> F: Fn(
&'c mut reqwest::RequestBuilder, surf::RequestBuilder,
) -> BoxFuture<'c, std::result::Result<reqwest::Response, reqwest::Error>> ) -> BoxFuture<'static, std::result::Result<surf::Response, surf::Error>>
+ 'static + 'static
+ Send + Send
+ Sync, + Sync,
@ -147,17 +132,11 @@ impl ClientBuilder {
pub fn build(self) -> Client { 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( let request_handler =
|request_builder: &mut reqwest::RequestBuilder| { self.custom_request
Box::pin(async move { .unwrap_or(Arc::new(|request_builder: surf::RequestBuilder| {
let request = request_builder Box::pin(request_builder)
.try_clone() }));
.expect("non-stream body request clone to succeed");
request.send().await
})
},
));
let http_client = Arc::from(get_http_client(&notion_api_key)); let http_client = Arc::from(get_http_client(&notion_api_key));
@ -186,7 +165,7 @@ impl ClientBuilder {
} }
pub struct Client { pub struct Client {
http_client: Arc<reqwest::Client>, http_client: Arc<surf::Client>,
request_handler: Arc<Callback>, request_handler: Arc<Callback>,
pub pages: Pages, pub pages: Pages,
@ -196,6 +175,7 @@ pub struct Client {
} }
impl<'a> Client { impl<'a> Client {
#[allow(clippy::new_ret_no_self)]
pub fn new() -> ClientBuilder { pub fn new() -> ClientBuilder {
ClientBuilder::default() ClientBuilder::default()
} }
@ -204,19 +184,19 @@ impl<'a> Client {
self, self,
options: SearchOptions<'b>, options: SearchOptions<'b>,
) -> Result<QueryResponse<T>> { ) -> Result<QueryResponse<T>> {
let mut request = self let request = self
.http_client .http_client
.post("https://api.notion.com/v1/search") .post("https://api.notion.com/v1/search")
.json(&options); .body_json(&options)
.expect("to parse JSON for doing `search`");
let response = (self.request_handler)(&mut request).await?; let mut response = (self.request_handler)(request)
.await
.expect("to request through a request handler");
match response.error_for_status_ref() { match response.status() {
Ok(_) => Ok(response.json().await?), StatusCode::Ok => Ok(response.body_json().await?),
Err(error) => { status => Err(Error::Http(status, response)),
let body = response.json::<Value>().await?;
Err(Error::Http(error, Some(body)))
}
} }
} }
} }
@ -227,34 +207,30 @@ pub struct PageOptions<'a> {
#[derive(Clone)] #[derive(Clone)]
pub struct Pages { pub struct Pages {
http_client: Arc<reqwest::Client>, http_client: Arc<surf::Client>,
request_handler: Arc<Callback>, request_handler: Arc<Callback>,
} }
impl Pages { impl Pages {
pub async fn retrieve<'a>(self, options: PageOptions<'a>) -> Result<Page> { pub async fn retrieve(self, options: PageOptions<'_>) -> Result<Page> {
let url = format!( let url = format!(
"https://api.notion.com/v1/pages/{page_id}", "https://api.notion.com/v1/pages/{page_id}",
page_id = options.page_id page_id = options.page_id
); );
let mut request = self.http_client.get(url); let request = self.http_client.get(url);
let mut response = (self.request_handler)(request).await?;
let response = (self.request_handler)(&mut request).await?; match response.status() {
StatusCode::Ok => Ok(response.body_json().await?),
match response.error_for_status_ref() { status => Err(Error::Http(status, response)),
Ok(_) => Ok(response.json().await?),
Err(error) => {
let body = response.json::<Value>().await?;
Err(Error::Http(error, Some(body)))
}
} }
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Blocks { pub struct Blocks {
http_client: Arc<reqwest::Client>, http_client: Arc<surf::Client>,
request_handler: Arc<Callback>, request_handler: Arc<Callback>,
} }
@ -268,7 +244,7 @@ impl Blocks {
} }
pub struct BlockChildren { pub struct BlockChildren {
http_client: Arc<reqwest::Client>, http_client: Arc<surf::Client>,
request_handler: Arc<Callback>, request_handler: Arc<Callback>,
} }
@ -277,32 +253,26 @@ pub struct BlockChildrenListOptions<'a> {
} }
impl BlockChildren { impl BlockChildren {
pub async fn list<'a>( pub async fn list(self, options: BlockChildrenListOptions<'_>) -> Result<QueryResponse<Block>> {
self,
options: BlockChildrenListOptions<'a>,
) -> Result<QueryResponse<Block>> {
let url = format!( let url = format!(
"https://api.notion.com/v1/blocks/{block_id}/children", "https://api.notion.com/v1/blocks/{block_id}/children",
block_id = options.block_id block_id = options.block_id
); );
let mut request = self.http_client.get(&url); let request = self.http_client.get(&url);
let response = (self.request_handler)(&mut request).await?; let mut response = (self.request_handler)(request).await?;
match response.error_for_status_ref() { match response.status() {
Ok(_) => Ok(response.json().await?), StatusCode::Ok => Ok(response.body_json().await?),
Err(error) => { status => Err(Error::Http(status, response)),
let body = response.json::<Value>().await?;
Err(Error::Http(error, Some(body)))
}
} }
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Databases { pub struct Databases {
http_client: Arc<reqwest::Client>, http_client: Arc<surf::Client>,
request_handler: Arc<Callback>, request_handler: Arc<Callback>,
} }
@ -318,11 +288,7 @@ impl Databases {
let mut request = self.http_client.post(url); let mut request = self.http_client.post(url);
let json = if let Some(filter) = options.filter { let json = options.filter.map(|filter| json!({ "filter": filter }));
Some(json!({ "filter": filter }))
} else {
None
};
let json = if let Some(sorts) = options.sorts { let json = if let Some(sorts) = options.sorts {
if let Some(mut json) = json { if let Some(mut json) = json {
@ -352,18 +318,19 @@ impl Databases {
json json
}; };
if let Some(json) = json { if let Some(ref json) = json {
request = request.json(&json); request = request
.body_json(json)
.expect("to parse JSON for start_cursor");
} }
let response = (self.request_handler)(&mut request).await?; log::trace!("Querying database with request: {request:#?} and body: {json:#?}");
match response.error_for_status_ref() { let mut response = (self.request_handler)(request).await?;
Ok(_) => try_to_parse_response(response).await,
Err(error) => { match response.status() {
let body = try_to_parse_response::<Value>(response).await?; StatusCode::Ok => Ok(response.body_json().await?),
Err(Error::Http(error, Some(body))) status => Err(Error::Http(status, response)),
}
} }
} }
} }
@ -372,7 +339,7 @@ impl Databases {
mod tests { mod tests {
use super::*; use super::*;
#[tokio::test] #[async_std::test]
async fn check_database_query() { async fn check_database_query() {
let databases = Client::new() let databases = Client::new()
.api_key("secret_FuhJkAoOVZlk8YUT9ZOeYqWBRRZN6OMISJwhb4dTnud") .api_key("secret_FuhJkAoOVZlk8YUT9ZOeYqWBRRZN6OMISJwhb4dTnud")
@ -394,7 +361,7 @@ mod tests {
println!("{databases:#?}"); println!("{databases:#?}");
} }
#[tokio::test] #[async_std::test]
async fn test_blocks() { async fn test_blocks() {
let blocks = Client::new() let blocks = Client::new()
.api_key("secret_FuhJkAoOVZlk8YUT9ZOeYqWBRRZN6OMISJwhb4dTnud") .api_key("secret_FuhJkAoOVZlk8YUT9ZOeYqWBRRZN6OMISJwhb4dTnud")
@ -421,7 +388,7 @@ pub struct DatabaseQueryOptions<'a> {
#[derive(Clone)] #[derive(Clone)]
pub struct Users { pub struct Users {
http_client: Arc<reqwest::Client>, http_client: Arc<surf::Client>,
request_handler: Arc<Callback>, request_handler: Arc<Callback>,
} }
@ -429,16 +396,13 @@ impl Users {
pub async fn get(&self) -> Result<QueryResponse<User>> { pub async fn get(&self) -> Result<QueryResponse<User>> {
let url = "https://api.notion.com/v1/users".to_owned(); let url = "https://api.notion.com/v1/users".to_owned();
let mut request = self.http_client.get(&url); let request = self.http_client.get(&url);
let response = (self.request_handler)(&mut request).await?; let mut response = (self.request_handler)(request).await?;
match response.error_for_status_ref() { match response.status() {
Ok(_) => Ok(response.json().await?), StatusCode::Ok => Ok(response.body_json().await?),
Err(error) => { status => Err(Error::Http(status, response)),
let body = response.json::<Value>().await?;
Err(Error::Http(error, Some(body)))
}
} }
} }
} }
@ -727,6 +691,9 @@ pub enum CodeLanguage {
YAML, YAML,
#[serde(rename = "java/c/c++/c#")] #[serde(rename = "java/c/c++/c#")]
JavaCCppCSharp, JavaCCppCSharp,
#[serde(other)]
Unsupported,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@ -957,7 +924,7 @@ where
serde_json::from_value::<DatabaseProperty>(value.to_owned()).unwrap_or_else(|error| { serde_json::from_value::<DatabaseProperty>(value.to_owned()).unwrap_or_else(|error| {
log::warn!( log::warn!(
"Could not parse value because of error, defaulting to DatabaseProperty::Unsupported:\n= ERROR:\n{error:#?}\n= JSON:\n{:#?}\n---", "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).unwrap() serde_json::to_string_pretty(&value).expect("to pretty print the database property error")
); );
DatabaseProperty::Unsupported(value.to_owned()) DatabaseProperty::Unsupported(value.to_owned())
}), }),
@ -983,7 +950,7 @@ pub struct Relation {
// TODO: Paginate all possible responses // TODO: Paginate all possible responses
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct QueryResponse<T> { pub struct QueryResponse<T> {
pub has_more: bool, pub has_more: Option<bool>,
pub next_cursor: Option<String>, pub next_cursor: Option<String>,
pub results: Vec<T>, pub results: Vec<T>,
} }
@ -1180,7 +1147,7 @@ where
// Notion forgets to set the formula type, so we're doing it's homework // Notion forgets to set the formula type, so we're doing it's homework
"formula" => { "formula" => {
if let Value::Object(object) = value { if let Value::Object(object) = value {
if let None = object.get("type") { if object.get("type").is_none() {
object.insert("type".to_owned(), json!("string")); object.insert("type".to_owned(), json!("string"));
} }
} }
@ -1223,7 +1190,7 @@ where
serde_json::from_value::<Property>(value.to_owned()).unwrap_or_else(|error| { serde_json::from_value::<Property>(value.to_owned()).unwrap_or_else(|error| {
log::warn!( log::warn!(
"Could not parse value because of error, defaulting to Property::Unsupported:\n= ERROR:\n{error:#?}\n= JSON:\n{}\n---", "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).unwrap() serde_json::to_string_pretty(&value).expect("to pretty print Property errors")
); );
Property::Unsupported(value.to_owned()) Property::Unsupported(value.to_owned())
}), }),
@ -1236,10 +1203,21 @@ where
#[serde(tag = "type")] #[serde(tag = "type")]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Formula { pub enum Formula {
Boolean { boolean: Option<bool> }, Boolean {
Date { date: Option<Date> }, boolean: Option<bool>,
Number { number: Option<f32> }, },
String { string: Option<String> }, Date {
date: Option<Date>,
},
Number {
number: Option<f32>,
},
String {
string: Option<String>,
},
#[serde(other)]
Unsupported,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@ -1293,6 +1271,9 @@ pub enum RichText {
href: Option<String>, href: Option<String>,
annotations: Annotations, annotations: Annotations,
}, },
#[serde(other)]
Unsupported,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@ -1305,11 +1286,24 @@ pub struct Text {
#[serde(tag = "type")] #[serde(tag = "type")]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Mention { pub enum Mention {
Database { database: PartialDatabase }, Database {
Date { date: Date }, database: PartialDatabase,
LinkPreview { link_preview: LinkPreview }, },
Page { page: PartialPage }, Date {
User { user: PartialUser }, date: Date,
},
LinkPreview {
link_preview: LinkPreview,
},
Page {
page: PartialPage,
},
User {
user: PartialUser,
},
#[serde(other)]
Unsupported,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@ -1435,33 +1429,60 @@ pub enum Color {
PurpleBackground, PurpleBackground,
PinkBackground, PinkBackground,
RedBackground, RedBackground,
#[serde(other)]
Unsupported,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Parent { pub enum Parent {
PageId { page_id: String }, PageId {
DatabaseId { database_id: String }, page_id: String,
BlockId { block_id: String }, },
DatabaseId {
database_id: String,
},
BlockId {
block_id: String,
},
Workspace, Workspace,
#[serde(other)]
Unsupported,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum File { pub enum File {
File { file: NotionFile }, File {
External { external: ExternalFile }, file: NotionFile,
},
External {
external: ExternalFile,
},
#[serde(other)]
Unsupported,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Icon { pub enum Icon {
Emoji { emoji: String }, Emoji {
File { file: NotionFile }, emoji: String,
External { external: ExternalFile }, },
File {
file: NotionFile,
},
External {
external: ExternalFile,
},
#[serde(other)]
Unsupported,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@ -1482,4 +1503,7 @@ pub enum DatabaseFormulaType {
Date, Date,
Number, Number,
String, String,
#[serde(other)]
Unsupported,
} }