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]
name = "notion-client"
version = "0.1.0"
edition = "2024"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
@ -20,4 +20,3 @@ regex = "1.7.1"
serde = { version = "^1.0", features = ["derive"], default-features = false }
serde_json = { version = "^1.0", features = ["raw_value"], 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 chrono::{DateTime, Utc};
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::Value;
use serde_json::json;
use serde_json::Value;
use surf::http::StatusCode;
use futures_core::future::BoxFuture;
@ -276,7 +276,6 @@ pub struct Databases<'a> {
}
impl Databases<'_> {
// FIXME: This call can have Databases mixed in with the Page's, needs an intermediary
pub async fn query<'a>(
&self,
options: DatabaseQueryOptions<'a>,
@ -389,7 +388,9 @@ pub struct Users<'a> {
impl Users<'_> {
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?;
@ -701,7 +702,7 @@ pub struct ChildDatabase {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct User {
pub id: uuid::Uuid,
pub id: String,
pub name: Option<String>,
pub person: Option<Person>,
pub avatar_url: Option<String>,
@ -802,7 +803,7 @@ pub enum DatabaseProperty {
Relation {
id: String,
name: String,
relation: DatabaseRelation,
// relation: Relation,
},
RichText {
id: String,
@ -811,11 +812,7 @@ pub enum DatabaseProperty {
Rollup {
id: String,
name: String,
function: RollupFunction,
relation_property_id: String,
relation_property_name: String,
rollup_property_id: String,
rollup_property_name: String,
// TODO: Implement Rollup
},
Select {
id: String,
@ -825,8 +822,7 @@ pub enum DatabaseProperty {
Status {
id: String,
name: String,
// TODO: add "groups"
options: DatabaseSelectOptions,
// TODO: Implement Status
},
Title {
id: String,
@ -845,7 +841,7 @@ pub enum DatabaseProperty {
}
impl DatabaseProperty {
pub fn id(&self) -> Option<&str> {
pub fn id(&self) -> Option<String> {
use DatabaseProperty::*;
match self {
@ -868,13 +864,13 @@ impl DatabaseProperty {
| Status { id, .. }
| Title { id, .. }
| Button { id, .. }
| Url { id, .. } => Some(id),
| Url { id, .. } => Some(id.to_owned()),
Unsupported(..) => None,
}
}
pub fn name(&self) -> Option<&str> {
pub fn name(&self) -> Option<String> {
use DatabaseProperty::*;
match self {
@ -897,7 +893,7 @@ impl DatabaseProperty {
| Status { name, .. }
| Button { name, .. }
| Title { name, .. }
| Url { name, .. } => Some(name),
| Url { name, .. } => Some(name.to_owned()),
Unsupported(..) => None,
}
@ -921,7 +917,7 @@ where
serde_json::from_value::<DatabaseProperty>(value.to_owned()).unwrap_or_else(|error| {
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")
);
DatabaseProperty::Unsupported(value.to_owned())
@ -933,179 +929,16 @@ where
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Number {
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,
// TODO: Implement NumberFormat
// pub format: NumberFormat
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Relation {
id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[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),
// #[serde(alias = "database_id")]
// id: String,
// synced_property_name: String,
// synced_property_id: String,
}
// TODO: Paginate all possible responses
@ -1114,7 +947,6 @@ pub struct QueryResponse<T> {
pub has_more: Option<bool>,
pub next_cursor: Option<String>,
pub results: Vec<T>,
pub page_or_database: Value,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@ -1134,7 +966,6 @@ pub struct Page {
pub properties: HashMap<String, Property>,
pub archived: bool,
pub in_trash: bool,
}
impl Page {
@ -1168,7 +999,6 @@ pub enum Property {
},
CreatedBy {
id: String,
created_by: User,
},
CreatedTime {
id: String,
@ -1193,8 +1023,7 @@ pub enum Property {
},
LastEditedBy {
id: String,
last_edited_by: User,
},
}, // TODO: Implement LastEditedBy
LastEditedTime {
id: String,
last_edited_time: DateValue,
@ -1213,29 +1042,24 @@ pub enum Property {
},
People {
id: String,
people: Vec<User>,
},
PhoneNumber {
id: String,
phone_number: Option<String>,
},
Relation {
id: String,
has_more: bool,
relation: Vec<Relation>,
// relation: Vec<Relation>,
},
Rollup {
id: String,
rollup: Rollup,
},
}, // TODO: Implement Rollup
RichText {
id: String,
rich_text: Vec<RichText>,
},
Status {
id: String,
status: Option<SelectOption>,
},
}, // TODO: Implement Status
Title {
id: String,
title: Vec<RichText>,
@ -1245,12 +1069,10 @@ pub enum Property {
url: Option<String>,
},
Verification {
id: String,
verification: Verification,
id: String, // TODO: Implement
},
UniqueId {
id: String,
unique_id: UniqueId,
},
Button {
id: String,
@ -1260,7 +1082,7 @@ pub enum Property {
}
impl Property {
pub fn id(&self) -> Option<&str> {
pub fn id(&self) -> Option<String> {
use Property::*;
match self {
@ -1286,7 +1108,7 @@ impl Property {
| Formula { id, .. }
| Verification { id, .. }
| Button { id, .. }
| UniqueId { id, .. } => Some(id),
| UniqueId { id, .. } => Some(id.to_owned()),
Unsupported(_) => None,
}
@ -1305,7 +1127,7 @@ where
.map_err(D::Error::custom)?
.into_iter()
.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
for value in object.values_mut() {
if value == &mut json!({}) {
@ -1361,7 +1183,7 @@ where
key.to_owned(),
serde_json::from_value::<Property>(value.to_owned()).unwrap_or_else(|error| {
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")
);
Property::Unsupported(value.to_owned())
@ -1371,27 +1193,6 @@ where
.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)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
@ -1413,38 +1214,6 @@ pub enum Formula {
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)]
pub struct PartialUser {
pub id: String,
@ -1555,7 +1324,7 @@ pub struct PartialBlock {
pub struct Date {
pub start: DateValue,
pub end: Option<DateValue>,
// TODO: Implement for updating with timezone in the future?
// TODO: Implement for setting
pub time_zone: Option<String>,
}
@ -1573,8 +1342,8 @@ impl std::fmt::Display for Date {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(try_from = "String", into = "String")]
pub enum DateValue {
Date(DateTime<Utc>),
DateTime(DateTime<Utc>),
Date(chrono::NaiveDate),
}
impl TryFrom<String> for DateValue {
@ -1583,7 +1352,9 @@ impl TryFrom<String> for DateValue {
fn try_from(string: String) -> Result<DateValue> {
// 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"))?.into())
DateValue::Date(
DateTime::parse_from_rfc3339(&format!("{string}T00:00:00Z"))?.date_naive(),
)
} else {
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 {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
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(),
};
Ok(write!(formatter, "{}", value)?)