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(rename_all = "snake_case")]
264pub enum TimestampCondition {
265 CreatedTime(DateCondition),
266 LastEditedTime(DateCondition),
267}
268
269#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
270#[serde(untagged)]
271pub enum FilterCondition {
272 Property {
273 property: String,
274 #[serde(flatten)]
275 condition: PropertyCondition,
276 },
277 Timestamp {
278 timestamp: String,
279 #[serde(flatten)]
280 condition: TimestampCondition,
281 },
282 And { and: Vec<FilterCondition> },
284 Or { or: Vec<FilterCondition> },
286}
287
288#[derive(Serialize, Debug, Eq, PartialEq, Hash, Copy, Clone)]
289#[serde(rename_all = "snake_case")]
290pub enum DatabaseSortTimestamp {
291 CreatedTime,
292 LastEditedTime,
293}
294
295#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
296pub struct DatabaseSort {
297 #[serde(skip_serializing_if = "Option::is_none")]
301 pub property: Option<String>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub timestamp: Option<DatabaseSortTimestamp>,
305 pub direction: SortDirection,
306}
307
308#[derive(Serialize, Debug, Eq, PartialEq, Default, Clone)]
309pub struct DatabaseQuery {
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub sorts: Option<Vec<DatabaseSort>>,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 pub filter: Option<FilterCondition>,
314 #[serde(flatten)]
315 pub paging: Option<Paging>,
316}
317
318impl Pageable for DatabaseQuery {
319 fn start_from(
320 self,
321 starting_point: Option<PagingCursor>,
322 ) -> Self {
323 DatabaseQuery {
324 paging: Some(Paging {
325 start_cursor: starting_point,
326 page_size: self.paging.and_then(|p| p.page_size),
327 }),
328 ..self
329 }
330 }
331}
332
333#[derive(Debug, Eq, PartialEq)]
334pub enum NotionSearch {
335 Query(String),
337 Sort {
341 timestamp: SortTimestamp,
342 direction: SortDirection,
343 },
344 Filter {
348 property: FilterProperty,
351 value: FilterValue,
354 },
355}
356
357impl NotionSearch {
358 pub fn filter_by_databases() -> Self {
359 Self::Filter {
360 property: FilterProperty::Object,
361 value: FilterValue::Database,
362 }
363 }
364}
365
366impl From<NotionSearch> for SearchRequest {
367 fn from(search: NotionSearch) -> Self {
368 match search {
369 NotionSearch::Query(query) => SearchRequest {
370 query: Some(query),
371 ..Default::default()
372 },
373 NotionSearch::Sort {
374 direction,
375 timestamp,
376 } => SearchRequest {
377 sort: Some(Sort {
378 timestamp,
379 direction,
380 }),
381 ..Default::default()
382 },
383 NotionSearch::Filter { property, value } => SearchRequest {
384 filter: Some(Filter { property, value }),
385 ..Default::default()
386 },
387 }
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 mod text_filters {
394 use crate::models::search::PropertyCondition::{Checkbox, Number, RichText, Select};
395 use crate::models::search::{
396 CheckboxCondition, FilterCondition, NumberCondition, SelectCondition, TextCondition,
397 };
398 use serde_json::json;
399
400 #[test]
401 fn text_property_equals() -> Result<(), Box<dyn std::error::Error>> {
402 let json = serde_json::to_value(&FilterCondition::Property {
403 property: "Name".to_string(),
404 condition: RichText(TextCondition::Equals("Test".to_string())),
405 })?;
406 assert_eq!(
407 json,
408 json!({"property":"Name","rich_text":{"equals":"Test"}})
409 );
410
411 Ok(())
412 }
413
414 #[test]
415 fn text_property_contains() -> Result<(), Box<dyn std::error::Error>> {
416 let json = serde_json::to_value(&FilterCondition::Property {
417 property: "Name".to_string(),
418 condition: RichText(TextCondition::Contains("Test".to_string())),
419 })?;
420 assert_eq!(
421 dbg!(json),
422 json!({"property":"Name","rich_text":{"contains":"Test"}})
423 );
424
425 Ok(())
426 }
427
428 #[test]
429 fn text_property_is_empty() -> Result<(), Box<dyn std::error::Error>> {
430 let json = serde_json::to_value(&FilterCondition::Property {
431 property: "Name".to_string(),
432 condition: RichText(TextCondition::IsEmpty),
433 })?;
434 assert_eq!(
435 dbg!(json),
436 json!({"property":"Name","rich_text":{"is_empty":true}})
437 );
438
439 Ok(())
440 }
441
442 #[test]
443 fn text_property_is_not_empty() -> Result<(), Box<dyn std::error::Error>> {
444 let json = serde_json::to_value(&FilterCondition::Property {
445 property: "Name".to_string(),
446 condition: RichText(TextCondition::IsNotEmpty),
447 })?;
448 assert_eq!(
449 dbg!(json),
450 json!({"property":"Name","rich_text":{"is_not_empty":true}})
451 );
452
453 Ok(())
454 }
455
456 #[test]
457 fn compound_query_and() -> Result<(), Box<dyn std::error::Error>> {
458 let json = serde_json::to_value(&FilterCondition::And {
459 and: vec![
460 FilterCondition::Property {
461 property: "Seen".to_string(),
462 condition: Checkbox(CheckboxCondition::Equals(false)),
463 },
464 FilterCondition::Property {
465 property: "Yearly visitor count".to_string(),
466 condition: Number(NumberCondition::GreaterThan(serde_json::Number::from(
467 1000000,
468 ))),
469 },
470 ],
471 })?;
472 assert_eq!(
473 dbg!(json),
474 json!({"and":[
475 {"property":"Seen","checkbox":{"equals":false}},
476 {"property":"Yearly visitor count","number":{"greater_than":1000000}}
477 ]})
478 );
479
480 Ok(())
481 }
482
483 #[test]
484 fn compound_query_or() -> Result<(), Box<dyn std::error::Error>> {
485 let json = serde_json::to_value(&FilterCondition::Or {
486 or: vec![
487 FilterCondition::Property {
488 property: "Description".to_string(),
489 condition: RichText(TextCondition::Contains("fish".to_string())),
490 },
491 FilterCondition::And {
492 and: vec![
493 FilterCondition::Property {
494 property: "Food group".to_string(),
495 condition: Select(SelectCondition::Equals(
496 "🥦Vegetable".to_string(),
497 )),
498 },
499 FilterCondition::Property {
500 property: "Is protein rich?".to_string(),
501 condition: Checkbox(CheckboxCondition::Equals(true)),
502 },
503 ],
504 },
505 ],
506 })?;
507 assert_eq!(
508 dbg!(json),
509 json!({"or":[
510 {"property":"Description","rich_text":{"contains":"fish"}},
511 {"and":[
512 {"property":"Food group","select":{"equals":"🥦Vegetable"}},
513 {"property":"Is protein rich?","checkbox":{"equals":true}}
514 ]}
515 ]})
516 );
517
518 Ok(())
519 }
520 }
521}