1use crate::ids::{PageId, UserId};
2use crate::models::paging::{Pageable, Paging, PagingCursor};
3use crate::models::Number;
4use chrono::{DateTime, Utc};
5use serde::ser::SerializeMap;
6use serde::{Serialize, Serializer};
7
8#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
9#[serde(rename_all = "snake_case")]
10pub enum SortDirection {
11 Ascending,
12 Descending,
13}
14
15#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
16#[serde(rename_all = "snake_case")]
17pub enum SortTimestamp {
18 LastEditedTime,
19}
20
21#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
22#[serde(rename_all = "snake_case")]
23pub enum FilterValue {
24 Page,
25 Database,
26}
27
28#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
29#[serde(rename_all = "snake_case")]
30pub enum FilterProperty {
31 Object,
32}
33
34#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
35pub struct Sort {
36 timestamp: SortTimestamp,
38 direction: SortDirection,
39}
40
41#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
42pub struct Filter {
43 property: FilterProperty,
44 value: FilterValue,
45}
46
47#[derive(Serialize, Debug, Eq, PartialEq, Default)]
48pub struct SearchRequest {
49 #[serde(skip_serializing_if = "Option::is_none")]
50 query: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 sort: Option<Sort>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 filter: Option<Filter>,
55 #[serde(flatten)]
56 paging: Option<Paging>,
57}
58
59#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
60#[serde(rename_all = "snake_case")]
61pub enum TextCondition {
62 Equals(String),
63 DoesNotEqual(String),
64 Contains(String),
65 DoesNotContain(String),
66 StartsWith(String),
67 EndsWith(String),
68 #[serde(serialize_with = "serialize_to_true")]
69 IsEmpty,
70 #[serde(serialize_with = "serialize_to_true")]
71 IsNotEmpty,
72}
73
74fn serialize_to_true<S>(serializer: S) -> Result<S::Ok, S::Error>
75where
76 S: Serializer,
77{
78 serializer.serialize_bool(true)
79}
80
81fn serialize_to_empty_object<S>(serializer: S) -> Result<S::Ok, S::Error>
82where
83 S: Serializer,
84{
85 serializer.serialize_map(Some(0))?.end()
87}
88
89#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
90#[serde(rename_all = "snake_case")]
91pub enum NumberCondition {
92 Equals(Number),
93 DoesNotEqual(Number),
94 GreaterThan(Number),
95 LessThan(Number),
96 GreaterThanOrEqualTo(Number),
97 LessThanOrEqualTo(Number),
98 #[serde(serialize_with = "serialize_to_true")]
99 IsEmpty,
100 #[serde(serialize_with = "serialize_to_true")]
101 IsNotEmpty,
102}
103
104#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
105#[serde(rename_all = "snake_case")]
106pub enum CheckboxCondition {
107 Equals(bool),
108 DoesNotEqual(bool),
109}
110
111#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
112#[serde(rename_all = "snake_case")]
113pub enum SelectCondition {
114 Equals(String),
116 DoesNotEqual(String),
118 #[serde(serialize_with = "serialize_to_true")]
120 IsEmpty,
121 #[serde(serialize_with = "serialize_to_true")]
123 IsNotEmpty,
124}
125
126#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
127#[serde(rename_all = "snake_case")]
128pub enum MultiSelectCondition {
129 Contains(String),
131 DoesNotContain(String),
133 #[serde(serialize_with = "serialize_to_true")]
135 IsEmpty,
136 #[serde(serialize_with = "serialize_to_true")]
138 IsNotEmpty,
139}
140
141#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
142#[serde(rename_all = "snake_case")]
143pub enum DateCondition {
144 Equals(DateTime<Utc>),
148 Before(DateTime<Utc>),
152 After(DateTime<Utc>),
156 OnOrBefore(DateTime<Utc>),
160 OnOrAfter(DateTime<Utc>),
164 #[serde(serialize_with = "serialize_to_true")]
166 IsEmpty,
167 #[serde(serialize_with = "serialize_to_true")]
169 IsNotEmpty,
170 #[serde(serialize_with = "serialize_to_empty_object")]
172 PastWeek,
173 #[serde(serialize_with = "serialize_to_empty_object")]
175 PastMonth,
176 #[serde(serialize_with = "serialize_to_empty_object")]
178 PastYear,
179 #[serde(serialize_with = "serialize_to_empty_object")]
181 NextWeek,
182 #[serde(serialize_with = "serialize_to_empty_object")]
184 NextMonth,
185 #[serde(serialize_with = "serialize_to_empty_object")]
187 NextYear,
188}
189
190#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
191#[serde(rename_all = "snake_case")]
192pub enum PeopleCondition {
193 Contains(UserId),
194 DoesNotContain(UserId),
196 #[serde(serialize_with = "serialize_to_true")]
198 IsEmpty,
199 #[serde(serialize_with = "serialize_to_true")]
201 IsNotEmpty,
202}
203
204#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
205#[serde(rename_all = "snake_case")]
206pub enum FilesCondition {
207 #[serde(serialize_with = "serialize_to_true")]
209 IsEmpty,
210 #[serde(serialize_with = "serialize_to_true")]
212 IsNotEmpty,
213}
214
215#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
216#[serde(rename_all = "snake_case")]
217pub enum RelationCondition {
218 Contains(PageId),
220 DoesNotContain(PageId),
222 #[serde(serialize_with = "serialize_to_true")]
224 IsEmpty,
225 #[serde(serialize_with = "serialize_to_true")]
227 IsNotEmpty,
228}
229
230#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
231#[serde(rename_all = "snake_case")]
232pub enum FormulaCondition {
233 Text(TextCondition),
236 Number(NumberCondition),
239 Checkbox(CheckboxCondition),
242 Date(DateCondition),
245}
246
247#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
248#[serde(rename_all = "snake_case")]
249pub enum PropertyCondition {
250 RichText(TextCondition),
251 Number(NumberCondition),
252 Checkbox(CheckboxCondition),
253 Select(SelectCondition),
254 MultiSelect(MultiSelectCondition),
255 Date(DateCondition),
256 People(PeopleCondition),
257 Files(FilesCondition),
258 Relation(RelationCondition),
259 Formula(FormulaCondition),
260}
261
262#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
263#[serde(untagged)]
264pub enum FilterCondition {
265 Property {
266 property: String,
267 #[serde(flatten)]
268 condition: PropertyCondition,
269 },
270 And { and: Vec<FilterCondition> },
272 Or { or: Vec<FilterCondition> },
274}
275
276#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
277#[serde(rename_all = "snake_case")]
278pub enum DatabaseSortTimestamp {
279 CreatedTime,
280 LastEditedTime,
281}
282
283#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
284pub struct DatabaseSort {
285 #[serde(skip_serializing_if = "Option::is_none")]
289 pub property: Option<String>,
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub timestamp: Option<DatabaseSortTimestamp>,
293 pub direction: SortDirection,
294}
295
296#[derive(Serialize, Debug, Eq, PartialEq, Default, Clone)]
297pub struct DatabaseQuery {
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub sorts: Option<Vec<DatabaseSort>>,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub filter: Option<FilterCondition>,
302 #[serde(flatten)]
303 pub paging: Option<Paging>,
304}
305
306impl Pageable for DatabaseQuery {
307 fn start_from(
308 self,
309 starting_point: Option<PagingCursor>,
310 ) -> Self {
311 DatabaseQuery {
312 paging: Some(Paging {
313 start_cursor: starting_point,
314 page_size: self.paging.and_then(|p| p.page_size),
315 }),
316 ..self
317 }
318 }
319}
320
321#[derive(Debug, Eq, PartialEq)]
322pub enum NotionSearch {
323 Query(String),
325 Sort {
329 timestamp: SortTimestamp,
330 direction: SortDirection,
331 },
332 Filter {
336 property: FilterProperty,
339 value: FilterValue,
342 },
343}
344
345impl NotionSearch {
346 pub fn filter_by_databases() -> Self {
347 Self::Filter {
348 property: FilterProperty::Object,
349 value: FilterValue::Database,
350 }
351 }
352}
353
354impl From<NotionSearch> for SearchRequest {
355 fn from(search: NotionSearch) -> Self {
356 match search {
357 NotionSearch::Query(query) => SearchRequest {
358 query: Some(query),
359 ..Default::default()
360 },
361 NotionSearch::Sort {
362 direction,
363 timestamp,
364 } => SearchRequest {
365 sort: Some(Sort {
366 timestamp,
367 direction,
368 }),
369 ..Default::default()
370 },
371 NotionSearch::Filter { property, value } => SearchRequest {
372 filter: Some(Filter { property, value }),
373 ..Default::default()
374 },
375 }
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 mod text_filters {
382 use crate::models::search::PropertyCondition::{Checkbox, Number, RichText, Select};
383 use crate::models::search::{
384 CheckboxCondition, FilterCondition, NumberCondition, SelectCondition, TextCondition,
385 };
386 use serde_json::json;
387
388 #[test]
389 fn text_property_equals() -> Result<(), Box<dyn std::error::Error>> {
390 let json = serde_json::to_value(&FilterCondition::Property {
391 property: "Name".to_string(),
392 condition: RichText(TextCondition::Equals("Test".to_string())),
393 })?;
394 assert_eq!(
395 json,
396 json!({"property":"Name","rich_text":{"equals":"Test"}})
397 );
398
399 Ok(())
400 }
401
402 #[test]
403 fn text_property_contains() -> Result<(), Box<dyn std::error::Error>> {
404 let json = serde_json::to_value(&FilterCondition::Property {
405 property: "Name".to_string(),
406 condition: RichText(TextCondition::Contains("Test".to_string())),
407 })?;
408 assert_eq!(
409 dbg!(json),
410 json!({"property":"Name","rich_text":{"contains":"Test"}})
411 );
412
413 Ok(())
414 }
415
416 #[test]
417 fn text_property_is_empty() -> Result<(), Box<dyn std::error::Error>> {
418 let json = serde_json::to_value(&FilterCondition::Property {
419 property: "Name".to_string(),
420 condition: RichText(TextCondition::IsEmpty),
421 })?;
422 assert_eq!(
423 dbg!(json),
424 json!({"property":"Name","rich_text":{"is_empty":true}})
425 );
426
427 Ok(())
428 }
429
430 #[test]
431 fn text_property_is_not_empty() -> Result<(), Box<dyn std::error::Error>> {
432 let json = serde_json::to_value(&FilterCondition::Property {
433 property: "Name".to_string(),
434 condition: RichText(TextCondition::IsNotEmpty),
435 })?;
436 assert_eq!(
437 dbg!(json),
438 json!({"property":"Name","rich_text":{"is_not_empty":true}})
439 );
440
441 Ok(())
442 }
443
444 #[test]
445 fn compound_query_and() -> Result<(), Box<dyn std::error::Error>> {
446 let json = serde_json::to_value(&FilterCondition::And {
447 and: vec![
448 FilterCondition::Property {
449 property: "Seen".to_string(),
450 condition: Checkbox(CheckboxCondition::Equals(false)),
451 },
452 FilterCondition::Property {
453 property: "Yearly visitor count".to_string(),
454 condition: Number(NumberCondition::GreaterThan(serde_json::Number::from(
455 1000000,
456 ))),
457 },
458 ],
459 })?;
460 assert_eq!(
461 dbg!(json),
462 json!({"and":[
463 {"property":"Seen","checkbox":{"equals":false}},
464 {"property":"Yearly visitor count","number":{"greater_than":1000000}}
465 ]})
466 );
467
468 Ok(())
469 }
470
471 #[test]
472 fn compound_query_or() -> Result<(), Box<dyn std::error::Error>> {
473 let json = serde_json::to_value(&FilterCondition::Or {
474 or: vec![
475 FilterCondition::Property {
476 property: "Description".to_string(),
477 condition: RichText(TextCondition::Contains("fish".to_string())),
478 },
479 FilterCondition::And {
480 and: vec![
481 FilterCondition::Property {
482 property: "Food group".to_string(),
483 condition: Select(SelectCondition::Equals(
484 "🥦Vegetable".to_string(),
485 )),
486 },
487 FilterCondition::Property {
488 property: "Is protein rich?".to_string(),
489 condition: Checkbox(CheckboxCondition::Equals(true)),
490 },
491 ],
492 },
493 ],
494 })?;
495 assert_eq!(
496 dbg!(json),
497 json!({"or":[
498 {"property":"Description","rich_text":{"contains":"fish"}},
499 {"and":[
500 {"property":"Food group","select":{"equals":"🥦Vegetable"}},
501 {"property":"Is protein rich?","checkbox":{"equals":true}}
502 ]}
503 ]})
504 );
505
506 Ok(())
507 }
508 }
509}