From 146509efdffaa5e8ce62f4ab9a94c923a13bd845 Mon Sep 17 00:00:00 2001 From: AINDUSTRIES Date: Sun, 26 Oct 2025 22:01:13 +0100 Subject: [PATCH] Refactored project to use Builder pattern. --- src/html/elements.rs | 400 +++++++++++++++++++++++++++++++----------- src/html/layouts.rs | 83 +++++++++ src/html/mod.rs | 91 ++-------- src/html/pages.rs | 199 +++++++++++++++++++++ src/main.rs | 7 +- src/pages/mod.rs | 89 +++------- src/pages/projects.rs | 84 +++++---- 7 files changed, 669 insertions(+), 284 deletions(-) create mode 100644 src/html/layouts.rs create mode 100644 src/html/pages.rs diff --git a/src/html/elements.rs b/src/html/elements.rs index 570b61c..edb6166 100644 --- a/src/html/elements.rs +++ b/src/html/elements.rs @@ -1,115 +1,160 @@ use crate::html::Render; -#[allow(non_camel_case_types)] -pub(crate) struct h1 { +pub(crate) struct Heading { + level: u8, + id: String, text: String, + classes: Vec, } -impl Render for h1 { +impl Render for Heading { fn render(&self) -> String { - format!("

{}

", self.text) + let classes = self.classes.join(" "); + format!( + "{}", + self.level, self.id, classes, self.text, self.level + ) } } -impl h1 { - pub(crate) fn new(text: T) -> Self +impl Heading { + pub(crate) fn builder() -> HeadingBuilder { + HeadingBuilder::new() + } +} + +pub(crate) struct HeadingBuilder { + level: Option, + id: Option, + text: Option, + classes: Option>, +} + +impl HeadingBuilder { + pub(crate) fn new() -> Self { + Self { + level: None, + id: None, + text: None, + classes: None, + } + } + + pub(crate) fn level(self, level: u8) -> Self { + Self { + level: Some(level), + ..self + } + } + + pub(crate) fn id(self, id: T) -> Self where T: Into, { - Self { text: text.into() } + Self { + id: Some(id.into()), + ..self + } } -} -#[allow(non_camel_case_types)] -pub(crate) struct h2 { - text: String, -} - -impl Render for h2 { - fn render(&self) -> String { - format!("

{}

