Blog attempt 5
1pub mod admin;
2pub mod fetch;
3pub mod list;
4pub mod render;
5pub mod view;
6
7use std::fs::read_to_string;
8
9use chrono::{DateTime, Datelike as _, FixedOffset, Local, NaiveDateTime, Utc};
10use maud::{Markup, html};
11use serde::Deserialize;
12const ISO8601_DATE: &str = "%Y-%m-%dT%H:%M";
13
14#[derive(Deserialize, Debug)]
15pub struct Post {
16 pub bsky_uri: Option<String>,
17 pub category: Option<String>,
18 pub contents: String,
19 pub creation_datetime: DateTime<Local>,
20 pub slug: String,
21 pub subtitle: Option<String>,
22 pub title: String,
23}
24
25#[must_use]
26pub fn clock_icon() -> Markup {
27 html! {
28 span.emoji-icon aria-label="Published on" {( "🕒" )}
29 }
30}
31
32#[must_use]
33pub fn clean_empty_string(s: Option<String>) -> Option<String> {
34 match s {
35 Some(string) if string.trim().is_empty() => None,
36 other => other,
37 }
38}
39
40#[must_use]
41pub fn format_date(date: chrono::DateTime<Local>) -> String {
42 let suffix = eng_ordinal_suffix(date.day() as usize);
43 date.format(&format!("%B %d{suffix}, %Y")).to_string()
44}
45
46#[must_use]
47pub fn parse_date(datestring: &str) -> chrono::DateTime<FixedOffset> {
48 match chrono::DateTime::parse_from_rfc3339(datestring) {
49 Ok(dt) => dt,
50 Err(_) => {
51 match NaiveDateTime::parse_from_str(datestring, "%Y-%m-%dT%H:%M")
52 .map(|ndt| DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc))
53 {
54 Ok(dt) => dt.into(),
55 Err(_) => chrono::DateTime::UNIX_EPOCH.into(),
56 }
57 }
58 }
59}
60
61#[must_use]
62#[expect(
63 clippy::expect_used,
64 reason = "We need the secret for progam execution"
65)]
66pub fn read_secret() -> String {
67 read_to_string("./.secret")
68 .expect("Failed to read secret file")
69 .trim()
70 .to_owned()
71}
72
73#[must_use]
74pub fn eng_ordinal_suffix(n: usize) -> &'static str {
75 let tens = n % 100;
76 if (11..=13).contains(&tens) {
77 return "th";
78 }
79
80 match n % 10 {
81 1 => "st",
82 2 => "nd",
83 3 => "rd",
84 _ => "th",
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::error::AppError;
92
93 #[test]
94 fn test_eng_ordinal_suffix_basic() {
95 assert_eq!(eng_ordinal_suffix(1), "st");
96 assert_eq!(eng_ordinal_suffix(2), "nd");
97 assert_eq!(eng_ordinal_suffix(3), "rd");
98 assert_eq!(eng_ordinal_suffix(4), "th");
99 assert_eq!(eng_ordinal_suffix(11), "th");
100 assert_eq!(eng_ordinal_suffix(12), "th");
101 assert_eq!(eng_ordinal_suffix(13), "th");
102 assert_eq!(eng_ordinal_suffix(21), "st");
103 assert_eq!(eng_ordinal_suffix(22), "nd");
104 assert_eq!(eng_ordinal_suffix(23), "rd");
105 }
106
107 #[test]
108 fn test_app_error_conversion() {
109 let db_error = rusqlite::Error::QueryReturnedNoRows;
110 let app_error: AppError = db_error.into();
111 match app_error {
112 AppError::NotFound => {}
113 _ => panic!("Should convert to NotFound"),
114 }
115 }
116}