aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jake Swenson <jake@jakeswenson.com> 2021-08-29 12:59:04 -0700
committerGravatar Jake Swenson <jake@jakeswenson.com> 2021-08-29 12:59:04 -0700
commit1c22088640e5deeadd61017bd90920b652685b5f (patch)
tree59d110a7c8792c39e0c1ea180fa972eee1c4662d
parent8d5779a661f4537ef7d87a44bdfbf240eb054ff1 (diff)
downloadnotion-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.toml7
-rw-r--r--examples/todo/commands/configure.rs9
-rw-r--r--examples/todo/main.rs6
-rw-r--r--src/ids.rs52
-rw-r--r--src/lib.rs169
-rw-r--r--src/models.rs168
-rw-r--r--src/models/error.rs70
-rw-r--r--src/models/properties.rs42
-rw-r--r--src/models/properties/formulas.rs27
-rw-r--r--src/models/properties/tests.rs28
-rw-r--r--src/models/properties/tests/date_property.json8
-rw-r--r--src/models/properties/tests/formula_date_value.json11
-rw-r--r--src/models/properties/tests/formula_number_value.json8
-rw-r--r--src/models/properties/tests/null_select_property.json5
-rw-r--r--src/models/properties/tests/select_property.json9
-rw-r--r--src/models/search.rs5
-rw-r--r--src/models/tests/error.json6
-rw-r--r--src/models/tests/search_results.json209
-rw-r--r--src/models/tests/unknown_error.json6
-rw-r--r--src/models/users.rs34
20 files changed, 688 insertions, 191 deletions
diff --git a/Cargo.toml b/Cargo.toml
index d0bbdc5..b87124f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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)
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index cba9091..98fe0fa 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,
+ },
+}