Compare commits

..

No commits in common. "main" and "feat-switch-to-surf-instead-of-reqwest" have entirely different histories.

5 changed files with 36 additions and 266 deletions

0
.gitignore vendored Executable file → Normal file
View file

3
Cargo.toml Executable file → Normal file
View file

@ -1,7 +1,7 @@
[package] [package]
name = "notion-client" name = "notion-client"
version = "0.1.0" version = "0.1.0"
edition = "2024" 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]
@ -20,4 +20,3 @@ regex = "1.7.1"
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 } surf = { version = "2.3.2", default-features = false }
uuid = "1.16.0"

0
LICENSE Executable file → Normal file
View file

0
README.md Executable file → Normal file
View file

299
src/lib.rs Executable file → Normal file
View file

@ -1,12 +1,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use chrono::{DateTime, Utc}; use chrono::{DateTime, NaiveTime, Utc};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
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::Value;
use serde_json::json; use serde_json::json;
use serde_json::Value;
use surf::http::StatusCode; use surf::http::StatusCode;
use futures_core::future::BoxFuture; use futures_core::future::BoxFuture;
@ -276,7 +276,6 @@ pub struct Databases<'a> {
} }
impl Databases<'_> { impl Databases<'_> {
// FIXME: This call can have Databases mixed in with the Page's, needs an intermediary
pub async fn query<'a>( pub async fn query<'a>(
&self, &self,
options: DatabaseQueryOptions<'a>, options: DatabaseQueryOptions<'a>,
@ -389,7 +388,9 @@ pub struct Users<'a> {
impl Users<'_> { impl Users<'_> {
pub async fn get(&self) -> Result<QueryResponse<User>> { pub async fn get(&self) -> Result<QueryResponse<User>> {
let request = self.http_client.get("https://api.notion.com/v1/users"); 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?; let mut response = (self.request_handler)(request).await?;
@ -701,7 +702,7 @@ pub struct ChildDatabase {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct User { pub struct User {
pub id: uuid::Uuid, pub id: String,
pub name: Option<String>, pub name: Option<String>,
pub person: Option<Person>, pub person: Option<Person>,
pub avatar_url: Option<String>, pub avatar_url: Option<String>,
@ -802,7 +803,7 @@ pub enum DatabaseProperty {
Relation { Relation {
id: String, id: String,
name: String, name: String,
relation: DatabaseRelation, // relation: Relation,
}, },
RichText { RichText {
id: String, id: String,
@ -811,11 +812,7 @@ pub enum DatabaseProperty {
Rollup { Rollup {
id: String, id: String,
name: String, name: String,
function: RollupFunction, // TODO: Implement Rollup
relation_property_id: String,
relation_property_name: String,
rollup_property_id: String,
rollup_property_name: String,
}, },
Select { Select {
id: String, id: String,
@ -825,8 +822,7 @@ pub enum DatabaseProperty {
Status { Status {
id: String, id: String,
name: String, name: String,
// TODO: add "groups" // TODO: Implement Status
options: DatabaseSelectOptions,
}, },
Title { Title {
id: String, id: String,
@ -845,7 +841,7 @@ pub enum DatabaseProperty {
} }
impl DatabaseProperty { impl DatabaseProperty {
pub fn id(&self) -> Option<&str> { pub fn id(&self) -> Option<String> {
use DatabaseProperty::*; use DatabaseProperty::*;
match self { match self {
@ -868,13 +864,13 @@ impl DatabaseProperty {
| Status { id, .. } | Status { id, .. }
| Title { id, .. } | Title { id, .. }
| Button { id, .. } | Button { id, .. }
| Url { id, .. } => Some(id), | Url { id, .. } => Some(id.to_owned()),
Unsupported(..) => None, Unsupported(..) => None,
} }
} }
pub fn name(&self) -> Option<&str> { pub fn name(&self) -> Option<String> {
use DatabaseProperty::*; use DatabaseProperty::*;
match self { match self {
@ -897,7 +893,7 @@ impl DatabaseProperty {
| Status { name, .. } | Status { name, .. }
| Button { name, .. } | Button { name, .. }
| Title { name, .. } | Title { name, .. }
| Url { name, .. } => Some(name), | Url { name, .. } => Some(name.to_owned()),
Unsupported(..) => None, Unsupported(..) => None,
} }
@ -921,7 +917,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 DatabaseProperty 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).expect("to pretty print the database property error") serde_json::to_string_pretty(&value).expect("to pretty print the database property error")
); );
DatabaseProperty::Unsupported(value.to_owned()) DatabaseProperty::Unsupported(value.to_owned())
@ -933,179 +929,16 @@ where
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Number { pub struct Number {
pub format: NumberFormat, // TODO: Implement NumberFormat
} // pub format: NumberFormat
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum NumberFormat {
ArgentinePeso,
Baht,
AustralianDollar,
CanadianDollar,
ChileanPeso,
ColombianPeso,
DanishKrone,
Dirham,
Dollar,
Euro,
Forint,
Franc,
HongKongDollar,
Koruna,
Krona,
Leu,
Lira,
MexicanPeso,
NewTaiwanDollar,
NewZealandDollar,
NorwegianKrone,
Number,
NumberWithCommas,
Percent,
PhilippinePeso,
Pound,
PeruvianSol,
Rand,
Real,
Ringgit,
Riyal,
Ruble,
Rupee,
Rupiah,
Shekel,
SingaporeDollar,
UruguayanPeso,
Yen,
Yuan,
Won,
Zloty,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct DatabaseRelation {
database_id: String,
synced_property_name: String,
synced_property_id: String,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Relation { pub struct Relation {
id: String, // #[serde(alias = "database_id")]
} // id: String,
// synced_property_name: String,
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] // synced_property_id: String,
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
// TODO: Implement all enums here
pub enum Rollup {
Array {
function: RollupFunction,
array: Vec<RollupProperty>,
},
Date,
Incomplete,
Number,
Unsupported,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum RollupFunction {
Average,
Checked,
CountPerGroup,
Count,
CountValues,
DateRange,
EarliestDate,
Empty,
LatestDate,
Max,
Median,
Min,
NotEmpty,
PercentChecked,
PercentEmpty,
PercentNotEmpty,
PercentPerGroup,
PercentUnchecked,
Range,
Unchecked,
Unique,
ShowOriginal,
ShowUnique,
Sum,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum RollupProperty {
Checkbox {
checkbox: bool,
},
CreatedBy {},
CreatedTime {
created_time: DateValue,
},
Date {
date: Option<Date>,
},
Email {
email: Option<String>,
},
Files {
files: Vec<File>,
},
Formula {
name: Option<String>,
formula: Formula,
},
LastEditedBy {
last_edited_by: User,
},
LastEditedTime {
last_edited_time: DateValue,
},
Select {
select: Option<SelectOption>,
},
MultiSelect {
multi_select: Vec<SelectOption>,
},
Number {
number: Option<f32>,
},
People {},
PhoneNumber {},
Relation {
relation: Vec<Relation>,
},
Rollup {
rollup: Rollup,
},
RichText {
rich_text: Vec<RichText>,
},
Status {
status: Option<SelectOption>,
},
Title {
title: Vec<RichText>,
},
Url {
url: Option<String>,
},
Verification {
state: VerificationState,
verified_by: Option<User>,
date: Option<Date>,
},
UniqueId {},
Button {},
Unsupported(Value),
} }
// TODO: Paginate all possible responses // TODO: Paginate all possible responses
@ -1114,7 +947,6 @@ pub struct QueryResponse<T> {
pub has_more: Option<bool>, pub has_more: Option<bool>,
pub next_cursor: Option<String>, pub next_cursor: Option<String>,
pub results: Vec<T>, pub results: Vec<T>,
// pub page_or_database: Value,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@ -1134,7 +966,6 @@ pub struct Page {
pub properties: HashMap<String, Property>, pub properties: HashMap<String, Property>,
pub archived: bool, pub archived: bool,
pub in_trash: bool,
} }
impl Page { impl Page {
@ -1168,7 +999,6 @@ pub enum Property {
}, },
CreatedBy { CreatedBy {
id: String, id: String,
created_by: User,
}, },
CreatedTime { CreatedTime {
id: String, id: String,
@ -1193,8 +1023,7 @@ pub enum Property {
}, },
LastEditedBy { LastEditedBy {
id: String, id: String,
last_edited_by: User, }, // TODO: Implement LastEditedBy
},
LastEditedTime { LastEditedTime {
id: String, id: String,
last_edited_time: DateValue, last_edited_time: DateValue,
@ -1213,29 +1042,24 @@ pub enum Property {
}, },
People { People {
id: String, id: String,
people: Vec<User>,
}, },
PhoneNumber { PhoneNumber {
id: String, id: String,
phone_number: Option<String>,
}, },
Relation { Relation {
id: String, id: String,
has_more: bool, // relation: Vec<Relation>,
relation: Vec<Relation>,
}, },
Rollup { Rollup {
id: String, id: String,
rollup: Rollup, }, // TODO: Implement Rollup
},
RichText { RichText {
id: String, id: String,
rich_text: Vec<RichText>, rich_text: Vec<RichText>,
}, },
Status { Status {
id: String, id: String,
status: Option<SelectOption>, }, // TODO: Implement Status
},
Title { Title {
id: String, id: String,
title: Vec<RichText>, title: Vec<RichText>,
@ -1245,12 +1069,10 @@ pub enum Property {
url: Option<String>, url: Option<String>,
}, },
Verification { Verification {
id: String, id: String, // TODO: Implement
verification: Verification,
}, },
UniqueId { UniqueId {
id: String, id: String,
unique_id: UniqueId,
}, },
Button { Button {
id: String, id: String,
@ -1260,7 +1082,7 @@ pub enum Property {
} }
impl Property { impl Property {
pub fn id(&self) -> Option<&str> { pub fn id(&self) -> Option<String> {
use Property::*; use Property::*;
match self { match self {
@ -1286,7 +1108,7 @@ impl Property {
| Formula { id, .. } | Formula { id, .. }
| Verification { id, .. } | Verification { id, .. }
| Button { id, .. } | Button { id, .. }
| UniqueId { id, .. } => Some(id), | UniqueId { id, .. } => Some(id.to_owned()),
Unsupported(_) => None, Unsupported(_) => None,
} }
@ -1305,7 +1127,7 @@ where
.map_err(D::Error::custom)? .map_err(D::Error::custom)?
.into_iter() .into_iter()
.map(|(key, value)| { .map(|(key, value)| {
if let Value::Object(object) = 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 // Notion sometimes sends an empty object when it means "null", so we gotta do it's homework
for value in object.values_mut() { for value in object.values_mut() {
if value == &mut json!({}) { if value == &mut json!({}) {
@ -1361,7 +1183,7 @@ where
key.to_owned(), key.to_owned(),
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 Property 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).expect("to pretty print Property errors") serde_json::to_string_pretty(&value).expect("to pretty print Property errors")
); );
Property::Unsupported(value.to_owned()) Property::Unsupported(value.to_owned())
@ -1371,27 +1193,6 @@ where
.collect::<HashMap<String, Property>>()) .collect::<HashMap<String, Property>>())
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum VerificationState {
Verified,
Unverified,
Expired,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Verification {
state: VerificationState,
verified_by: Option<User>,
date: Option<Date>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct UniqueId {
number: i32,
prefix: Option<String>,
}
#[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")]
@ -1413,38 +1214,6 @@ pub enum Formula {
Unsupported, Unsupported,
} }
impl ToString for Formula {
fn to_string(&self) -> String {
match self {
Formula::String {
string: Some(string),
..
} => string.to_owned(),
Formula::Number {
number: Some(number),
..
} => format!("{number}"),
Formula::Boolean {
boolean: Some(boolean),
..
} => format!("{boolean}"),
Formula::Date {
date: Some(date), ..
} => format!("{date}"),
Formula::Unsupported => {
log::warn!("User tried stringifying unsupported formula");
"Formula output unsupported".to_owned()
}
Formula::Number { number: None, .. }
| Formula::Date { date: None, .. }
| Formula::Boolean { boolean: None, .. }
| Formula::String { string: None, .. } => "".to_owned(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PartialUser { pub struct PartialUser {
pub id: String, pub id: String,
@ -1555,7 +1324,7 @@ pub struct PartialBlock {
pub struct Date { pub struct Date {
pub start: DateValue, pub start: DateValue,
pub end: Option<DateValue>, pub end: Option<DateValue>,
// TODO: Implement for updating with timezone in the future? // TODO: Implement for setting
pub time_zone: Option<String>, pub time_zone: Option<String>,
} }
@ -1573,8 +1342,8 @@ impl std::fmt::Display for Date {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
pub enum DateValue { pub enum DateValue {
Date(DateTime<Utc>),
DateTime(DateTime<Utc>), DateTime(DateTime<Utc>),
Date(chrono::NaiveDate),
} }
impl TryFrom<String> for DateValue { impl TryFrom<String> for DateValue {
@ -1583,7 +1352,9 @@ impl TryFrom<String> for DateValue {
fn try_from(string: String) -> Result<DateValue> { fn try_from(string: String) -> Result<DateValue> {
// NOTE: is either ISO 8601 Date or assumed to be ISO 8601 DateTime // NOTE: is either ISO 8601 Date or assumed to be ISO 8601 DateTime
let value = if ISO_8601_DATE.is_match(&string) { let value = if ISO_8601_DATE.is_match(&string) {
DateValue::Date(DateTime::parse_from_rfc3339(&format!("{string}T00:00:00Z"))?.into()) DateValue::Date(
DateTime::parse_from_rfc3339(&format!("{string}T00:00:00Z"))?.date_naive(),
)
} else { } else {
DateValue::DateTime(DateTime::parse_from_rfc3339(&string)?.with_timezone(&Utc)) DateValue::DateTime(DateTime::parse_from_rfc3339(&string)?.with_timezone(&Utc))
}; };
@ -1601,7 +1372,7 @@ impl From<DateValue> for String {
impl std::fmt::Display for DateValue { impl std::fmt::Display for DateValue {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
let value = match self { let value = match self {
DateValue::Date(date) => date.format("%Y-%m-%d").to_string(), DateValue::Date(date) => date.and_time(NaiveTime::MIN).and_utc().to_rfc3339(),
DateValue::DateTime(date_time) => date_time.to_rfc3339(), DateValue::DateTime(date_time) => date_time.to_rfc3339(),
}; };
Ok(write!(formatter, "{}", value)?) Ok(write!(formatter, "{}", value)?)