use crate::ids::{PageId, UserId};
use crate::models::paging::{Pageable, Paging, PagingCursor};
use crate::models::Number;
use chrono::{DateTime, Utc};
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum SortDirection {
Ascending,
Descending,
}
#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum SortTimestamp {
LastEditedTime,
}
#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum FilterValue {
Page,
Database,
}
#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum FilterProperty {
Object,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
pub struct Sort {
timestamp: SortTimestamp,
direction: SortDirection,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
pub struct Filter {
property: FilterProperty,
value: FilterValue,
}
#[derive(Serialize, Debug, Eq, PartialEq, Default)]
pub struct SearchRequest {
#[serde(skip_serializing_if = "Option::is_none")]
query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
sort: Option<Sort>,
#[serde(skip_serializing_if = "Option::is_none")]
filter: Option<Filter>,
#[serde(flatten)]
paging: Option<Paging>,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TextCondition {
Equals(String),
DoesNotEqual(String),
Contains(String),
DoesNotContain(String),
StartsWith(String),
EndsWith(String),
#[serde(serialize_with = "serialize_to_true")]
IsEmpty,
#[serde(serialize_with = "serialize_to_true")]
IsNotEmpty,
}
fn serialize_to_true<S>(serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_bool(true)
}
fn serialize_to_empty_object<S>(serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_map(Some(0))?.end()
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum NumberCondition {
Equals(Number),
DoesNotEqual(Number),
GreaterThan(Number),
LessThan(Number),
GreaterThanOrEqualTo(Number),
LessThanOrEqualTo(Number),
#[serde(serialize_with = "serialize_to_true")]
IsEmpty,
#[serde(serialize_with = "serialize_to_true")]
IsNotEmpty,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum CheckboxCondition {
Equals(bool),
DoesNotEqual(bool),
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum SelectCondition {
Equals(String),
DoesNotEqual(String),
#[serde(serialize_with = "serialize_to_true")]
IsEmpty,
#[serde(serialize_with = "serialize_to_true")]
IsNotEmpty,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum MultiSelectCondition {
Contains(String),
DoesNotContain(String),
#[serde(serialize_with = "serialize_to_true")]
IsEmpty,
#[serde(serialize_with = "serialize_to_true")]
IsNotEmpty,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum DateCondition {
Equals(DateTime<Utc>),
Before(DateTime<Utc>),
After(DateTime<Utc>),
OnOrBefore(DateTime<Utc>),
OnOrAfter(DateTime<Utc>),
#[serde(serialize_with = "serialize_to_true")]
IsEmpty,
#[serde(serialize_with = "serialize_to_true")]
IsNotEmpty,
#[serde(serialize_with = "serialize_to_empty_object")]
PastWeek,
#[serde(serialize_with = "serialize_to_empty_object")]
PastMonth,
#[serde(serialize_with = "serialize_to_empty_object")]
PastYear,
#[serde(serialize_with = "serialize_to_empty_object")]
NextWeek,
#[serde(serialize_with = "serialize_to_empty_object")]
NextMonth,
#[serde(serialize_with = "serialize_to_empty_object")]
NextYear,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum PeopleCondition {
Contains(UserId),
DoesNotContain(UserId),
#[serde(serialize_with = "serialize_to_true")]
IsEmpty,
#[serde(serialize_with = "serialize_to_true")]
IsNotEmpty,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum FilesCondition {
#[serde(serialize_with = "serialize_to_true")]
IsEmpty,
#[serde(serialize_with = "serialize_to_true")]
IsNotEmpty,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum RelationCondition {
Contains(PageId),
DoesNotContain(PageId),
#[serde(serialize_with = "serialize_to_true")]
IsEmpty,
#[serde(serialize_with = "serialize_to_true")]
IsNotEmpty,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum FormulaCondition {
Text(TextCondition),
Number(NumberCondition),
Checkbox(CheckboxCondition),
Date(DateCondition),
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(rename_all = "snake_case")]
pub enum PropertyCondition {
RichText(TextCondition),
Number(NumberCondition),
Checkbox(CheckboxCondition),
Select(SelectCondition),
MultiSelect(MultiSelectCondition),
Date(DateCondition),
People(PeopleCondition),
Files(FilesCondition),
Relation(RelationCondition),
Formula(FormulaCondition),
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[serde(untagged)]
pub enum FilterCondition {
Property {
property: String,
#[serde(flatten)]
condition: PropertyCondition,
},
And { and: Vec<FilterCondition> },
Or { or: Vec<FilterCondition> },
}
#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum DatabaseSortTimestamp {
CreatedTime,
LastEditedTime,
}
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
pub struct DatabaseSort {
#[serde(skip_serializing_if = "Option::is_none")]
pub property: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<DatabaseSortTimestamp>,
pub direction: SortDirection,
}
#[derive(Serialize, Debug, Eq, PartialEq, Default, Clone)]
pub struct DatabaseQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub sorts: Option<Vec<DatabaseSort>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<FilterCondition>,
#[serde(flatten)]
pub paging: Option<Paging>,
}
impl Pageable for DatabaseQuery {
fn start_from(
self,
starting_point: Option<PagingCursor>,
) -> Self {
DatabaseQuery {
paging: Some(Paging {
start_cursor: starting_point,
page_size: self.paging.and_then(|p| p.page_size),
}),
..self
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum NotionSearch {
Query(String),
Sort {
timestamp: SortTimestamp,
direction: SortDirection,
},
Filter {
property: FilterProperty,
value: FilterValue,
},
}
impl NotionSearch {
pub fn filter_by_databases() -> Self {
Self::Filter {
property: FilterProperty::Object,
value: FilterValue::Database,
}
}
}
impl From<NotionSearch> for SearchRequest {
fn from(search: NotionSearch) -> Self {
match search {
NotionSearch::Query(query) => SearchRequest {
query: Some(query),
..Default::default()
},
NotionSearch::Sort {
direction,
timestamp,
} => SearchRequest {
sort: Some(Sort {
timestamp,
direction,
}),
..Default::default()
},
NotionSearch::Filter { property, value } => SearchRequest {
filter: Some(Filter { property, value }),
..Default::default()
},
}
}
}
#[cfg(test)]
mod tests {
mod text_filters {
use crate::models::search::PropertyCondition::{Checkbox, Number, RichText, Select};
use crate::models::search::{
CheckboxCondition, FilterCondition, NumberCondition, SelectCondition, TextCondition,
};
use serde_json::json;
#[test]
fn text_property_equals() -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_value(&FilterCondition::Property {
property: "Name".to_string(),
condition: RichText(TextCondition::Equals("Test".to_string())),
})?;
assert_eq!(
json,
json!({"property":"Name","rich_text":{"equals":"Test"}})
);
Ok(())
}
#[test]
fn text_property_contains() -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_value(&FilterCondition::Property {
property: "Name".to_string(),
condition: RichText(TextCondition::Contains("Test".to_string())),
})?;
assert_eq!(
dbg!(json),
json!({"property":"Name","rich_text":{"contains":"Test"}})
);
Ok(())
}
#[test]
fn text_property_is_empty() -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_value(&FilterCondition::Property {
property: "Name".to_string(),
condition: RichText(TextCondition::IsEmpty),
})?;
assert_eq!(
dbg!(json),
json!({"property":"Name","rich_text":{"is_empty":true}})
);
Ok(())
}
#[test]
fn text_property_is_not_empty() -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_value(&FilterCondition::Property {
property: "Name".to_string(),
condition: RichText(TextCondition::IsNotEmpty),
})?;
assert_eq!(
dbg!(json),
json!({"property":"Name","rich_text":{"is_not_empty":true}})
);
Ok(())
}
#[test]
fn compound_query_and() -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_value(&FilterCondition::And {
and: vec![
FilterCondition::Property {
property: "Seen".to_string(),
condition: Checkbox(CheckboxCondition::Equals(false)),
},
FilterCondition::Property {
property: "Yearly visitor count".to_string(),
condition: Number(NumberCondition::GreaterThan(serde_json::Number::from(
1000000,
))),
},
],
})?;
assert_eq!(
dbg!(json),
json!({"and":[
{"property":"Seen","checkbox":{"equals":false}},
{"property":"Yearly visitor count","number":{"greater_than":1000000}}
]})
);
Ok(())
}
#[test]
fn compound_query_or() -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_value(&FilterCondition::Or {
or: vec![
FilterCondition::Property {
property: "Description".to_string(),
condition: RichText(TextCondition::Contains("fish".to_string())),
},
FilterCondition::And {
and: vec![
FilterCondition::Property {
property: "Food group".to_string(),
condition: Select(SelectCondition::Equals(
"🥦Vegetable".to_string(),
)),
},
FilterCondition::Property {
property: "Is protein rich?".to_string(),
condition: Checkbox(CheckboxCondition::Equals(true)),
},
],
},
],
})?;
assert_eq!(
dbg!(json),
json!({"or":[
{"property":"Description","rich_text":{"contains":"fish"}},
{"and":[
{"property":"Food group","select":{"equals":"🥦Vegetable"}},
{"property":"Is protein rich?","checkbox":{"equals":true}}
]}
]})
);
Ok(())
}
}
}