diff options
author | 2021-05-14 07:18:20 -0700 | |
---|---|---|
committer | 2021-05-14 07:18:20 -0700 | |
commit | f7ef8d6d7f009990ba0c6be61cca507f097673b6 (patch) | |
tree | 1b58a5a8ca08f8fe8bb2a71ae832db21dd4953c0 | |
parent | 5b3816b043f3e97878b04b948241466a2527b94d (diff) | |
download | notion-f7ef8d6d7f009990ba0c6be61cca507f097673b6.tar.gz notion-f7ef8d6d7f009990ba0c6be61cca507f097673b6.tar.zst notion-f7ef8d6d7f009990ba0c6be61cca507f097673b6.zip |
First pass at a working Notion api client from the public beta
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | Cargo.toml | 24 | ||||
-rw-r--r-- | src/lib.rs | 70 | ||||
-rw-r--r-- | src/models.rs | 222 | ||||
-rw-r--r-- | src/models/search.rs | 89 |
5 files changed, 408 insertions, 1 deletions
@@ -1,6 +1,6 @@ # Generated by Cargo # will have compiled files and executables -/target/ +target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html @@ -8,3 +8,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +.api_token diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0c3f3d4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "notion" +version = "0.1.0" +authors = ["Jake Swenson <jake@jakeswenson.com>"] +edition = "2018" + +[dependencies] +serde_json = "1.0.64" + +[dependencies.chrono] +version = "0.4.19" +features = ["serde"] + +[dependencies.reqwest] +version = "0.11" +features = ["json"] + +[dependencies.tokio] +version = "1" +features = ["full"] + +[dependencies.serde] +version = "1.0" +features = ["derive"] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7a65f31 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,70 @@ +use crate::models::{Database, ListResponse}; +use std::collections::HashMap; +use crate::models::search::SearchRequest; + +mod models; + +struct NotionApi { + token: String +} + +impl NotionApi { + /// This method is apparently deprecated + pub async fn list_databases(&self) -> Result<ListResponse<Database>, Box<dyn std::error::Error>> { + let client = reqwest::ClientBuilder::new().build()?; + let json = client.get("https://api.notion.com/v1/databases") + .bearer_auth(self.token.clone()) + .send() + .await? + .text().await?; + dbg!(&json); + let result = serde_json::from_str(&json)?; + + Ok(result) + } + + + pub async fn search<T: Into<SearchRequest>>(&self, query: T) -> Result<ListResponse<Database>, Box<dyn std::error::Error>> { + let client = reqwest::ClientBuilder::new().build()?; + let json = client.post("https://api.notion.com/v1/search") + .bearer_auth(self.token.clone()) + .json(&query.into()) + .send() + .await? + .text().await?; + + dbg!(serde_json::from_str::<serde_json::Value>(&json)?); + let result = serde_json::from_str(&json)?; + + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use crate::NotionApi; + use crate::models::search::{NotionSearch, FilterValue, FilterProperty}; + const TEST_TOKEN: &'static str = include_str!(".api_token"); + + #[tokio::test] + async fn list_databases() -> Result<(), Box<dyn std::error::Error>> { + let api = NotionApi { + token: TEST_TOKEN.to_string() + }; + + dbg!(api.list_databases().await?); + + Ok(()) + } + + #[tokio::test] + async fn search() -> Result<(), Box<dyn std::error::Error>> { + let api = NotionApi { + token: TEST_TOKEN.to_string() + }; + + dbg!(api.search(NotionSearch::Filter {value: FilterValue::Database, property: FilterProperty::Object}).await?); + + Ok(()) + } +} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..c11c97b --- /dev/null +++ b/src/models.rs @@ -0,0 +1,222 @@ +pub mod search; + +use serde::{Serialize, Deserialize}; +use chrono::{DateTime, Utc}; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[serde(rename_all = "lowercase")] +enum ObjectType { + Database, + List, +} + +/// A zero-cost wrapper type around a Database ID +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[serde(transparent)] +pub struct DatabaseId(String); + +/// Represents a Notion Database +/// See https://developers.notion.com/reference/database +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Database { + /// Always "database" + object: ObjectType, + /// Unique identifier for the database. + id: DatabaseId, + /// Date and time when this database was created. + created_time: DateTime<Utc>, + /// Date and time when this database was updated. + last_edited_time: DateTime<Utc>, + /// Name of the database as it appears in Notion. + title: Vec<RichText>, + /// Schema of properties for the database as they appear in Notion. + // + // key string + // The name of the property as it appears in Notion. + // + // value object + // A Property object. + properties: HashMap<String, PropertyConfiguration>, +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct ListResponse<T> { + object: ObjectType, + results: Vec<T>, + next_cursor: Option<String>, + has_more: bool, +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[serde(transparent)] +struct PropertyId(String); + +/// How the number is displayed in Notion. +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +enum NumberFormat { + Number, + NumberWithCommas, + Percent, + Dollar, + Euro, + Pound, + Yen, + Ruble, + Rupee, + Won, + Yuan, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[serde(transparent)] +pub struct SelectOptionId(String); + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Color { + Default, + Gray, + Brown, + Orange, + Yellow, + Green, + Blue, + Purple, + Pink, + Red, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct SelectOption { + name: String, + id: SelectOptionId, + color: Color, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Select { + /// Sorted list of options available for this property. + options: Vec<SelectOption>, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum PropertyConfiguration { + /// Represents the special Title property required on every database. + /// See https://developers.notion.com/reference/database#title-configuration + Title { + id: PropertyId, + }, + /// Represents a Text property + /// https://developers.notion.com/reference/database#text-configuration + #[serde(rename = "rich_text")] + Text { + id: PropertyId, + }, + /// Represents a Number Property + /// See https://developers.notion.com/reference/database#number-configuration + Number { + id: PropertyId, + /// How the number is displayed in Notion. + format: NumberFormat, + }, + /// Represents a Select Property + /// See https://developers.notion.com/reference/database#select-configuration + Select { + id: PropertyId, + select: Select, + }, + /// Represents a Date Property + /// See https://developers.notion.com/reference/database#date-configuration + Date { + id: PropertyId + }, + /// Represents a File Property + /// See https://developers.notion.com/reference/database#date-configuration + File { + id: PropertyId + }, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum TextColor { + Default, + Gray, + Brown, + Orange, + Yellow, + Green, + Blue, + Purple, + Pink, + Red, + GrayBackground, + BrownBackground, + OrangeBackground, + YellowBackground, + GreenBackground, + BlueBackground, + PurpleBackground, + PinkBackground, + RedBackground, +} + +/// Rich text annotations +/// See https://developers.notion.com/reference/rich-text#annotations +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +struct Annotations { + bold: bool, + code: bool, + color: TextColor, + italic: bool, + strikethrough: bool, + underline: bool, +} + +/// Properties common on all rich text objects +/// See https://developers.notion.com/reference/rich-text#all-rich-text +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +struct RichTextCommon { + plain_text: String, + href: Option<String>, + annotations: Annotations, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Link { + url: String +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Text { + content: String, + link: Option<String>, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum RichText { + /// See https://developers.notion.com/reference/rich-text#text-objects + Text { + #[serde(flatten)] + rich_text: RichTextCommon, + text: Text, + }, + /// See https://developers.notion.com/reference/rich-text#mention-objects + Mention { + #[serde(flatten)] + rich_text: RichTextCommon, + }, + /// See https://developers.notion.com/reference/rich-text#equation-objects + Equation { + #[serde(flatten)] + rich_text: RichTextCommon, + + }, +} diff --git a/src/models/search.rs b/src/models/search.rs new file mode 100644 index 0000000..cc0cdf4 --- /dev/null +++ b/src/models/search.rs @@ -0,0 +1,89 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum SortDirection { + Ascending, + Descending +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum SortTimestamp { + LastEditedTime, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum FilterValue { + Page, + Database +} + + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum FilterProperty { + Object +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct Sort { + direction: SortDirection, + timestamp: SortTimestamp +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct Filter { + value: FilterValue, + property: FilterProperty +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone)] +#[serde(transparent)] +pub struct PagingCursor(String); + +#[derive(Serialize, Debug, Eq, PartialEq, Default)] +pub struct SearchRequest { + #[serde(skip_serializing_if = "Option::is_none")] + query: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + sort: Option<Sort>, + #[serde(skip_serializing_if = "Option::is_none")] + filter: Option<Filter>, + #[serde(skip_serializing_if = "Option::is_none")] + start_cursor: Option<PagingCursor>, + #[serde(skip_serializing_if = "Option::is_none")] + page_size: Option<u8> +} + +#[derive(Debug, Eq, PartialEq)] +pub enum NotionSearch { + Query(String), + Sort{ + direction: SortDirection, + timestamp: SortTimestamp + }, + Filter { + value: FilterValue, + property: FilterProperty + } +} + +impl From<NotionSearch> for SearchRequest { + fn from(search: NotionSearch) -> Self { + match search { + NotionSearch::Query(query) => SearchRequest { query: Some(query), ..Default::default() }, + NotionSearch::Sort { direction, timestamp } => SearchRequest { sort: Some(Sort { + direction, timestamp + }), ..Default::default()}, + NotionSearch::Filter { value, property } => SearchRequest { + filter: Some(Filter { + value, property + }), + ..Default::default() + } + } + } +} |