use serde::Deserialize; const NOTION_API_VERSION: &str = "2022-06-28"; #[derive(Deserialize)] pub struct QueryResponse { pub results: Vec, pub has_more: bool, } #[derive(Deserialize)] pub struct Page { pub id: String, pub properties: serde_json::Value, } #[derive(Deserialize)] pub struct BlockChildrenResponse { pub results: Vec, pub has_more: bool, } #[derive(Deserialize)] pub struct Block { #[serde(rename = "type")] pub kind: String, #[serde(flatten)] pub content: serde_json::Value, pub id: String, } pub fn extract_title(properties: &serde_json::Value, field: &str) -> String { properties[field]["title"] .as_array() .and_then(|rich_text_array| rich_text_array.first()) .and_then(|rich_text_element| rich_text_element["plain_text"].as_str()) .unwrap_or("") .to_string() } pub fn extract_rich_text( properties: &serde_json::Value, field: &str, ) -> String { properties[field]["rich_text"] .as_array() .map(|rich_text_array| { rich_text_array .iter() .filter_map(|rich_text_element| { rich_text_element["plain_text"].as_str() }) .collect::>() .join("") }) .unwrap_or_default() } pub fn extract_number( properties: &serde_json::Value, field: &str, ) -> Option { extract_non_negative_whole_number(properties, field) .and_then(|number_value| u8::try_from(number_value).ok()) } fn extract_non_negative_whole_number( properties: &serde_json::Value, field: &str, ) -> Option { let number_value = &properties[field]["number"]; number_value.as_u64().or_else(|| { number_value.as_f64().and_then(|floating_value| { if !floating_value.is_finite() || floating_value < 0.0 { return None; } let rounded_value = floating_value.round(); let is_whole_number = (floating_value - rounded_value).abs() <= f64::EPSILON; if !is_whole_number { return None; } format!("{rounded_value:.0}").parse::().ok() }) }) } pub fn extract_date(properties: &serde_json::Value, field: &str) -> String { properties[field]["date"]["start"].as_str().unwrap_or("").to_string() } pub fn extract_relation_ids( properties: &serde_json::Value, field: &str, ) -> Vec { properties[field]["relation"] .as_array() .map(|relation_array| { relation_array .iter() .filter_map(|relation_entry| { relation_entry["id"].as_str().map(String::from) }) .collect() }) .unwrap_or_default() } pub fn extract_checkbox(properties: &serde_json::Value, field: &str) -> bool { properties[field]["checkbox"].as_bool().unwrap_or(false) } pub fn format_notion_date(iso_date: &str) -> String { chrono::NaiveDate::parse_from_str(iso_date, "%Y-%m-%d").map_or_else( |_| iso_date.to_string(), |date| date.format("%B %-d, %Y").to_string(), ) } pub fn extract_block_plain_text(block: &Block) -> String { let block_content = &block.content[&block.kind]; block_content["rich_text"] .as_array() .map(|rich_text_array| { rich_text_array .iter() .filter_map(|rich_text_element| { rich_text_element["plain_text"].as_str() }) .collect::>() .join("") }) .unwrap_or_default() } pub fn query_database( http_client: &reqwest::blocking::Client, api_key: &str, database_identifier: &str, ) -> Result, reqwest::Error> { let mut all_pages = Vec::new(); let mut start_cursor: Option = None; loop { let mut request_body = serde_json::json!({}); if let Some(ref cursor) = start_cursor { request_body["start_cursor"] = serde_json::json!(cursor); } let response = http_client .post(format!( "https://api.notion.com/v1/databases/{database_identifier}/query" )) .header("Authorization", format!("Bearer {api_key}")) .header("Notion-Version", NOTION_API_VERSION) .json(&request_body) .send()? .json::()?; let has_more = response.has_more; all_pages.extend(response.results); if !has_more { break; } start_cursor = all_pages.last().map(|last_page| last_page.id.clone()); } Ok(all_pages) } pub fn fetch_page_content( http_client: &reqwest::blocking::Client, api_key: &str, page_identifier: &str, ) -> Result { let mut all_lines = Vec::new(); let mut start_cursor: Option = None; loop { let mut request_url = format!( "https://api.notion.com/v1/blocks/{page_identifier}/children?page_size=100" ); if let Some(ref cursor) = start_cursor { request_url.push_str(&format!("&start_cursor={cursor}")); } let response = http_client .get(&request_url) .header("Authorization", format!("Bearer {api_key}")) .header("Notion-Version", NOTION_API_VERSION) .send()? .json::()?; let has_more = response.has_more; for block in &response.results { let line = match block.kind.as_str() { "heading_1" => format!("# {}", extract_block_plain_text(block)), "heading_2" => format!("## {}", extract_block_plain_text(block)), "heading_3" => format!("### {}", extract_block_plain_text(block)), "bulleted_list_item" | "numbered_list_item" => { format!("* {}", extract_block_plain_text(block)) } "quote" => format!("> {}", extract_block_plain_text(block)), "code" => format!("```\n{}\n```", extract_block_plain_text(block)), "divider" => "---".to_string(), _ => extract_block_plain_text(block), }; all_lines.push(line); } if !has_more { break; } start_cursor = response.results.last().map(|last_block| last_block.id.clone()); } Ok(all_lines.join("\n")) } #[cfg(test)] mod tests { use {super::extract_checkbox, serde_json::json}; #[test] fn extract_checkbox_true() { let properties = json!({ "Hidden": { "checkbox": true } }); assert!(extract_checkbox(&properties, "Hidden")); } #[test] fn extract_checkbox_false_when_missing() { let properties = json!({ "Title": { "title": [] } }); assert!(!extract_checkbox(&properties, "Hidden")); } }