diff --git a/src/models/mod.rs b/src/models/mod.rs index 9bd6d77..418e892 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -6,14 +6,15 @@ pub mod search; pub mod text; pub mod users; -use crate::models::properties::{PropertyConfiguration, PropertyValue}; +use crate::models::properties::PropertyValue; use crate::models::text::RichText; use crate::Error; use block::FileOrEmojiObject; +use properties::{PropertyConfigurationData, PropertyWithId}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use crate::ids::{AsIdentifier, DatabaseId, PageId}; +use crate::ids::{AsIdentifier, DatabaseId, PageId, PropertyId}; use crate::models::block::{Block, CreateBlock}; use crate::models::error::ErrorResponse; use crate::models::paging::PagingCursor; @@ -48,7 +49,7 @@ pub struct Database { // // value object // A Property object. - pub properties: HashMap, + pub properties: HashMap>, } impl AsIdentifier for Database { @@ -150,25 +151,47 @@ pub enum Parent { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Properties { + #[serde(flatten)] + pub properties: HashMap>, +} + +/// A struct that contains only the data of the properties without the id for create and update requests. +/// The key can either be the name or the id of the property. +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct PropertiesWithoutIds { #[serde(flatten)] pub properties: HashMap, } impl Properties { pub fn title(&self) -> Option { - self.properties.values().find_map(|p| match p { + self.properties.values().find_map(|p| match &p.value { PropertyValue::Title { title, .. } => { Some(title.iter().map(|t| t.plain_text()).collect()) } _ => None, }) } + + pub fn get_by_name( + &self, + name: &str, + ) -> Option<&PropertyWithId> { + self.properties.get(name) + } + + pub fn get_by_id( + &self, + id: &PropertyId, + ) -> Option<&PropertyWithId> { + self.properties.values().find(|p| p.id == *id) + } } #[derive(Serialize, Debug, Eq, PartialEq)] pub struct PageCreateRequest { pub parent: Parent, - pub properties: Properties, + pub properties: PropertiesWithoutIds, #[serde(skip_serializing_if = "Option::is_none")] pub children: Option>, } diff --git a/src/models/properties.rs b/src/models/properties.rs index 0547cf3..cd1848f 100644 --- a/src/models/properties.rs +++ b/src/models/properties.rs @@ -1,3 +1,6 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::{Display, Formatter}; + use crate::models::text::RichText; use crate::models::users::User; @@ -9,6 +12,109 @@ use serde::{Deserialize, Serialize}; #[cfg(test)] mod tests; +/// A property can exist together with an id or standalone as `PropertyValue`. +/// This trait allows us to treat both cases the same way, when we don't care about the id. +/// The `type_name` method is used to get the type of the property value as string, which is useful for error handling. +pub trait Property { + fn value(&self) -> &PropertyValue; + fn type_name(&self) -> String; +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct PropertyWithId { + pub id: PropertyId, + #[serde(flatten)] + pub value: T, +} + +impl Property for PropertyWithId { + fn value(&self) -> &PropertyValue { + &self.value + } + + fn type_name(&self) -> String { + self.value.type_name() + } +} + +impl Property for PropertyValue { + fn value(&self) -> &PropertyValue { + self + } + + fn type_name(&self) -> String { + match self { + PropertyValue::Title { .. } => "Title", + PropertyValue::Text { .. } => "Text", + PropertyValue::Number { .. } => "Number", + PropertyValue::Select { .. } => "Select", + PropertyValue::Status { .. } => "Status", + PropertyValue::MultiSelect { .. } => "MultiSelect", + PropertyValue::Date { .. } => "Date", + PropertyValue::People { .. } => "People", + PropertyValue::Files { .. } => "Files", + PropertyValue::Checkbox { .. } => "Checkbox", + PropertyValue::Url { .. } => "Url", + PropertyValue::Email { .. } => "Email", + PropertyValue::PhoneNumber { .. } => "PhoneNumber", + PropertyValue::Formula { .. } => "Formula", + PropertyValue::Relation { .. } => "Relation", + PropertyValue::Rollup { .. } => "Rollup", + PropertyValue::CreatedTime { .. } => "CreatedTime", + PropertyValue::CreatedBy { .. } => "CreatedBy", + PropertyValue::LastEditedTime { .. } => "LastEditedTime", + PropertyValue::LastEditedBy { .. } => "LastEditedBy", + PropertyValue::Button { .. } => "Button", + } + .to_string() + } +} + +impl PropertyExpect for PropertyValue {} +impl PropertyExpect for PropertyWithId {} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum PropertyConfigurationData { + Title, + #[serde(rename = "rich_text")] + Text, + Number { + number: NumberDetails, + }, + Select { + select: Select, + }, + Status { + status: Status, + }, + MultiSelect { + multi_select: Select, + }, + Date, + People, + Files, + Checkbox, + Url, + Email, + PhoneNumber, + Formula { + formula: Formula, + }, + Relation { + relation: Relation, + }, + Rollup { + rollup: Rollup, + }, + CreatedTime, + CreatedBy, + LastEditedTime, + LastEditBy, + Button, +} + /// How the number is displayed in Notion. #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Copy, Clone, Hash)] #[serde(rename_all = "snake_case")] @@ -151,114 +257,69 @@ pub struct Rollup { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] -pub enum PropertyConfiguration { - /// Represents the special Title property required on every database. - /// See +pub enum PropertyValue { Title { - id: PropertyId, + title: Vec, }, - /// Represents a Text property - /// #[serde(rename = "rich_text")] Text { - id: PropertyId, + rich_text: Vec, }, - /// Represents a Number Property - /// See Number { - id: PropertyId, - /// How the number is displayed in Notion. - number: NumberDetails, + number: Option, }, - /// Represents a Select Property - /// See Select { - id: PropertyId, - select: Select, + select: Option, }, - /// Represents a Status property Status { - id: PropertyId, - status: Status, + status: Option, }, - /// Represents a Multi-select Property - /// See MultiSelect { - id: PropertyId, - multi_select: Select, + multi_select: Option>, }, - /// Represents a Date Property - /// See Date { - id: PropertyId, + date: Option, + }, + Formula { + formula: FormulaResultValue, + }, + Relation { + relation: Option>, + }, + Rollup { + rollup: Option, }, - /// Represents a People Property - /// See People { - id: PropertyId, + people: Vec, }, - /// Represents a File Property - /// See - // Todo: File a bug with notion - // Documentation issue: docs claim type name is `file` but it is in fact `files` Files { - id: PropertyId, + files: Option>, }, - /// Represents a Checkbox Property - /// See Checkbox { - id: PropertyId, + checkbox: bool, }, - /// Represents a URL Property - /// See Url { - id: PropertyId, + url: Option, }, - /// Represents a Email Property - /// See Email { - id: PropertyId, + email: Option, }, - /// Represents a Phone number Property - /// See PhoneNumber { - id: PropertyId, - }, - /// See - Formula { - id: PropertyId, - formula: Formula, - }, - /// See - Relation { - id: PropertyId, - relation: Relation, - }, - /// See - Rollup { - id: PropertyId, - rollup: Rollup, + phone_number: Option, }, - /// See CreatedTime { - id: PropertyId, + created_time: DateTime, }, - /// See CreatedBy { - id: PropertyId, + created_by: User, }, - /// See LastEditedTime { - id: PropertyId, - }, - /// See - LastEditBy { - id: PropertyId, + last_edited_time: DateTime, }, - - Button { - id: PropertyId, + LastEditedBy { + last_edited_by: User, }, + Button, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] @@ -331,118 +392,6 @@ pub struct File { pub expiry_time: String, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[serde(tag = "type")] -#[serde(rename_all = "snake_case")] - -pub enum PropertyValue { - // - Title { - id: PropertyId, - title: Vec, - }, - /// - #[serde(rename = "rich_text")] - Text { - id: PropertyId, - rich_text: Vec, - }, - /// - Number { - id: PropertyId, - number: Option, - }, - /// - Select { - id: PropertyId, - select: Option, - }, - /// - Status { - id: PropertyId, - status: Option, - }, - /// - MultiSelect { - id: PropertyId, - multi_select: Option>, - }, - /// - Date { - id: PropertyId, - date: Option, - }, - /// - Formula { - id: PropertyId, - formula: FormulaResultValue, - }, - /// - /// It is actually an array of relations - Relation { - id: PropertyId, - relation: Option>, - }, - /// - Rollup { - id: PropertyId, - rollup: Option, - }, - /// - People { - id: PropertyId, - people: Vec, - }, - /// - Files { - id: PropertyId, - files: Option>, - }, - /// - Checkbox { - id: PropertyId, - checkbox: bool, - }, - /// - Url { - id: PropertyId, - url: Option, - }, - /// - Email { - id: PropertyId, - email: Option, - }, - /// - PhoneNumber { - id: PropertyId, - phone_number: Option, - }, - /// - CreatedTime { - id: PropertyId, - created_time: DateTime, - }, - /// - CreatedBy { - id: PropertyId, - created_by: User, - }, - /// - LastEditedTime { - id: PropertyId, - last_edited_time: DateTime, - }, - /// - LastEditedBy { - id: PropertyId, - last_edited_by: User, - }, - Button { - id: PropertyId, - }, -} - /// #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type")] @@ -514,3 +463,577 @@ pub enum RollupPropertyValue { last_edited_by: User, }, } + +pub trait PropertyExpect: Property { + /// Allows for easy access to the property value. + /// + /// This is useful if you know the type of the property you want to access and don't need match statements + /// + /// ```ignore + /// let title = property.expect_value::>().unwrap(); + /// ``` + /// This will fail if the actual property type is not compatible with the expected value type. + /// + /// See the following implementations of `TryFrom` for supported types: + /// - `Vec` for Title, Text + /// - `Option` for Number + /// - `Option` for Select, Status + /// - `Option>` for MultiSelect + /// - `Option` for Date + /// - `Option>` for Relation + /// - `Option>` for Files + /// - `bool` for Checkbox + /// - `Option` for Url, Email, PhoneNumber + /// - `DateTime` for CreatedTime, LastEditedTime + /// - `User` for CreatedBy, LastEditedBy + /// + /// You can also create your own implementation of `TryFrom` for custom types. This is useful if you + /// need to convert the property value to a specific type in your project. + /// + /// For example if you want to access text property values always as a `String` without formatting you can do the following: + /// + /// ```ignore + /// use rusticnotion::models::properties::{Property, PropertyExpect, PropertyValue, WrongPropertyTypeError} + /// + /// struct TextValue(String); + /// impl TryFrom for TextValue { + /// type Error = WrongPropertyTypeError; + /// + /// fn try_from(value: PropertyValue) -> Result { + /// match value { + /// PropertyValue::Text { rich_text, .. } => { + /// let combined_text = rich_text + /// .iter() + /// .map(|rt| rt.plain_text().to_string()) + /// .collect::>() + /// .join(""); + /// Ok(TextValue(combined_text)) + /// } + /// _ => Err(WrongPropertyTypeError { + /// expected: vec!["Text".to_string()], + /// actual: value.type_name(), + /// }), + /// } + /// } + ///} + /// assert_eq!( + /// page.properties + /// .get_by_name("Text") + /// .unwrap() + /// .expect_value::() + /// .unwrap() + /// .0, + /// "hello world".to_string() + /// ); + /// ``` + fn expect_value(&self) -> Result + where + T: TryFrom, + { + self.value().to_owned().try_into() + } + + /// Allows for easy access to the title property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a title, even if `Vec` is implemented for the property. + fn expect_title(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Title { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Title".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the text property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a text, even if `Vec` is implemented for the property. + fn expect_text(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Text { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Text".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the number property value. + /// This is a shortcut for `expect_value::()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a number, even if `Option` is implemented for the property. + fn expect_number(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Number { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Number".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the select property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a select, even if `Option` is implemented for the property. + fn expect_select(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Select { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Select".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the status property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a status, even if `Option` is implemented for the property. + fn expect_status(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Status { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Status".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the multi-select property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a multi-select, even if `Option>` is implemented for the property. + fn expect_multi_select(&self) -> Result>, WrongPropertyTypeError> { + match self.value() { + PropertyValue::MultiSelect { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["MultiSelect".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the date property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a date, even if `Option` is implemented for the property. + fn expect_date(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Date { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Date".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the people property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a people, even if `Option>` is implemented for the property. + fn expect_people(&self) -> Result>, WrongPropertyTypeError> { + match self.value() { + PropertyValue::People { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["People".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the files property value. + /// This is a shortcut for `expect_value::>>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a files, even if `Option>` is implemented for the property. + fn expect_files(&self) -> Result>, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Files { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Files".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the checkbox property value. + /// This is a shortcut for `expect_value::()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a checkbox, even if `bool` is implemented for the property. + fn expect_checkbox(&self) -> Result { + match self.value() { + PropertyValue::Checkbox { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Checkbox".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the url property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a url, even if `Option` is implemented for the property. + fn expect_url(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Url { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Url".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the email property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not an email, even if `Option` is implemented for the property. + fn expect_email(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Email { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Email".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the phone number property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a phone number, even if `Option` is implemented for the property. + fn expect_phone_number(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::PhoneNumber { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["PhoneNumber".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the formula property value. + /// This is a shortcut for `expect_value::()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a formula, even if `FormulaResultValue` is implemented for the property. + fn expect_formula(&self) -> Result { + match self.value() { + PropertyValue::Formula { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Formula".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the relation property value. + /// This is a shortcut for `expect_value::>>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a relation, even if `Option>` is implemented for the property. + fn expect_relation(&self) -> Result>, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Relation { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Relation".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the rollup property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a rollup, even if `Option` is implemented for the property. + fn expect_rollup(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::Rollup { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["Rollup".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the created time property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a created time, even if `DateTime` is implemented for the property. + fn expect_created_time(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::CreatedTime { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["CreatedTime".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the created by property value. + /// This is a shortcut for `expect_value::()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a created by, even if `User` is implemented for the property. + fn expect_created_by(&self) -> Result { + match self.value() { + PropertyValue::CreatedBy { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["CreatedBy".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the last edited time property value. + /// This is a shortcut for `expect_value::>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a last edited time, even if `DateTime` is implemented for the property. + fn expect_last_edited_time(&self) -> Result, WrongPropertyTypeError> { + match self.value() { + PropertyValue::LastEditedTime { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["LastEditedTime".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the last edited by property value. + /// This is a shortcut for `expect_value::()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a last edited by, even if `User` is implemented for the property. + fn expect_last_edited_by(&self) -> Result { + match self.value() { + PropertyValue::LastEditedBy { .. } => self.value().to_owned().try_into(), + _ => Err(WrongPropertyTypeError { + expected: vec!["LastEditedBy".to_string()], + actual: self.type_name(), + }), + } + } + + /// Allows for easy access to the button property value. + /// This is a shortcut for `expect_value::<()>()` which is more explicit about the expected property type. + /// This will also return an error if the property is not a button, even if `()` is implemented for the property. + fn expect_button(&self) -> Result<(), WrongPropertyTypeError> { + match self.value() { + PropertyValue::Button { .. } => Ok(()), + _ => Err(WrongPropertyTypeError { + expected: vec!["Button".to_string()], + actual: self.type_name().to_string(), + }), + } + } +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] +pub struct WrongPropertyTypeError { + pub expected: Vec, + pub actual: String, +} + +impl Display for WrongPropertyTypeError { + fn fmt( + &self, + f: &mut Formatter<'_>, + ) -> std::fmt::Result { + write!( + f, + "Wrong property type: expected one of {:?}, got {}", + self.expected, self.actual + ) + } +} + +pub trait FromPropertyValue: Sized { + fn from_property_value(property: impl Property) -> Result; +} + +impl TryFrom for Vec { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Title { title, .. } => Ok(title), + PropertyValue::Text { rich_text, .. } => Ok(rich_text), + _ => Err(WrongPropertyTypeError { + expected: vec!["Title".to_string(), "Text".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for Option { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Number { number, .. } => Ok(number), + _ => Err(WrongPropertyTypeError { + expected: vec!["Number".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for Option { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Select { select, .. } => Ok(select), + PropertyValue::Status { status, .. } => Ok(status), + _ => Err(WrongPropertyTypeError { + expected: vec!["Select".to_string(), "Status".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for Option> { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::MultiSelect { multi_select, .. } => Ok(multi_select), + _ => Err(WrongPropertyTypeError { + expected: vec!["MultiSelect".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for Option { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Date { date, .. } => Ok(date), + _ => Err(WrongPropertyTypeError { + expected: vec!["Date".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for FormulaResultValue { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Formula { formula, .. } => Ok(formula), + _ => Err(WrongPropertyTypeError { + expected: vec!["Formula".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for Option> { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Relation { relation, .. } => Ok(relation), + _ => Err(WrongPropertyTypeError { + expected: vec!["Relation".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for Option> { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Files { files, .. } => Ok(files), + _ => Err(WrongPropertyTypeError { + expected: vec!["Files".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for bool { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Checkbox { checkbox, .. } => Ok(checkbox), + _ => Err(WrongPropertyTypeError { + expected: vec!["Checkbox".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for Option { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Url { url, .. } => Ok(url), + PropertyValue::Email { email, .. } => Ok(email), + PropertyValue::PhoneNumber { phone_number, .. } => Ok(phone_number), + _ => Err(WrongPropertyTypeError { + expected: vec![ + "Url".to_string(), + "Email".to_string(), + "PhoneNumber".to_string(), + ], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for DateTime { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::CreatedTime { created_time, .. } => Ok(created_time), + PropertyValue::LastEditedTime { + last_edited_time, .. + } => Ok(last_edited_time), + _ => Err(WrongPropertyTypeError { + expected: vec!["CreatedTime".to_string(), "LastEditedTime".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for User { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::CreatedBy { created_by, .. } => Ok(created_by), + PropertyValue::LastEditedBy { last_edited_by, .. } => Ok(last_edited_by), + _ => Err(WrongPropertyTypeError { + expected: vec!["CreatedBy".to_string(), "LastEditedBy".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for Option> { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::People { people, .. } => Ok(Some(people)), + _ => Err(WrongPropertyTypeError { + expected: vec!["People".to_string()], + actual: value.type_name(), + }), + } + } +} + +impl TryFrom for Option { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Rollup { rollup, .. } => Ok(rollup), + _ => Err(WrongPropertyTypeError { + expected: vec!["Rollup".to_string()], + actual: value.type_name(), + }), + } + } +} diff --git a/src/models/properties/tests.rs b/src/models/properties/tests.rs index fc00a40..83163b4 100644 --- a/src/models/properties/tests.rs +++ b/src/models/properties/tests.rs @@ -1,5 +1,5 @@ -use crate::models::properties::{DateOrDateTime, RollupPropertyValue, RollupValue}; -use crate::models::properties::{FormulaResultValue, PropertyValue}; +use crate::models::properties::FormulaResultValue; +use crate::models::properties::{DateOrDateTime, PropertyValue, RollupPropertyValue, RollupValue}; use chrono::NaiveDate; #[test] diff --git a/tests/databases.rs b/tests/databases.rs index ce147a7..6d45bd4 100644 --- a/tests/databases.rs +++ b/tests/databases.rs @@ -1,8 +1,11 @@ +use std::convert::TryFrom; + use test_log::test; mod common; use common::test_client; use rusticnotion::models::{ block::FileOrEmojiObject, + properties::{Property, PropertyExpect, PropertyValue, WrongPropertyTypeError}, search::{ DatabaseQuery, FilterCondition, FilterProperty, FilterValue, NotionSearch, PropertyCondition, TextCondition, @@ -85,13 +88,46 @@ async fn query_database() -> Result<(), Box> { .await?; assert_eq!(pages.results().len(), 1); + let page = pages.results()[0].clone(); assert_eq!( - pages.results()[0].icon, + page.icon, Some(FileOrEmojiObject::Emoji { emoji: "🌋".to_string(), }) ); + struct TextValue(String); + impl TryFrom for TextValue { + type Error = WrongPropertyTypeError; + + fn try_from(value: PropertyValue) -> Result { + match value { + PropertyValue::Text { rich_text, .. } => { + let combined_text = rich_text + .iter() + .map(|rt| rt.plain_text().to_string()) + .collect::>() + .join(""); + Ok(TextValue(combined_text)) + } + _ => Err(WrongPropertyTypeError { + expected: vec!["Text".to_string()], + actual: value.type_name(), + }), + } + } + } + + assert_eq!( + page.properties + .get_by_name("Text") + .unwrap() + .expect_value::() + .unwrap() + .0, + "hello world".to_string() + ); + let pages = api.query_database(db, DatabaseQuery::default()).await?; for page in pages.results() {