macroblog.rs @ 4fb323f69c11557a51c7da0b2031029f63edf789

feat: Handle 404 result

Now gracefully handle 404, so instead of just panic now it will return a
proper http 404 response.
  1diff --git a/README.md b/README.md
  2index 59cf13b70688f24817edbebcbc7ad01a9706d9be..2a9d334be198dd7701555aba08efc1b1f009dec1 100644
  3--- a/README.md
  4+++ b/README.md
  5@@ -3,7 +3,7 @@
  6 After reading [this
  7 article](https://www.andreinc.net/2022/04/10/a-blog-that-is-a-single-executable-binary)
  8 by Andrei Ciobanu it sparkled in me to do the same thing but with rust.
  9-It is going to be a bit bigger than micro though ;)
 10+It is going to be a bit bigger than micro ;)
 11 
 12 To achieve that I'll be using the following:
 13 
 14diff --git a/src/assets.rs b/src/assets.rs
 15new file mode 100644
 16index 0000000000000000000000000000000000000000..2c39d1b71ae6e763aeb0c338f6e729173c2dee39
 17--- /dev/null
 18+++ b/src/assets.rs
 19@@ -0,0 +1,49 @@
 20+use chrono::NaiveDate;
 21+use regex::Regex;
 22+use rust_embed::RustEmbed;
 23+use sailfish::TemplateOnce;
 24+use std::cmp::{Eq, Ord, PartialEq, PartialOrd};
 25+use std::str;
 26+
 27+pub const BLOG_REGEX: &str = r"(?P<date>[\d]{4}-[\d]{2}-[\d]{2})(?P<title>[a-zA-Z0-9-_]*)";
 28+
 29+#[derive(RustEmbed)]
 30+#[folder = "content/posts/"]
 31+pub struct PostAsset;
 32+
 33+#[derive(TemplateOnce)]
 34+#[template(path = "index.html")]
 35+pub struct IndexTemplate {
 36+    pub posts: Vec<BlogEntry>,
 37+}
 38+
 39+#[derive(TemplateOnce)]
 40+#[template(path = "post.html")]
 41+pub struct PostTemplate {
 42+    pub content: String,
 43+    pub title: String,
 44+    pub date: String,
 45+}
 46+
 47+#[derive(PartialEq, Eq, PartialOrd, Ord)]
 48+pub struct BlogEntry {
 49+    pub title: String,
 50+    pub datetime: NaiveDate,
 51+    pub file: String,
 52+}
 53+
 54+impl BlogEntry {
 55+    pub fn new(path: &String) -> BlogEntry {
 56+        let re = Regex::new(BLOG_REGEX).unwrap();
 57+        let caps = re.captures(path).unwrap();
 58+        let date = &caps["date"];
 59+        let title = str::replace(&caps["title"], "_", " ");
 60+
 61+        BlogEntry {
 62+            title: String::from(title),
 63+            file: String::from(path),
 64+            datetime: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
 65+        }
 66+    }
 67+
 68+}
 69diff --git a/src/bin/actix.rs b/src/bin/actix.rs
 70index 3f00f360f48bf245b9f02814685bf6a687e7d6f5..101fe2e4dc8116e33656ba2f00ad23c5dec0d138 100644
 71--- a/src/bin/actix.rs
 72+++ b/src/bin/actix.rs
 73@@ -1,6 +1,7 @@
 74 use actix_web::{get, web, middleware, App, HttpResponse, HttpServer, Responder, http::header::ContentType};
 75 use macroblog::blog::{render_index_page, render_post_page};
 76-use std::{env};
 77+use macroblog::router::blog_post_exists;
 78+use std::env;
 79 
 80 #[get("/")]
 81 async fn index() -> impl Responder {
 82@@ -14,6 +15,12 @@
 83 
 84 #[get("/posts/{name}")]
 85 async fn posts(name: web::Path<String>) -> impl Responder {
 86+
 87+    if !blog_post_exists(&name) {
 88+        return HttpResponse::NotFound()
 89+            .body("Not Found".to_string());
 90+    }
 91+
 92     let body = render_post_page(&name);
 93 
 94     HttpResponse::Ok()
 95diff --git a/src/blog.rs b/src/blog.rs
 96index c87730374672bbcc6fc8158177b0229644dc5fd8..eaa314abab08acb9c96c1cf4b2e274c4da566819 100644
 97--- a/src/blog.rs
 98+++ b/src/blog.rs
 99@@ -1,62 +1,18 @@
100-use chrono::NaiveDate;
101 use pulldown_cmark::{html, Options, Parser};
102-use regex::Regex;
103-use rust_embed::RustEmbed;
104 use sailfish::TemplateOnce;
105-use std::cmp::{Eq, Ord, PartialEq, PartialOrd};
106 use std::str;
107+use crate::assets::{BlogEntry, PostAsset, IndexTemplate, PostTemplate};
108 
109-const BLOG_REGEX: &str = r"(?P<date>[\d]{4}-[\d]{2}-[\d]{2})(?P<title>[a-zA-Z0-9-_]*)";
110 
111-#[derive(RustEmbed)]
112-#[folder = "content/posts/"]
113-struct PostAsset;
114+pub fn read_assets() -> Vec<BlogEntry> {
115+    let mut entries: Vec<BlogEntry> = PostAsset::iter()
116+        .map(|e| format!("{}", e))
117+        .map(|e| BlogEntry::new(&e))
118+        .collect();
119 
120-#[derive(TemplateOnce)]
121-#[template(path = "index.html")]
122-struct IndexTemplate {
123-    posts: Vec<BlogEntry>,
124-}
125+    entries.sort_by(|a, b| b.datetime.cmp(&a.datetime));
126 
127-#[derive(TemplateOnce)]
128-#[template(path = "post.html")]
129-struct PostTemplate {
130-    content: String,
131-    title: String,
132-    date: String,
133-}
134-
135-#[derive(PartialEq, Eq, PartialOrd, Ord)]
136-pub struct BlogEntry {
137-    pub title: String,
138-    pub datetime: NaiveDate,
139-    pub file: String,
140-}
141-
142-impl BlogEntry {
143-    pub fn new(path: &String) -> BlogEntry {
144-        let re = Regex::new(BLOG_REGEX).unwrap();
145-        let caps = re.captures(path).unwrap();
146-        let date = &caps["date"];
147-        let title = str::replace(&caps["title"], "_", " ");
148-
149-        BlogEntry {
150-            title: String::from(title),
151-            file: String::from(path),
152-            datetime: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
153-        }
154-    }
155-
156-    pub fn read_assets() -> Vec<BlogEntry> {
157-        let mut entries: Vec<BlogEntry> = PostAsset::iter()
158-            .map(|e| format!("{}", e))
159-            .map(|e| BlogEntry::new(&e))
160-            .collect();
161-
162-        entries.sort_by(|a, b| b.datetime.cmp(&a.datetime));
163-
164-        entries
165-    }
166+    entries
167 }
168 
169 fn get_file_content(path: &str) -> String {
170@@ -67,6 +23,7 @@     let mut html_output = &mut String::new();
171     html::push_html(&mut html_output, parser);
172     return html_output.to_string();
173 }
174+
175 
176 pub fn render_post_page(path: &String) -> String {
177     let blog = BlogEntry::new(path);
178@@ -82,7 +39,7 @@ }
179 
180 pub fn render_index_page() -> String {
181     IndexTemplate {
182-        posts: BlogEntry::read_assets(),
183+        posts: read_assets(),
184     }
185     .render_once()
186     .unwrap()
187diff --git a/src/lib.rs b/src/lib.rs
188index 0c698884bb717efdd6fb15fbe9d622e170a3d0c7..90ddff22d138f459fba27b64fd79bb5c9951a5e8 100644
189--- a/src/lib.rs
190+++ b/src/lib.rs
191@@ -1,2 +1,3 @@
192 pub mod blog;
193 pub mod router;
194+pub mod assets;
195diff --git a/src/router.rs b/src/router.rs
196index 35fdf3e8bed96a76bda385f0242e4757bfa06553..c196ab8419c1cf6fa71737e2083a8672ceb2251b 100644
197--- a/src/router.rs
198+++ b/src/router.rs
199@@ -1,3 +1,6 @@
200+use std::borrow::Borrow;
201+
202+use crate::assets::PostAsset;
203 use regex::Regex;
204 
205 const ACTION_REGEX: &str = r"/{0,1}(?P<action>\w*)/(?P<id>.+)";
206@@ -6,6 +9,10 @@ pub enum Router {
207     NotFound,
208     Index,
209     Post { page: String },
210+}
211+
212+pub fn blog_post_exists(name: &str) -> bool {
213+    PostAsset::iter().any(|x| name.eq(&x.to_string()))
214 }
215 
216 impl Router {
217@@ -16,6 +23,13 @@         let action = match caps {
218             Some(ref value) => &value["action"],
219             None => "index",
220         };
221+
222+
223+        // this 7 means the "/posts/" from the full path
224+        let trimmed_path: String = path.chars().skip(7).collect();
225+        if action.eq("posts") && !blog_post_exists(&trimmed_path) {
226+            return Router::NotFound;
227+        }
228 
229         match action {
230             "posts" => Router::Post {
231diff --git a/tests/test_blog.rs b/tests/test_blog.rs
232index b72f80014b9514a502450953c506968c5c53360c..6cd32495f611123f689f10cf5a47aac0290b8a69 100644
233--- a/tests/test_blog.rs
234+++ b/tests/test_blog.rs
235@@ -1,4 +1,5 @@
236 use macroblog::blog::*;
237+use macroblog::assets::*;
238 
239 use chrono::NaiveDate;
240 
241@@ -17,7 +18,7 @@
242 #[test]
243 fn test_read_assets() {
244     // This test meant to test if all files are parsed correctly
245-    let assets = BlogEntry::read_assets();
246+    let assets = read_assets();
247     assert!(assets.iter().count() > 1)
248 }
249 
250diff --git a/tests/test_router.rs b/tests/test_router.rs
251index 7ebe0196cbedddd7831d3a0ded8e62282997c056..cfd4c32bc8b356bfad745090c76a31d304614cdf 100644
252--- a/tests/test_router.rs
253+++ b/tests/test_router.rs
254@@ -1,11 +1,11 @@
255-use macroblog::router::{Router};
256+use macroblog::router::Router;
257 
258 #[test]
259 fn test_router_new_posts() {
260-    match Router::new("/posts/k8s.html") {
261+    match Router::new("/posts/2021-12-26Enable_NFS_on_K3S.md") {
262         Router::NotFound => assert!(false, "Wrong type parse"),
263         Router::Index => assert!(false, "Wrong type parse"),
264-        Router::Post { page } => assert_eq!(page, "k8s.html".to_string())
265+        Router::Post { page } => assert_eq!(page, "2021-12-26Enable_NFS_on_K3S.md".to_string()),
266     };
267 }
268 
269@@ -14,7 +14,7 @@ fn test_router_new_index() {
270     match Router::new("/") {
271         Router::Index => assert!(true),
272         Router::NotFound => assert!(false, "Wrong type parse"),
273-        Router::Post { page: _ } => assert!(false, "Wrong type parse")
274+        Router::Post { page: _ } => assert!(false, "Wrong type parse"),
275     };
276 }
277 
278@@ -23,6 +23,15 @@ fn test_router_new_not_found() {
279     match Router::new("/not_found") {
280         Router::NotFound => assert!(true),
281         Router::Index => assert!(false, "Wrong type parse"),
282-        Router::Post { page: _ } => assert!(false, "Wrong type parse")
283+        Router::Post { page: _ } => assert!(false, "Wrong type parse"),
284+    };
285+}
286+
287+#[test]
288+fn test_router_new_not_found_matching_regex() {
289+    match Router::new("/posts/2021-12-03Enable_NFS_on_K3S.html") {
290+        Router::NotFound => assert!(true),
291+        Router::Index => assert!(false, "Wrong type parse"),
292+        Router::Post { page: _ } => assert!(false, "Wrong type parse"),
293     };
294 }