diff options
author | 2021-08-29 12:59:04 -0700 | |
---|---|---|
committer | 2021-08-29 12:59:04 -0700 | |
commit | 1c22088640e5deeadd61017bd90920b652685b5f (patch) | |
tree | 59d110a7c8792c39e0c1ea180fa972eee1c4662d | |
parent | 8d5779a661f4537ef7d87a44bdfbf240eb054ff1 (diff) | |
download | notion-1c22088640e5deeadd61017bd90920b652685b5f.tar.gz notion-1c22088640e5deeadd61017bd90920b652685b5f.tar.zst notion-1c22088640e5deeadd61017bd90920b652685b5f.zip |
Notion API Version 2021-08-16 support; fix a bunch of modling errors as well
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | examples/todo/commands/configure.rs | 9 | ||||
-rw-r--r-- | examples/todo/main.rs | 6 | ||||
-rw-r--r-- | src/ids.rs | 52 | ||||
-rw-r--r-- | src/lib.rs | 169 | ||||
-rw-r--r-- | src/models.rs | 168 | ||||
-rw-r--r-- | src/models/error.rs | 70 | ||||
-rw-r--r-- | src/models/properties.rs | 42 | ||||
-rw-r--r-- | src/models/properties/formulas.rs | 27 | ||||
-rw-r--r-- | src/models/properties/tests.rs | 28 | ||||
-rw-r--r-- | src/models/properties/tests/date_property.json | 8 | ||||
-rw-r--r-- | src/models/properties/tests/formula_date_value.json | 11 | ||||
-rw-r--r-- | src/models/properties/tests/formula_number_value.json | 8 | ||||
-rw-r--r-- | src/models/properties/tests/null_select_property.json | 5 | ||||
-rw-r--r-- | src/models/properties/tests/select_property.json | 9 | ||||
-rw-r--r-- | src/models/search.rs | 5 | ||||
-rw-r--r-- | src/models/tests/error.json | 6 | ||||
-rw-r--r-- | src/models/tests/search_results.json | 209 | ||||
-rw-r--r-- | src/models/tests/unknown_error.json | 6 | ||||
-rw-r--r-- | src/models/users.rs | 34 |
20 files changed, 688 insertions, 191 deletions
@@ -10,11 +10,12 @@ description = "A Notion Api Client" license = "MIT" [dependencies] -serde_json = "1.0.64" -snafu = "0.6.10" +serde_json = "1.0" +thiserror = "1.0" +tracing = "0.1.26" [dependencies.chrono] -version = "0.4.19" +version = "0.4" features = ["serde"] [dependencies.reqwest] diff --git a/examples/todo/commands/configure.rs b/examples/todo/commands/configure.rs index 42ffafd..18ace89 100644 --- a/examples/todo/commands/configure.rs +++ b/examples/todo/commands/configure.rs @@ -1,8 +1,9 @@ use crate::TodoConfig; use anyhow::Result; +use notion::ids::{AsIdentifier, DatabaseId}; use notion::models::search::NotionSearch; -use notion::models::{Database, DatabaseId}; -use notion::{AsIdentifier, NotionApi}; +use notion::models::Database; +use notion::NotionApi; use skim::{Skim, SkimItem, SkimItemReceiver, SkimItemSender, SkimOptions}; use std::borrow::Cow; use std::ops::Deref; @@ -43,9 +44,9 @@ fn skim_select_database(databases: Vec<Database>) -> Result<DatabaseId> { .downcast_ref() .expect("Couldn't cast back to SkimDB"); - let database_id = db.db.id(); + let database_id = db.db.as_id(); - Ok(database_id) + Ok(database_id.clone()) } pub async fn configure(notion_api: NotionApi) -> Result<()> { diff --git a/examples/todo/main.rs b/examples/todo/main.rs index 2283549..3a01af3 100644 --- a/examples/todo/main.rs +++ b/examples/todo/main.rs @@ -2,13 +2,13 @@ mod commands; use anyhow::{Context, Result}; use clap::Clap; -use notion::models::DatabaseId; +use notion::ids::DatabaseId; use notion::NotionApi; use serde::{Deserialize, Serialize}; -// https://docs.rs/clap/3.0.0-beta.2/clap/ +// From <https://docs.rs/clap/3.0.0-beta.2/clap/> #[derive(Clap)] -#[clap(version = "1.0", author = "Kevin K. <kbknapp@gmail.com>")] +#[clap(version = "1.0", author = "Jake Swenson")] struct Opts { #[clap(subcommand)] command: SubCommand, diff --git a/src/ids.rs b/src/ids.rs new file mode 100644 index 0000000..6febe63 --- /dev/null +++ b/src/ids.rs @@ -0,0 +1,52 @@ +use std::fmt::Display; + +pub trait Identifier: Display { + fn value(&self) -> &str; +} + +/// Meant to be a helpful trait allowing anything that can be +/// identified by the type specified in `ById`. +pub trait AsIdentifier<ById: Identifier> { + fn as_id(&self) -> &ById; +} + +impl<T> AsIdentifier<T> for T +where + T: Identifier, +{ + fn as_id(&self) -> &T { + &self + } +} + +macro_rules! identifer { + ($name:ident) => { + #[derive(serde::Serialize, serde::Deserialize, Debug, Eq, PartialEq, Hash, Clone)] + #[serde(transparent)] + pub struct $name(String); + + impl Identifier for $name { + fn value(&self) -> &str { + &self.0 + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + }; +} + +identifer!(DatabaseId); +identifer!(PageId); +identifer!(BlockId); +identifer!(UserId); +identifer!(PropertyId); + +impl From<PageId> for BlockId { + fn from(page_id: PageId) -> Self { + BlockId(page_id.0) + } +} @@ -1,37 +1,46 @@ +use crate::ids::{BlockId, DatabaseId}; use crate::models::search::{DatabaseQuery, SearchRequest}; -use crate::models::{Block, BlockId, Database, DatabaseId, ListResponse, Object, Page}; +use crate::models::{Block, Database, ListResponse, Object, Page}; +use ids::AsIdentifier; use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::{header, Client, ClientBuilder, RequestBuilder}; -use serde::de::DeserializeOwned; -use snafu::{ResultExt, Snafu}; +pub mod ids; pub mod models; -const NOTION_API_VERSION: &str = "2021-05-13"; +use crate::models::error::ErrorResponse; +pub use chrono; + +const NOTION_API_VERSION: &str = "2021-08-16"; /// An wrapper Error type for all errors produced by the [`NotionApi`](NotionApi) client. -#[derive(Debug, Snafu)] +#[derive(Debug, thiserror::Error)] pub enum Error { - #[snafu(display("Invalid Notion API Token: {}", source))] + #[error("Invalid Notion API Token: {}", source)] InvalidApiToken { source: reqwest::header::InvalidHeaderValue, }, - #[snafu(display("Unable to build reqwest HTTP client: {}", source))] + + #[error("Unable to build reqwest HTTP client: {}", source)] ErrorBuildingClient { source: reqwest::Error }, - #[snafu(display("Error sending HTTP request: {}", source))] - RequestFailed { source: reqwest::Error }, - #[snafu(display("Error reading response: {}", source))] - ResponseError { source: reqwest::Error }, + #[error("Error sending HTTP request: {}", source)] + RequestFailed { + #[from] + source: reqwest::Error, + }, + + #[error("Error reading response: {}", source)] + ResponseIoError { source: reqwest::Error }, - #[snafu(display("Error parsing json response: {}", source))] + #[error("Error parsing json response: {}", source)] JsonParseError { source: serde_json::Error }, -} -/// Meant to be a helpful trait allowing anything that can be -/// identified by the type specified in `ById`. -pub trait AsIdentifier<ById> { - fn id(&self) -> ById; + #[error("Unexpected API Response")] + UnexpectedResponse { response: Object }, + + #[error("API Error {}({}): {}", .error.code, .error.status, .error.message)] + ApiError { error: ErrorResponse }, } /// An API client for Notion. @@ -51,37 +60,48 @@ impl NotionApi { HeaderValue::from_static(NOTION_API_VERSION), ); - let mut auth_value = - HeaderValue::from_str(&format!("Bearer {}", api_token)).context(InvalidApiToken)?; + let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", api_token)) + .map_err(|source| Error::InvalidApiToken { source })?; auth_value.set_sensitive(true); headers.insert(header::AUTHORIZATION, auth_value); let client = ClientBuilder::new() .default_headers(headers) .build() - .context(ErrorBuildingClient)?; + .map_err(|source| Error::ErrorBuildingClient { source })?; Ok(Self { client }) } - async fn make_json_request<T>(request: RequestBuilder) -> Result<T, Error> - where - T: DeserializeOwned, - { - let json = request - .send() + async fn make_json_request(&self, request: RequestBuilder) -> Result<Object, Error> { + let request = request.build()?; + let url = request.url(); + tracing::trace!( + method = request.method().as_str(), + url = url.as_str(), + "Sending request" + ); + let json = self + .client + .execute(request) .await - .context(RequestFailed)? + .map_err(|source| Error::RequestFailed { source })? .text() .await - .context(ResponseError)?; + .map_err(|source| Error::ResponseIoError { source })?; + + tracing::debug!("JSON Response: {}", json); #[cfg(test)] { - println!("JSON: {}", json); - dbg!(serde_json::from_str::<serde_json::Value>(&json).context(JsonParseError)?); + dbg!(serde_json::from_str::<serde_json::Value>(&json) + .map_err(|source| Error::JsonParseError { source })?); + } + let result = + serde_json::from_str(&json).map_err(|source| Error::JsonParseError { source })?; + match result { + Object::Error { error } => Err(Error::ApiError { error }), + response => Ok(response), } - let result = serde_json::from_str(&json).context(JsonParseError)?; - Ok(result) } /// List all the databases shared with the supplied integration token. @@ -90,7 +110,10 @@ impl NotionApi { pub async fn list_databases(&self) -> Result<ListResponse<Database>, Error> { let builder = self.client.get("https://api.notion.com/v1/databases"); - Ok(NotionApi::make_json_request(builder).await?) + match self.make_json_request(builder).await? { + Object::List { list } => Ok(list.expect_databases()?), + response => Err(Error::UnexpectedResponse { response }), + } } /// Search all pages in notion. @@ -100,12 +123,18 @@ impl NotionApi { &self, query: T, ) -> Result<ListResponse<Object>, Error> { - Ok(NotionApi::make_json_request( - self.client - .post("https://api.notion.com/v1/search") - .json(&query.into()), - ) - .await?) + let result = self + .make_json_request( + self.client + .post("https://api.notion.com/v1/search") + .json(&query.into()), + ) + .await?; + + match result { + Object::List { list } => Ok(list), + response => Err(Error::UnexpectedResponse { response }), + } } /// Get a database by [DatabaseId]. @@ -113,11 +142,17 @@ impl NotionApi { &self, database_id: T, ) -> Result<Database, Error> { - Ok(NotionApi::make_json_request(self.client.get(format!( - "https://api.notion.com/v1/databases/{}", - database_id.id().id() - ))) - .await?) + let result = self + .make_json_request(self.client.get(format!( + "https://api.notion.com/v1/databases/{}", + database_id.as_id() + ))) + .await?; + + match result { + Object::Database { database } => Ok(database), + response => Err(Error::UnexpectedResponse { response }), + } } /// Query a database and return the matching pages. @@ -130,31 +165,43 @@ impl NotionApi { T: Into<DatabaseQuery>, D: AsIdentifier<DatabaseId>, { - Ok(NotionApi::make_json_request( - self.client - .post(&format!( - "https://api.notion.com/v1/databases/{database_id}/query", - database_id = database.id() - )) - .json(&query.into()), - ) - .await?) + let result = self + .make_json_request( + self.client + .post(&format!( + "https://api.notion.com/v1/databases/{database_id}/query", + database_id = database.as_id() + )) + .json(&query.into()), + ) + .await?; + match result { + Object::List { list } => Ok(list.expect_pages()?), + response => Err(Error::UnexpectedResponse { response }), + } } pub async fn get_block_children<T: AsIdentifier<BlockId>>( &self, block_id: T, ) -> Result<ListResponse<Block>, Error> { - Ok(NotionApi::make_json_request(self.client.get(&format!( - "https://api.notion.com/v1/blocks/{block_id}/children", - block_id = block_id.id() - ))) - .await?) + let result = self + .make_json_request(self.client.get(&format!( + "https://api.notion.com/v1/blocks/{block_id}/children", + block_id = block_id.as_id() + ))) + .await?; + + match result { + Object::List { list } => Ok(list.expect_blocks()?), + response => Err(Error::UnexpectedResponse { response }), + } } } #[cfg(test)] mod tests { + use crate::ids::BlockId; use crate::models::search::PropertyCondition::Text; use crate::models::search::{ DatabaseQuery, FilterCondition, FilterProperty, FilterValue, NotionSearch, TextCondition, @@ -176,6 +223,7 @@ mod tests { } fn test_client() -> NotionApi { + std::env::set_var("RUST_LOG", "notion"); NotionApi::new(test_token()).unwrap() } @@ -265,7 +313,10 @@ mod tests { for object in search_response.results { match object { - Object::Page { page } => api.get_block_children(page).await.unwrap(), + Object::Page { page } => api + .get_block_children(BlockId::from(page.id)) + .await + .unwrap(), _ => panic!("Should not have received anything but pages!"), }; } diff --git a/src/models.rs b/src/models.rs index fc31d17..aead105 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,18 +1,22 @@ +pub mod error; pub mod paging; pub mod properties; pub mod search; pub mod text; +pub mod users; use crate::models::properties::{PropertyConfiguration, PropertyValue}; use crate::models::text::RichText; +use crate::Error; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use crate::ids::{AsIdentifier, BlockId, DatabaseId, PageId}; +use crate::models::error::ErrorResponse; use crate::models::paging::PagingCursor; -use crate::AsIdentifier; +use crate::models::users::User; pub use chrono::{DateTime, Utc}; pub use serde_json::value::Number; -use std::fmt::{Display, Formatter}; #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)] #[serde(rename_all = "snake_case")] @@ -21,23 +25,6 @@ enum ObjectType { List, } -/// A zero-cost wrapper type around a Database ID -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)] -#[serde(transparent)] -pub struct DatabaseId(String); - -impl DatabaseId { - pub fn id(&self) -> &str { - &self.0 - } -} - -impl Display for DatabaseId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - /// Represents a Notion Database /// See <https://developers.notion.com/reference/database> #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] @@ -61,8 +48,8 @@ pub struct Database { } impl AsIdentifier<DatabaseId> for Database { - fn id(&self) -> DatabaseId { - self.id.clone() + fn as_id(&self) -> &DatabaseId { + &self.id } } @@ -106,16 +93,56 @@ impl ListResponse<Object> { next_cursor: self.next_cursor, } } -} -/// A zero-cost wrapper type around a Page ID -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)] -#[serde(transparent)] -pub struct PageId(String); + pub(crate) fn expect_databases(self) -> Result<ListResponse<Database>, crate::Error> { + let databases: Result<Vec<_>, _> = self + .results + .into_iter() + .map(|object| match object { + Object::Database { database } => Ok(database), + response => Err(Error::UnexpectedResponse { response }), + }) + .collect(); + + Ok(ListResponse { + results: databases?, + has_more: self.has_more, + next_cursor: self.next_cursor, + }) + } + + pub(crate) fn expect_pages(self) -> Result<ListResponse<Page>, crate::Error> { + let items: Result<Vec<_>, _> = self + .results + .into_iter() + .map(|object| match object { + Object::Page { page } => Ok(page), + response => Err(Error::UnexpectedResponse { response }), + }) + .collect(); + + Ok(ListResponse { + results: items?, + has_more: self.has_more, + next_cursor: self.next_cursor, + }) + } + + pub(crate) fn expect_blocks(self) -> Result<ListResponse<Block>, crate::Error> { + let items: Result<Vec<_>, _> = self + .results + .into_iter() + .map(|object| match object { + Object::Block { block } => Ok(block), + response => Err(Error::UnexpectedResponse { response }), + }) + .collect(); -impl PageId { - pub fn id(&self) -> &str { - &self.0 + Ok(ListResponse { + results: items?, + has_more: self.has_more, + next_cursor: self.next_cursor, + }) } } @@ -241,7 +268,7 @@ pub enum Block { } impl AsIdentifier<BlockId> for Block { - fn id(&self) -> BlockId { + fn as_id(&self) -> &BlockId { use Block::*; match self { Paragraph { common, .. } @@ -252,7 +279,7 @@ impl AsIdentifier<BlockId> for Block { | NumberedListItem { common, .. } | ToDo { common, .. } | Toggle { common, .. } - | ChildPage { common, .. } => common.id.clone(), + | ChildPage { common, .. } => &common.id, Unsupported {} => { panic!("Trying to reference identifier for unsupported block!") } @@ -261,14 +288,8 @@ impl AsIdentifier<BlockId> for Block { } impl AsIdentifier<PageId> for Page { - fn id(&self) -> PageId { - self.id.clone() - } -} - -impl AsIdentifier<BlockId> for Page { - fn id(&self) -> BlockId { - self.id.clone().into() + fn as_id(&self) -> &PageId { + &self.id } } @@ -296,28 +317,10 @@ pub enum Object { #[serde(flatten)] user: User, }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)] -#[serde(transparent)] -pub struct BlockId(String); - -impl BlockId { - pub fn id(&self) -> &str { - &self.0 - } -} - -impl From<PageId> for BlockId { - fn from(page_id: PageId) -> Self { - BlockId(page_id.0) - } -} - -impl Display for BlockId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } + Error { + #[serde(flatten)] + error: ErrorResponse, + }, } impl Object { @@ -326,49 +329,6 @@ impl 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 { - pub id: UserId, - pub name: Option<String>, - pub avatar_url: Option<String>, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -pub struct Person { - pub email: String, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -pub struct Bot { - pub 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::{ListResponse, Object, Page}; diff --git a/src/models/error.rs b/src/models/error.rs new file mode 100644 index 0000000..e382719 --- /dev/null +++ b/src/models/error.rs @@ -0,0 +1,70 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +#[derive(Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Hash)] +#[serde(transparent)] +pub struct StatusCode(u16); + +impl StatusCode { + pub fn code(&self) -> u16 { + self.0 + } +} + +impl Display for StatusCode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +/// <https://developers.notion.com/reference/errors> +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] +pub struct ErrorResponse { + pub status: StatusCode, + pub code: ErrorCode, + pub message: String, +} + +/// <https://developers.notion.com/reference/errors> +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum ErrorCode { + InvalidJson, + InvalidRequestUrl, + InvalidRequest, + ValidationError, + MissionVersion, + Unauthorized, + RestrictedResource, + ObjectNotFound, + ConflictError, + RateLimited, + InternalServerError, + ServiceUnavailable, + #[serde(other)] // serde issue #912 + Unknown, +} + +impl Display for ErrorCode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[cfg(test)] +mod tests { + use crate::models::error::{ErrorCode, ErrorResponse}; + + #[test] + fn deserialize_error() { + let error: ErrorResponse = serde_json::from_str(include_str!("tests/error.json")).unwrap(); + assert_eq!(error.code, ErrorCode::ValidationError) + } + + #[test] + fn deserialize_unknown_error() { + let error: ErrorResponse = + serde_json::from_str(include_str!("tests/unknown_error.json")).unwrap(); + assert_eq!(error.code, ErrorCode::Unknown) + } +} diff --git a/src/models/properties.rs b/src/models/properties.rs index b3948b4..95ddef3 100644 --- a/src/models/properties.rs +++ b/src/models/properties.rs @@ -1,12 +1,15 @@ use crate::models::text::RichText; -use crate::models::{DatabaseId, PageId, User}; -use serde::{Deserialize, Serialize}; +use crate::models::users::User; use super::{DateTime, Number, Utc}; +use crate::ids::{DatabaseId, PageId, PropertyId}; +use chrono::NaiveDate; +use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone)] -#[serde(transparent)] -pub struct PropertyId(String); +pub mod formulas; + +#[cfg(test)] +mod tests; /// How the number is displayed in Notion. #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Copy, Clone, Hash)] @@ -179,7 +182,7 @@ pub enum PropertyConfiguration { /// See <https://developers.notion.com/reference/database#created-by-configuration> CreatedBy { id: PropertyId }, /// See <https://developers.notion.com/reference/database#last-edited-time-configuration> - LastEditTime { id: PropertyId }, + LastEditedTime { id: PropertyId }, /// See <https://developers.notion.com/reference/database#last-edited-by-configuration> LastEditBy { id: PropertyId }, } @@ -192,11 +195,16 @@ pub struct SelectedValue { } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[serde(untagged)] +pub enum DateOrDateTime { + Date(NaiveDate), + DateTime(DateTime<Utc>), +} + +#[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? - pub start: DateTime<Utc>, - pub end: Option<DateTime<Utc>>, + pub start: DateOrDateTime, + pub end: Option<DateOrDateTime>, } /// Formula property value objects represent the result of evaluating a formula @@ -205,10 +213,10 @@ pub struct DateValue { #[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>>), + String { string: Option<String> }, + Number { number: Option<Number> }, + Boolean { boolean: Option<bool> }, + Date { date: Option<DateValue> }, } /// Relation property value objects contain an array of page references within the relation property. @@ -260,11 +268,11 @@ pub enum PropertyValue { /// <https://developers.notion.com/reference/page#select-property-values> Select { id: PropertyId, - select: SelectedValue, + select: Option<SelectedValue>, }, MultiSelect { id: PropertyId, - multi_select: Vec<SelectedValue>, + multi_select: Option<Vec<SelectedValue>>, }, Date { id: PropertyId, @@ -290,7 +298,7 @@ pub enum PropertyValue { }, Files { id: PropertyId, - files: Vec<FileReference>, + files: Option<Vec<FileReference>>, }, Checkbox { id: PropertyId, diff --git a/src/models/properties/formulas.rs b/src/models/properties/formulas.rs new file mode 100644 index 0000000..20bc7f4 --- /dev/null +++ b/src/models/properties/formulas.rs @@ -0,0 +1,27 @@ +#[cfg(test)] +mod tests { + use crate::models::properties::{FormulaResultValue, PropertyValue}; + + #[test] + fn parse_number_formula_prop() { + let _property: PropertyValue = + serde_json::from_str(include_str!("tests/formula_number_value.json")).unwrap(); + } + + #[test] + fn parse_date_formula_prop() { + let _property: PropertyValue = + serde_json::from_str(include_str!("tests/formula_date_value.json")).unwrap(); + } + + #[test] + fn parse_number_formula() { + let _value: FormulaResultValue = serde_json::from_str( + r#"{ + "type": "number", + "number": 0 + }"#, + ) + .unwrap(); + } +} diff --git a/src/models/properties/tests.rs b/src/models/properties/tests.rs new file mode 100644 index 0000000..23b376d --- /dev/null +++ b/src/models/properties/tests.rs @@ -0,0 +1,28 @@ +use super::{DateOrDateTime, PropertyValue}; +use chrono::NaiveDate; + +#[test] +fn verify_date_parsing() { + let date = NaiveDate::from_ymd(2021, 01, 02); + let result = serde_json::to_string(&DateOrDateTime::Date(date)).unwrap(); + let parsed: DateOrDateTime = serde_json::from_str(&result).unwrap(); + println!("{:?}", parsed); +} + +#[test] +fn parse_date_property() { + let _property: PropertyValue = + serde_json::from_str(include_str!("tests/date_property.json")).unwrap(); +} + +#[test] +fn parse_null_select_property() { + let _property: PropertyValue = + serde_json::from_str(include_str!("tests/null_select_property.json")).unwrap(); +} + +#[test] +fn parse_select_property() { + let _property: PropertyValue = + serde_json::from_str(include_str!("tests/select_property.json")).unwrap(); +} diff --git a/src/models/properties/tests/date_property.json b/src/models/properties/tests/date_property.json new file mode 100644 index 0000000..fe2b04f --- /dev/null +++ b/src/models/properties/tests/date_property.json @@ -0,0 +1,8 @@ +{ + "id": "VXfM", + "type": "date", + "date": { + "start": "2021-09-30", + "end": null + } +} diff --git a/src/models/properties/tests/formula_date_value.json b/src/models/properties/tests/formula_date_value.json new file mode 100644 index 0000000..84728c5 --- /dev/null +++ b/src/models/properties/tests/formula_date_value.json @@ -0,0 +1,11 @@ +{ + "id": "7*%269", + "type": "formula", + "formula": { + "type": "date", + "date": { + "start": "2021-09-30", + "end": null + } + } +} diff --git a/src/models/properties/tests/formula_number_value.json b/src/models/properties/tests/formula_number_value.json new file mode 100644 index 0000000..2a831e3 --- /dev/null +++ b/src/models/properties/tests/formula_number_value.json @@ -0,0 +1,8 @@ +{ + "id": "abc", + "type": "formula", + "formula": { + "type": "number", + "number": 0 + } +} diff --git a/src/models/properties/tests/null_select_property.json b/src/models/properties/tests/null_select_property.json new file mode 100644 index 0000000..e0ed24e --- /dev/null +++ b/src/models/properties/tests/null_select_property.json @@ -0,0 +1,5 @@ +{ + "id": "uX", + "type": "select", + "select": null +} diff --git a/src/models/properties/tests/select_property.json b/src/models/properties/tests/select_property.json new file mode 100644 index 0000000..db65bdf --- /dev/null +++ b/src/models/properties/tests/select_property.json @@ -0,0 +1,9 @@ +{ + "id": "uX", + "type": "select", + "select": { + "id": "9c", + "name": "Reserved", + "color": "green" + } +} diff --git a/src/models/search.rs b/src/models/search.rs index aa2f62e..3e5d782 100644 --- a/src/models/search.rs +++ b/src/models/search.rs @@ -1,5 +1,6 @@ +use crate::ids::{PageId, UserId}; use crate::models::paging::Paging; -use crate::models::{Number, PageId, UserId}; +use crate::models::Number; use chrono::{DateTime, Utc}; use serde::ser::SerializeMap; use serde::{Serialize, Serializer}; @@ -281,8 +282,10 @@ pub struct DatabaseSort { // Todo: Should property and timestamp be mutually exclusive? (i.e a flattened enum?) // the documentation is not clear: // https://developers.notion.com/reference/post-database-query#post-database-query-sort + #[serde(skip_serializing_if = "Option::is_none")] pub property: Option<String>, /// The name of the timestamp to sort against. + #[serde(skip_serializing_if = "Option::is_none")] pub timestamp: Option<DatabaseSortTimestamp>, pub direction: SortDirection, } diff --git a/src/models/tests/error.json b/src/models/tests/error.json new file mode 100644 index 0000000..0194824 --- /dev/null +++ b/src/models/tests/error.json @@ -0,0 +1,6 @@ +{ + "object": "error", + "status": 400, + "code": "validation_error", + "message": "Could not find property with name or id: LastEditedTime" +} diff --git a/src/models/tests/search_results.json b/src/models/tests/search_results.json new file mode 100644 index 0000000..ddeb27f --- /dev/null +++ b/src/models/tests/search_results.json @@ -0,0 +1,209 @@ +{ + "object": "list", + "results": [ + { + "object": "database", + "id": "58", + "cover": null, + "icon": { + "type": "emoji", + "emoji": "✈️" + }, + "created_time": "2019-05-18T20:28:00.000Z", + "last_edited_time": "2021-08-29T16:13:00.000Z", + "title": [ + { + "type": "text", + "text": { + "content": "Plans", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "Plans", + "href": null + } + ], + "properties": { + "Places": { + "id": "6B", + "name": "Places", + "type": "rich_text", + "rich_text": {} + }, + "End Date": { + "id": "7*", + "name": "End Date", + "type": "formula", + "formula": { + "expression": "dateSubtract(end(prop(\"Date\")), hour(end(prop(\"Date\"))) * 60 + minute(end(prop(\"Date\"))), \"minutes\")" + } + }, + "Type": { + "id": "%3D", + "name": "Type", + "type": "select", + "select": { + "options": [ + { + "id": "f9", + "name": "Travel", + "color": "green" + }, + { + "id": "0b", + "name": "Event", + "color": "pink" + } + ] + } + }, + "People": { + "id": "G'", + "name": "People", + "type": "people", + "people": {} + }, + "Date": { + "id": "VX", + "name": "Date", + "type": "date", + "date": {} + }, + "Last Edited": { + "id": "ow", + "name": "Last Edited", + "type": "last_edited_time", + "last_edited_time": {} + }, + "Status": { + "id": "uX", + "name": "Status", + "type": "select", + "select": { + "options": [ + { + "id": "e4", + "name": "Time Block", + "color": "pink" + }, + { + "id": "27", + "name": "Idea", + "color": "purple" + }, + { + "id": "86", + "name": "Planning", + "color": "blue" + }, + { + "id": "fb", + "name": "Booked", + "color": "green" + }, + { + "id": "9c", + "name": "Reserved", + "color": "green" + }, + { + "id": "49", + "name": "Need Flights", + "color": "red" + }, + { + "id": "23", + "name": "Need Reservation", + "color": "red" + }, + { + "id": "2c", + "name": "Need Tickets", + "color": "red" + }, + { + "id": "69", + "name": "Need Hotel", + "color": "red" + }, + { + "id": "73", + "name": "Canceled", + "color": "red" + } + ] + } + }, + "Nights": { + "id": "%7", + "name": "Nights", + "type": "formula", + "formula": { + "expression": "dateBetween(end(prop(\"Date\")), start(prop(\"Date\")), \"days\")" + } + }, + "Name": { + "id": "title", + "name": "Name", + "type": "title", + "title": {} + } + }, + "parent": { + "type": "page_id", + "page_id": "12" + } + }, + { + "object": "page", + "id": "71", + "created_time": "2021-05-22T22:38:00.000Z", + "last_edited_time": "2021-05-31T17:09:00.000Z", + "cover": null, + "icon": { + "type": "emoji", + "emoji": "🎢" + }, + "parent": { + "type": "page_id", + "page_id": "7c" + }, + "archived": false, + "properties": { + "title": { + "id": "title", + "type": "title", + "title": [ + { + "type": "text", + "text": { + "content": "Plans", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "Plans", + "href": null + } + ] + } + }, + "url": "https://www.notion.so/Plans" + } + ], + "next_cursor": null, + "has_more": false +} diff --git a/src/models/tests/unknown_error.json b/src/models/tests/unknown_error.json new file mode 100644 index 0000000..0e30fea --- /dev/null +++ b/src/models/tests/unknown_error.json @@ -0,0 +1,6 @@ +{ + "object": "error", + "status": 400, + "code": "asadfsdfasd", + "message": "I made this up" +} diff --git a/src/models/users.rs b/src/models/users.rs new file mode 100644 index 0000000..b7702d0 --- /dev/null +++ b/src/models/users.rs @@ -0,0 +1,34 @@ +use crate::ids::UserId; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct UserCommon { + pub id: UserId, + pub name: Option<String>, + pub avatar_url: Option<String>, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct Person { + pub email: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct Bot { + pub email: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum User { + Person { + #[serde(flatten)] + common: UserCommon, + person: Person, + }, + Bot { + #[serde(flatten)] + common: UserCommon, + bot: Bot, + }, +} |