", self.text) - } -} - -impl h2 { - pub(crate) fn new(text: T) -> Self + pub(crate) fn text(self, text: T) -> Self where T: Into, { - Self { text: text.into() } + Self { + text: Some(text.into()), + ..self + } + } + + pub(crate) fn classes(self, classes: Vec) -> Self + where + T: Into, + { + Self { + classes: Some(classes.into_iter().map(|x| x.into()).collect()), + ..self + } + } + + pub(crate) fn build(self) -> Heading { + let level = self.level.unwrap_or(1); + let id = self.id.unwrap_or(String::new()); + let text = self.text.unwrap_or(String::new()); + let classes = self.classes.unwrap_or(Vec::new()); + Heading { + level, + id, + text, + classes, + } } } -#[allow(non_camel_case_types)] -pub(crate) struct link { +pub(crate) struct Link { rel: String, href: String, } -impl Render for link { +impl Render for Link { fn render(&self) -> String { format!("", self.rel, self.href) } } -impl link { - pub(crate) fn new(rel: T, href: U) -> Self - where - T: Into, - U: Into, - { - Self { - rel: rel.into(), - href: href.into(), - } +impl Link { + pub(crate) fn builder() -> LinkBuilder { + LinkBuilder::new() } } -#[allow(non_camel_case_types)] -pub(crate) struct div { - id: String, - classes: Vec, - content: Vec>, +pub(crate) struct LinkBuilder { + rel: Option, + href: Option, } -impl Render for div { - fn render(&self) -> String { - let classes = self.classes.join(" "); - format!( - "
{}
", - self.id, - classes, - self.content.render() - ) - } -} - -impl div { - pub(crate) fn new(id: T, classes: Vec) -> Self - where - T: Into, - U: Into + Clone, - { +impl LinkBuilder { + pub(crate) fn new() -> Self { Self { - id: id.into(), - classes: classes.iter().map(|x| x.clone().into()).collect(), - content: vec![], + rel: None, + href: None, } } - pub(crate) fn append_element(&mut self, element: impl Render + 'static) { - self.content.push(Box::new(element)); + pub(crate) fn rel(self, rel: T) -> Self + where + T: Into, + { + Self { + rel: Some(rel.into()), + ..self + } + } + + pub(crate) fn href(self, href: T) -> Self + where + T: Into, + { + Self { + href: Some(href.into()), + ..self + } + } + + pub(crate) fn build(self) -> Link { + let rel = self.rel.unwrap_or(String::new()); + let href = self.href.unwrap_or(String::new()); + Link { rel, href } } } -#[allow(non_camel_case_types)] -pub(crate) struct p { +pub(crate) struct Paragraph { id: String, classes: Vec, text: String, } -impl Render for p { +impl Render for Paragraph { fn render(&self) -> String { let classes = self.classes.join(" "); format!( @@ -119,29 +164,72 @@ impl Render for p { } } -impl p { - pub(crate) fn new(id: T, classes: Vec, text: V) -> Self - where - T: Into, - U: Into + Clone, - V: Into, - { - Self { - id: id.into(), - classes: classes.iter().map(|x| x.clone().into()).collect(), - text: text.into(), - } +impl Paragraph { + pub(crate) fn builder() -> ParagraphBuilder { + ParagraphBuilder::new() } } -#[allow(non_camel_case_types)] -pub(crate) struct img { +pub(crate) struct ParagraphBuilder { + id: Option, + classes: Option>, + text: Option, +} + +impl ParagraphBuilder { + pub(crate) fn new() -> Self { + Self { + id: None, + classes: None, + text: None, + } + } + + pub(crate) fn id(self, id: T) -> Self + where + T: Into, + { + Self { + id: Some(id.into()), + ..self + } + } + + pub(crate) fn classes(self, classes: Vec) -> Self + where + T: Into, + { + Self { + classes: Some(classes.into_iter().map(|x| x.into()).collect()), + ..self + } + } + + pub(crate) fn text(self, text: T) -> Self + where + T: Into, + { + Self { + text: Some(text.into()), + ..self + } + } + + pub(crate) fn build(self) -> Paragraph { + let id = self.id.unwrap_or(String::new()); + let classes = self.classes.unwrap_or(Vec::new()); + let text = self.text.unwrap_or(String::new()); + Paragraph { id, classes, text } + } +} + +pub(crate) struct Image { id: String, classes: Vec, src: String, } -impl Render for img { +impl Render for Image { fn render(&self) -> String { let classes = self.classes.join(" "); format!( @@ -151,30 +239,73 @@ impl Render for img { } } -impl img { - pub(crate) fn new(id: T, classes: Vec, src: V) -> Self - where - T: Into, - U: Into + Clone, - V: Into, - { - Self { - id: id.into(), - classes: classes.iter().map(|x| x.clone().into()).collect(), - src: src.into(), - } +impl Image { + pub(crate) fn builder() -> ImageBuilder { + ImageBuilder::new() } } -#[allow(non_camel_case_types)] -pub(crate) struct a { +pub(crate) struct ImageBuilder { + id: Option, + classes: Option>, + src: Option, +} + +impl ImageBuilder { + pub(crate) fn new() -> Self { + Self { + id: None, + classes: None, + src: None, + } + } + + pub(crate) fn id(self, id: T) -> Self + where + T: Into, + { + Self { + id: Some(id.into()), + ..self + } + } + + pub(crate) fn classes(self, classes: Vec) -> Self + where + T: Into, + { + Self { + classes: Some(classes.into_iter().map(|x| x.into()).collect()), + ..self + } + } + + pub(crate) fn src(self, src: T) -> Self + where + T: Into, + { + Self { + src: Some(src.into()), + ..self + } + } + + pub(crate) fn build(self) -> Image { + let id = self.id.unwrap_or(String::new()); + let classes = self.classes.unwrap_or(Vec::new()); + let src = self.src.unwrap_or(String::new()); + Image { id, classes, src } + } +} + +pub(crate) struct Anchor { id: String, classes: Vec, href: String, text: String, } -impl Render for a { +impl Render for Anchor { fn render(&self) -> String { let classes = self.classes.join(" "); format!( @@ -184,19 +315,78 @@ impl Render for a { } } -impl a { - pub(crate) fn new(id: T, classes: Vec, href: V, text: W) -> Self +impl Anchor { + pub(crate) fn builder() -> AnchorBuilder { + AnchorBuilder::new() + } +} + +pub(crate) struct AnchorBuilder { + id: Option, + classes: Option>, + href: Option, + text: Option, +} + +impl AnchorBuilder { + pub(crate) fn new() -> Self { + Self { + id: None, + classes: None, + href: None, + text: None, + } + } + + pub(crate) fn id(self, id: T) -> Self where T: Into, - U: Into + Clone, - V: Into, - W: Into, { Self { - id: id.into(), - classes: classes.iter().map(|x| x.clone().into()).collect(), - href: href.into(), - text: text.into(), + id: Some(id.into()), + ..self + } + } + + pub(crate) fn classes(self, classes: Vec) -> Self + where + T: Into, + { + Self { + classes: Some(classes.into_iter().map(|x| x.into()).collect()), + ..self + } + } + + pub(crate) fn href(self, href: T) -> Self + where + T: Into, + { + Self { + href: Some(href.into()), + ..self + } + } + pub(crate) fn text(self, text: T) -> Self + where + T: Into, + { + Self { + text: Some(text.into()), + ..self + } + } + + pub(crate) fn build(self) -> Anchor { + let id = self.id.unwrap_or(String::new()); + let classes = self.classes.unwrap_or(Vec::new()); + let href = self.href.unwrap_or(String::new()); + let text = self.text.unwrap_or(String::new()); + Anchor { + id, + classes, + href, + text, } } } diff --git a/src/html/layouts.rs b/src/html/layouts.rs new file mode 100644 index 0000000..2d8bc92 --- /dev/null +++ b/src/html/layouts.rs @@ -0,0 +1,83 @@ +use crate::html::Render; + +pub(crate) struct Division { + id: String, + classes: Vec, + elements: Vec>, +} + +impl Render for Division { + fn render(&self) -> String { + let classes = self.classes.join(" "); + format!( + "
{}
", + self.id, + classes, + self.elements.render() + ) + } +} + +impl Division { + pub(crate) fn builder() -> DivisionBuilder { + DivisionBuilder::new() + } + + pub(crate) fn append_element(&mut self, element: impl Render + 'static) { + self.elements.push(Box::new(element)); + } +} + +pub(crate) struct DivisionBuilder { + id: Option, + classes: Option>, + elements: Option>>, +} + +impl DivisionBuilder { + pub(crate) fn new() -> Self { + Self { + id: None, + classes: None, + elements: None, + } + } + + pub(crate) fn id(self, id: T) -> Self + where + T: Into, + { + Self { + id: Some(id.into()), + ..self + } + } + + pub(crate) fn classes(self, classes: Vec) -> Self + where + T: Into, + { + Self { + classes: Some(classes.into_iter().map(|x| x.into()).collect()), + ..self + } + } + + pub(crate) fn elements(self, elements: Vec>) -> Self { + Self { + elements: Some(elements), + ..self + } + } + + pub(crate) fn build(self) -> Division { + let id = self.id.unwrap_or(String::new()); + let classes = self.classes.unwrap_or(Vec::new()); + let elements = self.elements.unwrap_or(Vec::new()); + Division { + id, + classes, + elements, + } + } +} diff --git a/src/html/mod.rs b/src/html/mod.rs index d0ba92f..3499232 100644 --- a/src/html/mod.rs +++ b/src/html/mod.rs @@ -1,92 +1,31 @@ pub(crate) mod elements; +pub(crate) mod layouts; +pub(crate) mod pages; + +macro_rules! boxed_vec { + ($($element:ident),+) => { + vec![ + $( + Box::new($element) as Box, + )* + ] + }; +} + +pub(crate) use boxed_vec; pub(crate) trait Render { fn render(&self) -> String; } -pub(crate) struct Page { - title: String, - head: Vec>, - body: Vec>, -} - -pub(crate) struct PageBuilder { - title: Option, - head: Option>> - body: Option>> -} - -impl PageBuilder { - pub(crate) fn new() -> Self { - Self { - title: None, - head: None, - body: None, - } - } - - pub(crate) fn title(mut self, title: T) - where T: Into - { - self.title = Some(title.into()); - } -} - impl Render for Vec> { fn render(&self) -> String { let mut result = String::new(); for element in self { let render = element.render(); result.push_str(&render); + result.push('\n'); } result } } - -impl Render for Page { - fn render(&self) -> String { - format!( - "\ - - - - - {} - -{} - - -{} - -", - self.title, - self.head.render(), - self.body.render() - ) - } -} - -impl Page { - pub(crate) fn builder() -> PageBuilder { - PageBuilder::new() - } - - pub(crate) fn new(title: T) -> Self - where - T: Into, - { - Page { - title: title.into(), - head: vec![], - body: vec![], - } - } - - pub(crate) fn append_element_to_body(&mut self, element: impl Render + 'static) { - self.body.push(Box::new(element)); - } - - pub(crate) fn append_element_to_head(&mut self, element: impl Render + 'static) { - self.head.push(Box::new(element)); - } -} diff --git a/src/html/pages.rs b/src/html/pages.rs new file mode 100644 index 0000000..a380e81 --- /dev/null +++ b/src/html/pages.rs @@ -0,0 +1,199 @@ +use crate::html::Render; +use crate::html::boxed_vec; +use crate::html::elements::{Anchor, Image, Link, Paragraph}; +use crate::html::layouts::Division; + +pub(crate) struct Page { + title: String, + head: Vec>, + body: Vec>, +} + +impl Render for Page { + fn render(&self) -> String { + format!( + "\ + + + + + {} + +{} + + +{} + +", + self.title, + self.head.render(), + self.body.render() + ) + } +} + +impl Page { + pub(crate) fn builder() -> PageBuilder { + PageBuilder::new() + } + + pub(crate) fn append_element_to_body(&mut self, element: impl Render + 'static) { + self.body.push(Box::new(element)); + } + + pub(crate) fn append_element_to_head(&mut self, element: impl Render + 'static) { + self.head.push(Box::new(element)); + } +} + +pub(crate) struct PageBuilder { + title: Option, + head: Option>>, + body: Option>>, +} + +impl PageBuilder { + pub(crate) fn new() -> Self { + Self { + title: None, + head: None, + body: None, + } + } + + pub(crate) fn title(self, title: T) -> Self + where + T: Into, + { + Self { + title: Some(title.into()), + ..self + } + } + + pub(crate) fn head(self, head: Vec>) -> Self { + Self { + head: Some(head), + ..self + } + } + + pub(crate) fn body(self, body: Vec>) -> Self { + Self { + body: Some(body), + ..self + } + } + + pub(crate) fn build(self) -> Page { + let title = self.title.unwrap_or(String::new()); + let head = self.head.unwrap_or(Vec::new()); + let body = self.body.unwrap_or(Vec::new()); + Page { title, head, body } + } +} + +pub(crate) struct BasePage { + page: Page, +} + +impl BasePage { + pub(crate) fn builder() -> BasePageBuilder { + BasePageBuilder::new() + } + + pub(crate) fn append_element_to_head(&mut self, element: impl Render + 'static) { + self.page.append_element_to_head(element); + } + pub(crate) fn append_element_to_body(&mut self, element: impl Render + 'static) { + self.page.append_element_to_body(element); + } +} + +impl Render for BasePage { + fn render(&self) -> String { + self.page.render() + } +} + +pub(crate) struct BasePageBuilder { + title: Option, + head: Option>>, + body: Option>>, +} + +impl BasePageBuilder { + pub(crate) fn new() -> Self { + Self { + title: None, + head: None, + body: None, + } + } + + pub(crate) fn title(self, title: T) -> Self + where + T: Into, + { + Self { + title: Some(title.into()), + ..self + } + } + + pub(crate) fn head(self, head: Vec>) -> Self { + Self { + head: Some(head), + ..self + } + } + pub(crate) fn body(self, body: Vec>) -> Self { + Self { + body: Some(body), + ..self + } + } + + pub(crate) fn build(self) -> BasePage { + let title = self.title.unwrap_or(String::new()); + let head = self.head.unwrap_or(Vec::new()); + let body = self.body.unwrap_or(Vec::new()); + let name = Paragraph::builder() + .id("name") + .classes(vec!["name"]) + .text("AINDUSTRIES") + .build(); + let home = Anchor::builder().id("home").classes(vec!["nav-button"]).href("/").text("Home").build(); + let projects = Anchor::builder().id("projects").classes(vec!["nav-button"]).href("/projects").text("Projects").build(); + let buttons = Division::builder() + .classes(vec!["nav-buttons"]) + .elements(boxed_vec![home, projects]) + .build(); + let header = Division::builder() + .id("header") + .classes(vec!["header"]) + .elements(boxed_vec![name, buttons]) + .build(); + // background + let image = Image::builder() + .id("background") + .classes(vec!["background"]) + .src("/static/img/background.png") + .build(); + // css + let base = Link::builder() + .rel("stylesheet") + .href("/static/css/base.css") + .build(); + let mut final_head = boxed_vec![base]; + final_head.extend(head); + let mut final_body = boxed_vec![image, header]; + final_body.extend(body); + let page = Page::builder() + .title(title) + .head(final_head) + .body(final_body) + .build(); + BasePage { page } + } +} diff --git a/src/main.rs b/src/main.rs index 5f64ca6..783b333 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,12 @@ use pages::{ #[derive(Parser)] #[command(name = "aindustries-be", version, about = "This is the amazing webserver for aindustries.be!", long_about = None)] struct Cli { - #[arg(short, long, value_name = "CONFIG", help = "Path to config file, defaults to /etc/aindustries-be/config.toml")] + #[arg( + short, + long, + value_name = "CONFIG", + help = "Path to config file, defaults to /etc/aindustries-be/config.toml" + )] config: Option, } diff --git a/src/pages/mod.rs b/src/pages/mod.rs index a6ee46c..3a10661 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,76 +1,39 @@ pub(crate) mod files; pub(crate) mod projects; -use crate::html::elements::{a, div, h1, h2, img, link, p}; -use crate::html::{Page, Render}; +use crate::html::elements::{Heading, Link}; +use crate::html::layouts::Division; +use crate::html::pages::BasePage; +use crate::html::{Render, boxed_vec}; use actix_web::{Responder, get}; -pub(crate) struct BasePage { - page: Page, -} - -impl BasePage { - pub(crate) fn new(title: T) -> Self - where - T: Into, - { - let mut page = Page::new(title); - // header - let mut header = div::new("header", vec!["header"]); - let name = p::new("name", vec!["name"], "AINDUSTRIES"); - let mut buttons = div::new("buttons", vec!["nav-buttons"]); - let home = a::new("home", vec!["nav-button"], "/", "Home"); - let projects = a::new("projects", vec!["nav-button"], "/projects", "Projects"); - buttons.append_element(home); - buttons.append_element(projects); - header.append_element(name); - header.append_element(buttons); - // background - let image = img::new( - "background", - vec!["background"], - "/static/img/background.png", - ); - // css - let base = link::new("stylesheet", "/static/css/base.css"); - // add elements to the page in order - page.append_element_to_head(base); - page.append_element_to_body(image); - page.append_element_to_body(header); - Self { page } - } - - pub(crate) fn append_element_to_head(&mut self, element: impl Render + 'static) { - self.page.append_element_to_head(element); - } - pub(crate) fn append_element_to_body(&mut self, element: impl Render + 'static) { - self.page.append_element_to_body(element); - } -} - -impl Render for BasePage { - fn render(&self) -> String { - self.page.render() - } -} - #[get("/")] async fn index() -> impl Responder { - let mut page = BasePage::new("AINDUSTRIES"); // introduction - let mut intro = div::new("intro", vec!["intro"]); - let hi = h1::new("Hello and welcome!"); - let detail = h2::new( - "This here is a small website to show the passion I have for computers.
\ + let hi = Heading::builder().text("Hello and welcome!").build(); + let detail = Heading::builder() + .level(2) + .text( + "This here is a small website to show the passion I have for computers.
\ I have always had a good time creating and discovering new things.
\ - Your may discover some of my projects here on this showcase.", - ); - intro.append_element(hi); - intro.append_element(detail); + Your may discover some of my projects here on this little showcase.", + ) + .build(); + let intro = Division::builder() + .id("intro") + .classes(vec!["intro"]) + .elements(boxed_vec![hi, detail]) + .build(); // css - let index = link::new("stylesheet", "static/css/index.css"); + let index = Link::builder() + .rel("stylesheet") + .href("static/css/index.css") + .build(); // add elements to the page in order - page.append_element_to_head(index); - page.append_element_to_body(intro); + let page = BasePage::builder() + .title("AINDUSTRIES") + .head(boxed_vec![index]) + .body(boxed_vec![intro]) + .build(); page.render() } diff --git a/src/pages/projects.rs b/src/pages/projects.rs index 8b423ef..d2a4e0b 100644 --- a/src/pages/projects.rs +++ b/src/pages/projects.rs @@ -1,64 +1,70 @@ -use super::BasePage; -use crate::html::Render; -use crate::html::elements::{a, div, h1, h2, link, p}; +use crate::html::elements::{Anchor, Heading, Link, Paragraph}; +use crate::html::layouts::Division; +use crate::html::pages::BasePage; +use crate::html::{Render, boxed_vec}; use actix_web::{Responder, get, web}; #[get("/projects")] async fn projects() -> impl Responder { - let mut page = BasePage::new("Projects"); - let title = h1::new("My projects"); - let desc = h2::new( - "Here you will find all my projects which deserve to be shown
\ + let title = Heading::builder().text("My projects").build(); + let desc = Heading::builder() + .level(2) + .text( + "Here you will find all my projects which deserve to be shown
\ (I've done a lot of small projects but they are not worth it.)", - ); - let mut website = div::new("project-website", vec!["project"]); - let website_title = h1::new("Website"); - let mut info = div::new("project-website-info", vec!["project-info"]); - let website_desc = p::new( - "website-desc", - vec!["project-desc"], - "This project is the website you currently are on.", - ); - let view = a::new( - "website-view", - vec!["project-view"], - "/projects/website", - "Learn More", - ); - info.append_element(website_desc); - info.append_element(view); - website.append_element(website_title); - website.append_element(info); - let css = link::new("stylesheet", "/static/css/projects.css"); - page.append_element_to_head(css); - page.append_element_to_body(title); - page.append_element_to_body(desc); - page.append_element_to_body(website); + ) + .build(); + let website_title = Heading::builder().text("Website").build(); + let website_desc = Paragraph::builder() + .classes(vec!["project-desc"]) + .text("This project is the website you currently are on.") + .build(); + let view = Anchor::builder().classes(vec!["project-view"]).href("/projects/website").text("Learn More").build(); + let info = Division::builder() + .classes(vec!["project-info"]) + .elements(boxed_vec![website_desc, view]) + .build(); + let website = Division::builder() + .classes(vec!["project"]) + .elements(boxed_vec![website_title, info]) + .build(); + let css = Link::builder() + .rel("stylesheet") + .href("/static/css/projects.css") + .build(); + let page = BasePage::builder() + .title("Projects") + .head(boxed_vec![css]) + .body(boxed_vec![title, desc, website]) + .build(); page.render() } #[get("/projects/{project}")] async fn project(project: web::Path) -> impl Responder { let project = project.into_inner(); - let mut page = BasePage::new(format!("Project-{}", project)); + let css = Link::builder() + .rel("stylesheet") + .href("/static/css/project.css") + .build(); + let mut page = BasePage::builder() + .title(format!("Project-{}", project)) + .head(boxed_vec![css]) + .build(); match project.as_str() { "website" => { - let title = h1::new("Website"); - let desc = p::new( - "description", - vec!["description"], + let title = Heading::builder().text("Website").build(); + let desc = Paragraph::builder().classes(vec!["description"]).text( "This project, the website you are on, \ is made in Rust such that all the pages are generated by code.
\ That is that each html element is represented by a struct which implements the Render trait (as in render the element to html).
\ As it is right now the system is not that impressive but I believe it can do amazing things in the futur.
\
\ Wish to see more? Check out the gitea repository: ", - ); + ).build(); page.append_element_to_body(title); page.append_element_to_body(desc); } _ => {} } - let css = link::new("stylesheet", "/static/css/project.css"); - page.append_element_to_head(css); page.render() }