aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jake Swenson <jake@jakeswenson.com> 2021-05-15 09:23:32 -0700
committerGravatar Jake Swenson <jake@jakeswenson.com> 2021-05-15 09:23:32 -0700
commitd18843ab949e803e25c48f514b4e25244477c731 (patch)
tree7330e16f6228b990cee0ad58c74fafeaeaaa59c8
parentd11ac3c9ba709eb4a0691224601088252e49b61d (diff)
downloadnotion-d18843ab949e803e25c48f514b4e25244477c731.tar.gz
notion-d18843ab949e803e25c48f514b4e25244477c731.tar.zst
notion-d18843ab949e803e25c48f514b4e25244477c731.zip
pages
-rw-r--r--src/lib.rs106
-rw-r--r--src/models.rs151
-rw-r--r--src/models/properties.rs181
-rw-r--r--src/models/tests/page.json58
-rw-r--r--src/models/text.rs28
5 files changed, 460 insertions, 64 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 7fbaa15..19d79ee 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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 {