notion-client/src/lib.rs

1342 lines
31 KiB
Rust

use std::collections::HashMap;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
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");
}
#[cfg(feature = "request")]
const NOTION_VERSION: &str = "2022-06-28";
pub type Result<T> = std::result::Result<T, Error>;
pub type Callback = dyn Fn(
&mut reqwest::RequestBuilder,
) -> BoxFuture<'_, std::result::Result<reqwest::Response, reqwest::Error>>
+ 'static
+ Send
+ Sync;
#[derive(Debug)]
pub enum Error {
Http(reqwest::Error, Option<Value>),
Deserialization(serde_json::Error, Option<Value>),
Header(reqwest::header::InvalidHeaderValue),
ChronoParse(chrono::ParseError),
}
impl std::fmt::Display for Error {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
Ok(write!(formatter, "NotionError::{:?}", self)?)
}
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Error::Http(error, None)
}
}
impl From<reqwest::header::InvalidHeaderValue> for Error {
fn from(error: reqwest::header::InvalidHeaderValue) -> Self {
Error::Header(error)
}
}
impl From<serde_json::Error> for Error {
fn from(error: serde_json::Error) -> Self {
Error::Deserialization(error, None)
}
}
impl From<chrono::ParseError> for Error {
fn from(error: chrono::ParseError) -> Self {
Error::ChronoParse(error)
}
}
async fn try_to_parse_response<T: std::fmt::Debug + for<'de> serde::Deserialize<'de>>(
response: reqwest::Response,
) -> Result<T> {
let text = response.text().await?;
match serde_json::from_str::<T>(&text) {
Ok(value) => Ok(value),
Err(error) => match serde_json::from_str::<Value>(&text) {
Ok(body) => {
println!("Error: {error:#?}\n\nBody: {body:#?}");
Err(Error::Deserialization(error, None))
}
_ => {
println!("Error: {error:#?}\n\nBody: {text}");
Err(Error::Deserialization(error, None))
}
},
}
}
#[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("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")
}
#[derive(Serialize)]
pub struct SearchOptions<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_cursor: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_size: Option<u32>,
}
#[derive(Default)]
pub struct ClientBuilder {
api_key: Option<String>,
custom_request: Option<Arc<Callback>>,
}
impl ClientBuilder {
pub fn api_key(mut self, api_key: &str) -> Self {
self.api_key = Some(api_key.to_owned());
self
}
pub fn custom_request<F>(mut self, callback: F) -> Self
where
for<'c> F: Fn(
&'c mut reqwest::RequestBuilder,
) -> BoxFuture<'c, std::result::Result<reqwest::Response, reqwest::Error>>
+ 'static
+ Send
+ 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 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
})
},
));
let http_client = Arc::from(get_http_client(&notion_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 {
http_client: http_client.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(),
},
}
}
}
pub struct Client {
http_client: Arc<reqwest::Client>,
request_handler: Arc<Callback>,
pub pages: Pages,
pub blocks: Blocks,
pub databases: Databases,
pub users: Users,
}
impl<'a> Client {
pub fn new() -> ClientBuilder {
ClientBuilder::default()
}
pub async fn search<'b, T: std::fmt::Debug + for<'de> serde::Deserialize<'de>>(
self,
options: SearchOptions<'b>,
) -> Result<QueryResponse<T>> {
let mut request = self
.http_client
.post("https://api.notion.com/v1/search")
.json(&options);
let response = (self.request_handler)(&mut request).await?;
match response.error_for_status_ref() {
Ok(_) => Ok(response.json().await?),
Err(error) => {
let body = response.json::<Value>().await?;
Err(Error::Http(error, Some(body)))
}
}
}
}
pub struct PageOptions<'a> {
pub page_id: &'a str,
}
#[derive(Clone)]
pub struct Pages {
http_client: Arc<reqwest::Client>,
request_handler: Arc<Callback>,
}
impl Pages {
pub async fn retrieve<'a>(self, options: PageOptions<'a>) -> Result<Page> {
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 response = (self.request_handler)(&mut request).await?;
match response.error_for_status_ref() {
Ok(_) => Ok(response.json().await?),
Err(error) => {
let body = response.json::<Value>().await?;
Err(Error::Http(error, Some(body)))
}
}
}
}
#[derive(Clone)]
pub struct Blocks {
http_client: Arc<reqwest::Client>,
request_handler: Arc<Callback>,
}
impl Blocks {
pub fn children(&self) -> BlockChildren {
BlockChildren {
http_client: self.http_client.clone(),
request_handler: self.request_handler.clone(),
}
}
}
pub struct BlockChildren {
http_client: Arc<reqwest::Client>,
request_handler: Arc<Callback>,
}
pub struct BlockChildrenListOptions<'a> {
pub block_id: &'a str,
}
impl BlockChildren {
pub async fn list<'a>(
self,
options: BlockChildrenListOptions<'a>,
) -> Result<QueryResponse<Block>> {
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 response = (self.request_handler)(&mut request).await?;
match response.error_for_status_ref() {
Ok(_) => Ok(response.json().await?),
Err(error) => {
let body = response.json::<Value>().await?;
Err(Error::Http(error, Some(body)))
}
}
}
}
#[derive(Clone)]
pub struct Databases {
http_client: Arc<reqwest::Client>,
request_handler: Arc<Callback>,
}
impl Databases {
pub async fn query<'a>(
&self,
options: DatabaseQueryOptions<'a>,
) -> Result<QueryResponse<Page>> {
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 json = if let Some(filter) = options.filter {
Some(json!({ "filter": filter }))
} else {
None
};
let json = if let Some(sorts) = options.sorts {
if let Some(mut json) = json {
json.as_object_mut()
.expect("Some object to be editable")
.insert("sorts".to_string(), sorts);
Some(json)
} else {
Some(json!({ "sorts": sorts }))
}
} else {
json
};
if let Some(json) = json {
request = request.json(&json);
}
let response = (self.request_handler)(&mut request).await?;
match response.error_for_status_ref() {
Ok(_) => try_to_parse_response(response).await,
Err(error) => {
let body = try_to_parse_response::<Value>(response).await?;
Err(Error::Http(error, Some(body)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn check_database_query() {
let databases = Client::new()
.api_key("secret_FuhJkAoOVZlk8YUT9ZOeYqWBRRZN6OMISJwhb4dTnud")
.build()
.search::<Database>(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<Value>,
pub sorts: Option<Value>,
}
#[derive(Clone)]
pub struct Users {
http_client: Arc<reqwest::Client>,
request_handler: Arc<Callback>,
}
impl Users {
pub async fn get(&self) -> Result<QueryResponse<User>> {
let url = "https://api.notion.com/v1/users".to_owned();
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?),
Err(error) => {
let body = response.json::<Value>().await?;
Err(Error::Http(error, Some(body)))
}
}
}
}
// Start of normal entities
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
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,
#[serde(flatten)]
pub block: BlockType,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum BlockType {
Paragraph {
paragraph: Paragraph,
},
Bookmark {
bookmark: Bookmark,
},
Breadcrumb,
BulletedListItem {
bulleted_list_item: ListItem,
},
Callout {
callout: Callout,
},
ChildDatabase,
ChildPage,
Code {
code: Code,
},
Column,
ColumnList,
Divider,
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, PartialEq)]
pub struct Embed {
url: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Bookmark {
caption: Vec<RichText>,
url: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Heading {
pub color: Color,
pub rich_text: Vec<RichText>,
pub is_toggleable: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PDF {
pub pdf: File,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct FileBlock {
pub file: File,
pub caption: Vec<RichText>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ColumnList {
pub children: Option<Vec<Column>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Column {
pub children: Option<Vec<Block>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Callout {
pub icon: Option<Icon>,
pub color: Color,
pub rich_text: Vec<RichText>,
pub children: Option<Vec<Block>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Quote {
pub color: Color,
pub rich_text: Vec<RichText>,
pub children: Option<Vec<Block>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ToDoItem {
pub color: Color,
pub rich_text: Vec<RichText>,
pub checked: Option<bool>,
pub children: Option<Vec<Block>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ListItem {
pub color: Color,
pub rich_text: Vec<RichText>,
pub children: Option<Vec<Block>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Paragraph {
pub color: Color,
pub rich_text: Vec<RichText>,
pub children: Option<Vec<Block>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct Code {
caption: Vec<RichText>,
rich_text: Vec<RichText>,
language: CodeLanguage,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[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, PartialEq)]
pub struct ChildPage {
pub title: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ChildDatabase {
pub title: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct User {
pub id: String,
pub name: Option<String>,
pub person: Option<Person>,
pub avatar_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Workspace {
pub workspace: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Person {
pub email: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Database {
pub id: String,
pub title: Vec<RichText>,
pub description: Vec<RichText>,
pub properties: HashMap<String, DatabaseProperty>,
pub url: String,
pub parent: Parent,
pub created_time: DateValue,
pub last_edited_time: DateValue,
pub last_edited_by: PartialUser,
pub icon: Option<Icon>,
pub cover: Option<File>,
pub archived: 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<SelectOption>,
}
#[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<String> {
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<String> {
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, PartialEq)]
pub struct QueryResponse<T> {
pub has_more: bool,
pub next_cursor: Option<String>,
pub results: Vec<T>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
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<File>,
pub icon: Option<Icon>,
pub properties: HashMap<String, Property>,
pub archived: bool,
}
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
})
}
}
#[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<Date>,
},
Email {
id: String,
email: Option<String>,
},
Files {
id: String,
files: Vec<File>,
},
Formula {
id: String,
formula: Formula,
},
LastEditedBy {
id: String,
}, // TODO: Implement LastEditedBy
LastEditedTime {
id: String,
last_edited_time: DateValue,
},
Select {
id: String,
select: Option<SelectOption>,
},
MultiSelect {
id: String,
multi_select: Vec<SelectOption>,
},
Number {
id: String,
number: Option<f32>,
},
People {
id: String,
},
PhoneNumber {
id: String,
},
Relation {
id: String,
relation: Vec<Relation>,
},
Rollup {
id: String,
}, // TODO: Implement Rollup
RichText {
id: String,
rich_text: Vec<RichText>,
},
Status {
id: String,
}, // TODO: Implement Status
Title {
id: String,
title: Vec<RichText>,
},
Url {
id: String,
url: Option<String>,
},
// TODO: Implement Unsupported(Value)
#[serde(other)]
Unsupported,
}
impl Property {
pub fn id(&self) -> Option<String> {
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,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum Formula {
Boolean { boolean: Option<bool> },
Date { date: Option<Date> },
Number { number: Option<f32> },
String { string: Option<String> },
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PartialUser {
pub id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PartialProperty {
pub id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct DatabaseFormula {
pub expression: String,
pub suspected_type: Option<DatabaseFormulaType>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct SelectOption {
pub id: String,
pub name: String,
pub color: Color,
}
impl std::fmt::Display for SelectOption {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
Ok(write!(formatter, "MultiSelectOption::{}", self.name)?)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
pub enum RichText {
Text {
text: Text,
plain_text: String,
href: Option<String>,
annotations: Annotations,
},
Mention {
mention: Mention,
plain_text: String,
href: Option<String>,
annotations: Annotations,
},
Equation {
expression: Option<String>,
plain_text: String,
href: Option<String>,
annotations: Annotations,
},
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Text {
pub content: String,
pub link: Option<Link>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
pub enum Mention {
Database { database: PartialDatabase },
Date { date: Date },
LinkPreview { link_preview: LinkPreview },
Page { page: PartialPage },
User { user: PartialUser },
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct LinkPreview {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PartialPage {
pub id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PartialDatabase {
pub id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PartialBlock {
pub id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Date {
pub start: DateValue,
pub end: Option<DateValue>,
// TODO: Implement for setting
pub time_zone: Option<String>,
}
impl std::fmt::Display for Date {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
let start = &self.start;
if let Some(end) = &self.end {
Ok(write!(formatter, "{start} - {end}")?)
} else {
Ok(write!(formatter, "{start}")?)
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(try_from = "String", into = "String")]
pub enum DateValue {
DateTime(DateTime<Utc>),
Date(chrono::NaiveDate),
}
impl TryFrom<String> for DateValue {
type Error = Error;
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"))?.date_naive(),
)
} else {
DateValue::DateTime(DateTime::parse_from_rfc3339(&string)?.with_timezone(&Utc))
};
Ok(value)
}
}
impl From<DateValue> for String {
fn from(value: DateValue) -> String {
value.to_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) => DateTime::<Utc>::from_utc(
date.and_hms_opt(0, 0, 0)
.expect("to parse NaiveDate into DateTime "),
Utc,
)
.to_rfc3339(),
DateValue::DateTime(date_time) => date_time.to_rfc3339(),
};
Ok(write!(formatter, "{}", value)?)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Equation {
pub plain_text: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Link {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct Annotations {
pub bold: bool,
pub italic: bool,
pub strikethrough: bool,
pub underline: bool,
pub code: bool,
pub color: Color,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Color {
#[default]
Default,
Gray,
Brown,
Orange,
Yellow,
Green,
Blue,
Purple,
Pink,
Red,
GrayBackground,
BrownBackground,
OrangeBackground,
YellowBackground,
GreenBackground,
BlueBackground,
PurpleBackground,
PinkBackground,
RedBackground,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum Parent {
PageId { page_id: String },
DatabaseId { database_id: String },
BlockId { block_id: String },
Workspace,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
pub enum File {
File { file: NotionFile },
External { external: ExternalFile },
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
pub enum Icon {
Emoji { emoji: String },
File { file: NotionFile },
External { external: ExternalFile },
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct NotionFile {
expiry_time: DateValue,
url: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ExternalFile {
url: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum DatabaseFormulaType {
Boolean,
Date,
Number,
String,
}