diff options
author | 2021-05-15 09:23:32 -0700 | |
---|---|---|
committer | 2021-05-15 09:23:32 -0700 | |
commit | d18843ab949e803e25c48f514b4e25244477c731 (patch) | |
tree | 7330e16f6228b990cee0ad58c74fafeaeaaa59c8 | |
parent | d11ac3c9ba709eb4a0691224601088252e49b61d (diff) | |
download | notion-d18843ab949e803e25c48f514b4e25244477c731.tar.gz notion-d18843ab949e803e25c48f514b4e25244477c731.tar.zst notion-d18843ab949e803e25c48f514b4e25244477c731.zip |
pages
-rw-r--r-- | src/lib.rs | 106 | ||||
-rw-r--r-- | src/models.rs | 151 | ||||
-rw-r--r-- | src/models/properties.rs | 181 | ||||
-rw-r--r-- | src/models/tests/page.json | 58 | ||||
-rw-r--r-- | src/models/text.rs | 28 |
5 files changed, 460 insertions, 64 deletions
@@ -1,49 +1,77 @@ use crate::models::search::SearchRequest; -use crate::models::{Database, ListResponse}; +use crate::models::{Database, DatabaseId, ListResponse}; +use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::{header, Client, ClientBuilder, RequestBuilder}; +use serde::de::DeserializeOwned; mod models; +const NOTION_API_VERSION: &'static str = "2021-05-13"; + +// todo: replace with proper snafu error +pub type NotionApiClientError = Box<dyn std::error::Error>; + struct NotionApi { - token: String, + client: Client, } impl NotionApi { - /// This method is apparently deprecated + pub fn new(api_token: String) -> Result<Self, NotionApiClientError> { + let mut headers = HeaderMap::new(); + headers.insert( + "Notion-Version", + HeaderValue::from_static(NOTION_API_VERSION), + ); + + let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", api_token))?; + auth_value.set_sensitive(true); + headers.insert(header::AUTHORIZATION, auth_value); + + let client = ClientBuilder::new().default_headers(headers).build()?; + + Ok(Self { client }) + } + + async fn make_json_request<T>(request: RequestBuilder) -> Result<T, NotionApiClientError> + where + T: DeserializeOwned, + { + let json = request.send().await?.text().await?; + dbg!(serde_json::from_str::<serde_json::Value>(&json)?); + let result = serde_json::from_str(&json)?; + Ok(result) + } + + /// This method is apparently deprecated/"not recommended" 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)?; + let builder = self.client.get("https://api.notion.com/v1/databases"); - Ok(result) + Ok(NotionApi::make_json_request(builder).await?) } 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(NotionApi::make_json_request( + self.client + .post("https://api.notion.com/v1/search") + .json(&query.into()), + ) + .await?) + } - Ok(result) + pub async fn get_database<T: AsRef<DatabaseId>>( + &self, + database_id: T, + ) -> Result<Database, Box<dyn std::error::Error>> { + Ok(NotionApi::make_json_request(self.client.get(format!( + "https://api.notion.com/v1/databases/{}", + database_id.as_ref().id() + ))) + .await?) } } @@ -54,9 +82,7 @@ mod tests { const TEST_TOKEN: &'static str = include_str!(".api_token"); fn test_client() -> NotionApi { - NotionApi { - token: TEST_TOKEN.trim().to_string(), - } + NotionApi::new(TEST_TOKEN.trim().to_string()).unwrap() } #[tokio::test] @@ -82,4 +108,24 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn get_database() -> Result<(), Box<dyn std::error::Error>> { + let api = test_client(); + + let response = api + .search(NotionSearch::Filter { + value: FilterValue::Database, + property: FilterProperty::Object, + }) + .await?; + + let db = response.results()[0].clone(); + + let db_result = api.get_database(&db).await?; + + assert_eq!(db, db_result); + + Ok(()) + } } diff --git a/src/models.rs b/src/models.rs index 291ca4f..4500cc0 100644 --- a/src/models.rs +++ b/src/models.rs @@ -3,13 +3,15 @@ pub mod properties; pub mod search; pub mod text; -use crate::models::properties::PropertyConfiguration; +use crate::models::properties::{PropertyConfiguration, PropertyValue}; use crate::models::text::RichText; -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub use chrono::{DateTime, Utc}; +pub use serde_json::value::Number; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)] #[serde(rename_all = "lowercase")] enum ObjectType { Database, @@ -17,16 +19,26 @@ enum ObjectType { } /// A zero-cost wrapper type around a Database ID -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)] #[serde(transparent)] pub struct DatabaseId(String); +impl DatabaseId { + pub fn id(&self) -> &str { + &self.0 + } +} + +impl AsRef<DatabaseId> for DatabaseId { + fn as_ref(&self) -> &Self { + self + } +} + /// Represents a Notion Database /// See https://developers.notion.com/reference/database -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Database { - /// Always "database" - object: ObjectType, /// Unique identifier for the database. id: DatabaseId, /// Date and time when this database was created. @@ -45,10 +57,131 @@ pub struct Database { properties: HashMap<String, PropertyConfiguration>, } -#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +impl AsRef<DatabaseId> for Database { + fn as_ref(&self) -> &DatabaseId { + &self.id + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] pub struct ListResponse<T> { - object: ObjectType, results: Vec<T>, next_cursor: Option<String>, has_more: bool, } + +impl<T> ListResponse<T> { + pub fn results(&self) -> &[T] { + &self.results + } +} + +/// A zero-cost wrapper type around a Page ID +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)] +#[serde(transparent)] +pub struct PageId(String); + +impl PageId { + pub fn id(&self) -> &str { + &self.0 + } +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum Parent { + #[serde(rename = "database_id")] + Database(#[serde(rename = "database_id")] DatabaseId), + #[serde(rename = "page_id")] + Page(#[serde(rename = "page_id")] PageId), + Workspace, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct Properties { + #[serde(flatten)] + properties: HashMap<String, PropertyValue>, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct Page { + id: PageId, + /// Date and time when this page was created. + created_time: DateTime<Utc>, + /// Date and time when this page was updated. + last_edited_time: DateTime<Utc>, + /// The archived status of the page. + archived: bool, + properties: Properties, + parent: Parent, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct Block {} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(tag = "object")] +pub enum Object { + Database { + #[serde(flatten)] + database: Database, + }, + Page {}, + List { + list: ListResponse<Object>, + }, +} + +/// A zero-cost wrapper type around a Page ID +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)] +#[serde(transparent)] +pub struct UserId(String); + +impl UserId { + pub fn id(&self) -> &str { + &self.0 + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct UserCommon { + id: UserId, + name: Option<String>, + avatar_url: Option<String>, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct Person { + email: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct Bot { + email: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(tag = "type")] +pub enum User { + Person { + #[serde(flatten)] + common: UserCommon, + person: Person, + }, + Bot { + #[serde(flatten)] + common: UserCommon, + bot: Bot, + }, +} + +#[cfg(test)] +mod tests { + use crate::models::Page; + + #[test] + fn deserialize_page() { + let _page: Page = serde_json::from_str(include_str!("models/tests/page.json")).unwrap(); + } +} diff --git a/src/models/properties.rs b/src/models/properties.rs index 4139712..c55ce1a 100644 --- a/src/models/properties.rs +++ b/src/models/properties.rs @@ -1,12 +1,15 @@ -use crate::models::DatabaseId; +use crate::models::text::RichText; +use crate::models::{DatabaseId, PageId, User}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +use super::{DateTime, Number, Utc}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone)] #[serde(transparent)] pub struct PropertyId(String); /// How the number is displayed in Notion. -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Copy, Clone)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum NumberFormat { @@ -23,11 +26,11 @@ pub enum NumberFormat { Yuan, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone)] #[serde(transparent)] pub struct SelectOptionId(String); -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Copy, Clone)] #[serde(rename_all = "lowercase")] pub enum Color { Default, @@ -42,26 +45,26 @@ pub enum Color { Red, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct SelectOption { name: String, id: SelectOptionId, color: Color, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Select { /// Sorted list of options available for this property. options: Vec<SelectOption>, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Formula { /// Formula to evaluate for this property expression: String, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Relation { /// The database this relation refers to. /// New linked pages must belong to this database in order to be valid. @@ -77,7 +80,7 @@ pub struct Relation { synced_property_id: Option<PropertyId>, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Copy, Clone)] #[serde(rename_all = "snake_case")] pub enum RollupFunction { CountAll, @@ -95,7 +98,7 @@ pub enum RollupFunction { Range, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Rollup { /// The name of the relation property this property is responsible for rolling up. relation_property_name: String, @@ -111,7 +114,7 @@ pub struct Rollup { function: RollupFunction, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum PropertyConfiguration { @@ -132,12 +135,22 @@ pub enum PropertyConfiguration { /// Represents a Select Property /// See https://developers.notion.com/reference/database#select-configuration Select { id: PropertyId, select: Select }, + /// Represents a Multi-select Property + /// See https://developers.notion.com/reference/database#multi-select-configuration + MultiSelect { + id: PropertyId, + multi_select: Select, + }, /// Represents a Date Property /// See https://developers.notion.com/reference/database#date-configuration Date { id: PropertyId }, + /// Represents a People Property + /// See https://developers.notion.com/reference/database#people-configuration + People { id: PropertyId }, /// Represents a File Property /// See https://developers.notion.com/reference/database#file-configuration - /// Documentation issue: docs claim type name is `file` but it's is in fact `files` + // Todo: File a bug with notion + // Documentation issue: docs claim type name is `file` but it is in fact `files` Files { id: PropertyId }, /// Represents a Checkbox Property /// See https://developers.notion.com/reference/database#checkbox-configuration @@ -166,3 +179,145 @@ pub enum PropertyConfiguration { /// See https://developers.notion.com/reference/database#last-edited-by-configuration LastEditBy { id: PropertyId }, } + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct SelectedValue { + id: SelectOptionId, + name: String, + color: Color, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct DateValue { + // Todo: Will this work with dates (without time)? + // does there need to be an enum of Date|DateTime? + start: DateTime<Utc>, + end: Option<DateTime<Utc>>, +} + +/// Formula property value objects represent the result of evaluating a formula +/// described in the database's properties. +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum FormulaResultValue { + String(#[serde(rename = "string")] Option<String>), + Number(#[serde(rename = "number")] Option<Number>), + Boolean(#[serde(rename = "boolean")] Option<bool>), + Date(#[serde(rename = "date")] Option<DateTime<Utc>>), +} + +/// Relation property value objects contain an array of page references within the relation property. +/// A page reference is an object with an id property, +/// with a string value (UUIDv4) corresponding to a page ID in another database. +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct RelationValue { + id: PageId, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum RollupValue { + Number(#[serde(rename = "number")] Option<Number>), + Date(#[serde(rename = "date")] Option<DateTime<Utc>>), + // Todo: these property values don't have id properties... + // so this likely wont deserialize. would like to minimize duplicated code... + Array(#[serde(rename = "array")] Vec<PropertyValue>), +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct FileReference { + name: String, + url: String, + mime_type: String, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum PropertyValue { + // https://developers.notion.com/reference/page#title-property-values + Title { + id: PropertyId, + title: Vec<RichText>, + }, + /// https://developers.notion.com/reference/page#rich-text-property-values + #[serde(rename = "rich_text")] + Text { + id: PropertyId, + rich_text: Vec<RichText>, + }, + /// https://developers.notion.com/reference/page#number-property-values + Number { + id: PropertyId, + number: Number, + }, + /// https://developers.notion.com/reference/page#select-property-values + Select { + id: PropertyId, + select: SelectedValue, + }, + MultiSelect { + id: PropertyId, + multi_select: Vec<SelectedValue>, + }, + Date { + id: PropertyId, + date: DateValue, + }, + /// https://developers.notion.com/reference/page#formula-property-values + Formula { + id: PropertyId, + formula: FormulaResultValue, + }, + /// https://developers.notion.com/reference/page#relation-property-values + Relation { + id: PropertyId, + relation: RelationValue, + }, + Rollup { + id: PropertyId, + relation: Rollup, + }, + People { + id: PropertyId, + people: Vec<User>, + }, + Files { + id: PropertyId, + files: Vec<FileReference>, + }, + Checkbox { + id: PropertyId, + checkbox: bool, + }, + URL { + id: PropertyId, + url: String, + }, + Email { + id: PropertyId, + email: String, + }, + PhoneNumber { + id: PropertyId, + phone_number: String, + }, + CreatedTime { + id: PropertyId, + created_time: DateTime<Utc>, + }, + CreatedBy { + id: PropertyId, + created_by: User, + }, + LastEditedTime { + id: PropertyId, + last_edited_time: DateTime<Utc>, + }, + LastEditedBy { + id: PropertyId, + last_edited_by: User, + }, +} diff --git a/src/models/tests/page.json b/src/models/tests/page.json new file mode 100644 index 0000000..572bb75 --- /dev/null +++ b/src/models/tests/page.json @@ -0,0 +1,58 @@ +{ + "object": "page", + "id": "b55c9c91-384d-452b-81db-d1ef79372b75", + "created_time": "2020-03-17T19:10:04.968Z", + "last_edited_time": "2020-03-17T21:49:37.913Z", + "archived": false, + "parent": { + "type": "workspace" + }, + "properties": { + "Name": { + "type": "title", + "id": "some-property-id", + "title": [ + { + "type": "text", + "plain_text": "Stuff", + "text": { + "content": "Stuff" + } + }, + { + "type": "text", + "plain_text": "some", + "text": { + "content": "some" + }, + "annotations": { + "italic": true + } + } + ] + }, + "Description": { + "type": "rich_text", + "id": "some-property-id2", + "rich_text": [ + { + "type": "text", + "plain_text": "Stuff", + "text": { + "content": "Stuff" + } + }, + { + "type": "text", + "plain_text": "some", + "text": { + "content": "some" + }, + "annotations": { + "italic": true + } + } + ] + } + } +} diff --git a/src/models/text.rs b/src/models/text.rs index 0baf183..a10007d 100644 --- a/src/models/text.rs +++ b/src/models/text.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Copy, Clone)] #[serde(rename_all = "snake_case")] pub enum TextColor { Default, @@ -26,23 +26,23 @@ pub enum TextColor { /// Rich text annotations /// See https://developers.notion.com/reference/rich-text#annotations -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] struct Annotations { - bold: bool, - code: bool, - color: TextColor, - italic: bool, - strikethrough: bool, - underline: bool, + bold: Option<bool>, + code: Option<bool>, + color: Option<TextColor>, + italic: Option<bool>, + strikethrough: Option<bool>, + underline: Option<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)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct RichTextCommon { plain_text: String, href: Option<String>, - annotations: Annotations, + annotations: Option<Annotations>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -50,13 +50,17 @@ pub struct Link { url: String, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Text { content: String, link: Option<String>, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +/// Rich text objects contain data for displaying formatted text, mentions, and equations. +/// A rich text object also contains annotations for style information. +/// Arrays of rich text objects are used within property objects and property +/// value objects to create what a user sees as a single text value in Notion. +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum RichText { |