diff options
| author | gennyble <gen@nyble.dev> | 2026-09-21 14:23:28 -0500 |
|---|---|---|
| committer | gennyble <gen@nyble.dev> | 2026-09-21 14:23:28 -0500 |
| commit | de0e60603d3e470befad49f2ec59778b5d1b5d59 (patch) | |
| tree | 861edd0aaa1745e97cca926262e0eb73448da83c | |
| parent | be872aa0cd61ea16b95eddcd68c6a72f0fe3af96 (diff) | |
splorm, i wake and breathe
| -rw-r--r-- | splorm/Cargo.toml | 21 | ||||
| -rw-r--r-- | splorm/README.md | 2 | ||||
| -rw-r--r-- | splorm/TEMPLATES.md | 42 | ||||
| -rw-r--r-- | splorm/splorm.conf | 6 | ||||
| -rw-r--r-- | splorm/src/config.rs | 83 | ||||
| -rw-r--r-- | splorm/src/fs/mod.rs | 28 | ||||
| -rw-r--r-- | splorm/src/fs/request_path.rs | 171 | ||||
| -rw-r--r-- | splorm/src/handler/mod.rs | 25 | ||||
| -rw-r--r-- | splorm/src/handler/staticfile.rs | 52 | ||||
| -rw-r--r-- | splorm/src/handler/templated.rs | 131 | ||||
| -rw-r--r-- | splorm/src/main.rs | 39 | ||||
| -rw-r--r-- | splorm/src/runtime_error.rs | 190 | ||||
| -rw-r--r-- | splorm/src/state.rs | 2 |
13 files changed, 792 insertions, 0 deletions
diff --git a/splorm/Cargo.toml b/splorm/Cargo.toml new file mode 100644 index 0000000..d5f7f34 --- /dev/null +++ b/splorm/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "splorm" +version = "0.1.0" +edition = "2024" + +[dependencies.tokio] +workspace = true +features = ["net", "rt-multi-thread", "fs"] + +[dependencies] +# Here for it's ReaderStream which we use while streaming files back to the browser +tokio-util = { version = "0.7.10", features = ["io"] } + +camino.workspace = true +confindent.workspace = true +bempline.workspace = true +axum.workspace = true + +# subcrates +paige.workspace = true +timeparse.workspace = true diff --git a/splorm/README.md b/splorm/README.md new file mode 100644 index 0000000..8a0af2f --- /dev/null +++ b/splorm/README.md @@ -0,0 +1,2 @@ +**splorm**, i wake and breathe +*(an after-awake)* diff --git a/splorm/TEMPLATES.md b/splorm/TEMPLATES.md new file mode 100644 index 0000000..9fcde82 --- /dev/null +++ b/splorm/TEMPLATES.md @@ -0,0 +1,42 @@ +what variables are available in templates? + +**description** (default: none) +description of the page for OpenGraph tags + +**title** (default: filename) +the `<title>` of the page + +⁂ + +**skip-path** (default: none) +number of path components to remove while displaying the **`path`** pattern. + +### patterns +**pathnav** +creates a collection of navigation links linking to directories under the page. + +template variables: +- `pathnav_link`: link to navigate to +- `pathnav_name`: displayed name of the link + +frontmatter: +- `skip-path`: how many immediate parents to remove from the path + +**published** +inserts an html element showing the time a page was published + +template variables: +- `published_human`: time formatted for a human to read +- `published_machine`: time formatted for a machine to read + +frontmatter: +- `published`: time page was published + +**styles** +inserts `<link rel="stylesheet">` into the head of the document. + +template variables: +- `style`: path to a stylesheet + +frontmatter: +- `style`: path to a stylesheet diff --git a/splorm/splorm.conf b/splorm/splorm.conf new file mode 100644 index 0000000..b076d6e --- /dev/null +++ b/splorm/splorm.conf @@ -0,0 +1,6 @@ +Network + Port 25728 + +Serving /Users/gen/src/inf/served + Templates /Users/gen/src/inf/templates + Default minimal diff --git a/splorm/src/config.rs b/splorm/src/config.rs new file mode 100644 index 0000000..79eb115 --- /dev/null +++ b/splorm/src/config.rs @@ -0,0 +1,83 @@ +use std::net::{SocketAddr, ToSocketAddrs}; + +use camino::{Utf8Path, Utf8PathBuf}; +use confindent::Confindent; + +use crate::fs::{RequestPath, SafePathBuf}; + +pub struct Config { + port: u16, + settings: Settings, +} + +impl Config { + pub fn get(path: &Utf8Path) -> Config { + let confi = Confindent::from_file(path).unwrap(); + + let network = confi.child("Network").unwrap(); + let port = network.child_parse("Port").unwrap(); + + let serving = confi.child("Serving").unwrap(); + let webroot: Utf8PathBuf = serving.parse().unwrap(); + + let templates_sec = serving.child("Templates").unwrap(); + let templates: Utf8PathBuf = templates_sec.parse().unwrap(); + let default_template = templates_sec.child_owned("Default").unwrap(); + + Self { + port, + settings: Settings { + default_template, + template_dir: templates, // webroot.join(templates), + + webroot, + }, + } + } + + pub fn listen_address(&self) -> SocketAddr { + ("0.0.0.0", self.port) + .to_socket_addrs() + .unwrap() + .next() + .unwrap() + } + + pub fn settings(&self) -> &Settings { + &self.settings + } +} + +/// Settings are values that need to live for a long time. They enter into +/// SplormState so we can reference them later +#[derive(Clone, Debug)] +pub struct Settings { + webroot: Utf8PathBuf, + + default_template: String, + template_dir: Utf8PathBuf, +} + +impl Settings { + pub fn root(&self) -> &Utf8Path { + &self.webroot + } + + pub fn template_dir(&self) -> &Utf8Path { + &self.template_dir + } + + pub fn default_template_name(&self) -> &str { + &self.default_template + } + + pub fn default_template_path(&self) -> SafePathBuf { + RequestPath::verify_safe( + self.template_dir(), + &format!("{}.html", self.default_template), + ) + .unwrap() + .system_path() + .to_owned() + } +} diff --git a/splorm/src/fs/mod.rs b/splorm/src/fs/mod.rs new file mode 100644 index 0000000..032aaed --- /dev/null +++ b/splorm/src/fs/mod.rs @@ -0,0 +1,28 @@ +mod request_path; + +pub use request_path::{Mime, Mimetype, RequestPath, SafePath, SafePathBuf}; +use tokio::fs::File; + +use crate::RuntimeError; + +pub async fn open_file<'p>(path: SafePath<'p>) -> Result<File, RuntimeError> { + File::open(*path) + .await + .map_err(|err| RuntimeError::file_error(err, path.to_path_buf())) +} + +pub async fn read_file<'p>(path: SafePath<'p>) -> Result<Vec<u8>, RuntimeError> { + let system_path = *path; + match tokio::fs::read(system_path).await { + Err(err) => Err(RuntimeError::file_error(err, system_path.to_path_buf())), + Ok(vec) => Ok(vec), + } +} + +pub async fn read_file_to_string<'p>(path: SafePath<'p>) -> Result<String, RuntimeError> { + let system_path = *path; + match tokio::fs::read_to_string(system_path).await { + Err(err) => Err(RuntimeError::file_error(err, system_path.to_path_buf())), + Ok(str) => Ok(str), + } +} diff --git a/splorm/src/fs/request_path.rs b/splorm/src/fs/request_path.rs new file mode 100644 index 0000000..d065808 --- /dev/null +++ b/splorm/src/fs/request_path.rs @@ -0,0 +1,171 @@ +use std::{fs::Metadata, ops::Deref}; + +use camino::{Utf8Path, Utf8PathBuf}; + +use crate::RuntimeError; + +#[derive(Clone, Debug, PartialEq)] +pub struct RequestPath { + request_path: String, + system_path: Utf8PathBuf, +} + +impl RequestPath { + /// Use the `root` to verify a received `path` is safe to access and serve. + /// This function will canonicalize it and return a `RequestPath` if the file + /// exists and is within the root directory. + pub fn verify_safe(root: &Utf8Path, path: &str) -> Result<Self, RuntimeError> { + let system_unsafe_path = root.join(path); + match system_unsafe_path.canonicalize_utf8() { + Ok(canon) => match canon.strip_prefix(root) { + Err(_e) => Err(RuntimeError::outside_root(path.to_owned(), canon)), + Ok(path) => Ok(RequestPath { + request_path: path.to_string(), + system_path: Self::file_or_dirfile(canon)?, + }), + }, + Err(_io) => Err(RuntimeError::not_found( + path.to_string(), + system_unsafe_path, + )), + } + } + + fn file_or_dirfile(path: Utf8PathBuf) -> Result<Utf8PathBuf, RuntimeError> { + let meta = path + .metadata() + .map_err(|e| RuntimeError::file_error(e, path.clone()))?; + + if meta.is_dir() { + let name = path.file_stem().unwrap(); + let dirfile = path.join(format!("{name}.html")); + + if dirfile.exists() { + return Ok(dirfile); + } + } + + Ok(path) + } + + /// The web path from the request, after canonicalization. + pub fn web_path(&self) -> &str { + &self.request_path + } + + /// A verified-safe system path. + pub fn system_path<'p>(&'p self) -> SafePath<'p> { + SafePath(self.system_path.as_path()) + } + + pub fn guess_mime(&self) -> Mime { + let stem = self.system_path.file_stem().unwrap_or_default(); + let ext = self.system_path.extension().unwrap_or_default(); + + match ext { + // Text + "css" => "text/css".as_mime(Mimetype::Text), + "html" => "text/html".as_mime(Mimetype::Text), + "js" => "text/javascript".as_mime(Mimetype::Text), + "txt" => "text/plain".as_mime(Mimetype::Text), + "xml" if stem.ends_with("atom") => "application/atom+xml".as_mime(Mimetype::Text), + "xml" => "application/xml".as_mime(Mimetype::Text), + + // Multimedia + "gif" => "image/gif".as_mime(Mimetype::Media), + "jpg" | "jpeg" => "image/jpeg".as_mime(Mimetype::Media), + "mp4" => "video/mp4".as_mime(Mimetype::Media), + "png" => "image/png".as_mime(Mimetype::Media), + _ => "".as_mime(Mimetype::Media), + } + } +} + +/// A verified-safe system path. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct SafePath<'p>(&'p Utf8Path); + +impl<'p> SafePath<'p> { + pub fn to_owned(&self) -> SafePathBuf { + SafePathBuf(self.0.to_path_buf()) + } + + pub fn metadata(&self) -> Result<Metadata, RuntimeError> { + self.0 + .metadata() + .map_err(|err| RuntimeError::file_error(err, self.0.to_path_buf())) + } +} + +impl<'p> Deref for SafePath<'p> { + type Target = &'p Utf8Path; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'p> AsRef<Utf8Path> for SafePath<'p> { + fn as_ref(&self) -> &Utf8Path { + self.0 + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SafePathBuf(Utf8PathBuf); + +impl SafePathBuf { + pub fn as_path<'p>(&'p self) -> SafePath<'p> { + SafePath(&self.0) + } +} + +impl Deref for SafePathBuf { + type Target = Utf8Path; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<Utf8PathBuf> for SafePathBuf { + fn as_ref(&self) -> &Utf8PathBuf { + &self.0 + } +} + +pub struct Mime { + string: &'static str, + mimetype: Mimetype, +} + +impl Mime { + pub fn mimetype(&self) -> Mimetype { + self.mimetype + } +} + +impl AsRef<str> for Mime { + fn as_ref(&self) -> &str { + self.string + } +} + +pub trait AsMime { + fn as_mime(self, mimetype: Mimetype) -> Mime; +} + +impl AsMime for &'static str { + fn as_mime(self, mimetype: Mimetype) -> Mime { + Mime { + string: self, + mimetype, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Mimetype { + Text, + Media, +} diff --git a/splorm/src/handler/mod.rs b/splorm/src/handler/mod.rs new file mode 100644 index 0000000..0089c48 --- /dev/null +++ b/splorm/src/handler/mod.rs @@ -0,0 +1,25 @@ +use axum::{Extension, extract::Path, response::Response}; + +mod staticfile; +mod templated; + +use crate::{RuntimeError, config::Settings, fs::RequestPath}; + +pub async fn index(Extension(settings): Extension<Settings>) -> Result<Response, RuntimeError> { + println!("index!"); + let rqp = RequestPath::verify_safe(settings.root(), "home.html")?; + templated::send_file(rqp, &settings).await +} + +pub async fn static_path( + Extension(settings): Extension<Settings>, + Path(path): Path<String>, +) -> Result<Response, RuntimeError> { + println!("{path}"); + let rqp = RequestPath::verify_safe(settings.root(), &path)?; + if rqp.guess_mime().as_ref() == "text/html" { + templated::send_file(rqp, &settings).await + } else { + staticfile::send_or_stream_file(rqp).await + } +} diff --git a/splorm/src/handler/staticfile.rs b/splorm/src/handler/staticfile.rs new file mode 100644 index 0000000..8f26251 --- /dev/null +++ b/splorm/src/handler/staticfile.rs @@ -0,0 +1,52 @@ +use axum::{body::Body, response::Response}; +use tokio_util::io::ReaderStream; + +use crate::{ + RuntimeError, + fs::{self, Mimetype, RequestPath}, +}; + +const STREAM_AFTER: u64 = 1024 * 1024 * 5; +pub async fn send_or_stream_file(path: RequestPath) -> Result<Response, RuntimeError> { + let meta = path.system_path().metadata()?; + + if meta.len() > STREAM_AFTER { + stream_file(path).await + } else { + send_file(path).await + } +} + +pub async fn send_file(path: RequestPath) -> Result<Response, RuntimeError> { + let mime = path.guess_mime(); + + let body = match mime.mimetype() { + Mimetype::Text => { + let text = fs::read_file_to_string(path.system_path()).await?; + Body::from(text) + } + Mimetype::Media => { + let data = fs::read_file(path.system_path()).await?; + Body::from(data) + } + }; + + Response::builder() + .status(200) + .header("Content-Type", mime.as_ref()) + .body(body) + .map_err(|_| RuntimeError::unknown()) +} + +pub async fn stream_file(path: RequestPath) -> Result<Response, RuntimeError> { + let mime = path.guess_mime(); + + let file = fs::open_file(path.system_path()).await?; + let stream = ReaderStream::new(file); + + Response::builder() + .status(200) + .header("Content-Type", mime.as_ref()) + .body(Body::from_stream(stream)) + .map_err(|_| RuntimeError::unknown()) +} diff --git a/splorm/src/handler/templated.rs b/splorm/src/handler/templated.rs new file mode 100644 index 0000000..596210d --- /dev/null +++ b/splorm/src/handler/templated.rs @@ -0,0 +1,131 @@ +use std::str::FromStr; + +use axum::{body::Body, response::Response}; +use bempline::{Document, Options, options::IncludeMethod, variables}; +use paige::{Frontmatter, Templated}; +use timeparse::OffsetDateTime; + +use crate::{ + RuntimeError, + config::Settings, + fs::{self, RequestPath, SafePath, SafePathBuf}, +}; + +pub async fn send_file(path: RequestPath, settings: &Settings) -> Result<Response, RuntimeError> { + let safe = path.system_path().to_owned(); + let string = fs::read_file_to_string(path.system_path()).await?; + let template = Templated::from_str(&string) + .map_err(|_| RuntimeError::not_a_template(path.system_path().to_path_buf()))?; + let tvars = TemplateVariables::build(safe.as_path(), &template.frontmatter); + + let pt_path = match tvars.template { + Some(name) => { + let path = RequestPath::verify_safe(settings.template_dir(), &format!("{name}.html"))?; + path.system_path().to_owned() + } + None => settings.default_template_path(), + }; + + let mut page_template = get_template(pt_path).await?; + tvars.apply_title(&mut page_template); + tvars.apply_styles(&mut page_template); + tvars.apply_published(&mut page_template); + tvars.apply_pathnav(&mut page_template, path); + page_template.set("main", template.content); + + Response::builder() + .status(200) + .header("content-type", "text/html") + .body(Body::from(page_template.compile())) + .map_err(|_| RuntimeError::Unknown) +} + +async fn get_template(path: SafePathBuf) -> Result<Document, RuntimeError> { + let options = Options::new().include_path(IncludeMethod::Path(path.as_std_path().to_owned())); + let str = fs::read_file_to_string(path.as_path()).await?; + + Document::from_str(str, options) + .map_err(|perr| RuntimeError::template_parse_error(perr, path.to_path_buf())) +} + +pub struct TemplateVariables<'fm> { + template: Option<&'fm str>, + + style: Vec<&'fm str>, + + title: &'fm str, + description: Option<&'fm str>, + published: Option<OffsetDateTime>, + + skip_path: Option<usize>, +} + +impl<'fm> TemplateVariables<'fm> { + pub fn build(safe: SafePath<'fm>, fm: &'fm Frontmatter) -> Self { + let filename = safe.file_name().unwrap_or("unknown"); + + Self { + template: fm.get("template"), + title: fm.get("title").unwrap_or_else(|| filename), + style: fm.get_many("style"), + description: fm.get("description"), + published: fm + .get("published") + .map(|raw| timeparse::parse(raw).ok()) + .flatten(), + skip_path: fm.get("skip-path").map(|s| s.parse().ok()).flatten(), + } + } + + pub fn apply_styles(&self, doc: &mut Document) { + if let Some(styles) = doc.get_pattern("styles") { + self.style.iter().for_each(|style| { + let mut cloned = styles.clone(); + cloned.set("style", style); + doc.set_pattern(cloned); + }); + } + } + + pub fn apply_title(&self, doc: &mut Document) { + doc.set("title", self.title); + } + + pub fn apply_published(&self, doc: &mut Document) { + if let Some(mut published_pattern) = doc.get_pattern("published") { + if let Some(published) = self.published.as_ref() { + let published_human = timeparse::format::human_long(published); + let published_machine = timeparse::format::iso8601(published); + + variables!(published_pattern, published_human, published_machine); + doc.set_pattern(published_pattern); + } + } + } + + pub fn apply_pathnav(&self, doc: &mut Document, path: RequestPath) { + let webpath = path.web_path(); + + let mut components: Vec<&str> = webpath.split('/').collect(); + // we don't want the name itself + components.pop(); + + if let Some(path_pattern) = doc.get_pattern("pathnav") { + let mut home = path_pattern.clone(); + home.set("pathnav_link", "/"); + home.set("pathnav_name", "home"); + doc.set_pattern(home); + + let count = components.len().saturating_sub(self.skip_path.unwrap_or(0)); + let mut link = String::from("/"); + for part in components.iter().take(count) { + link.push_str(&format!("{part}/")); + + let mut pat = path_pattern.clone(); + pat.set("pathnav_link", &link); + pat.set("pathnav_name", part); + doc.set_pattern(pat); + } + } + } +} diff --git a/splorm/src/main.rs b/splorm/src/main.rs new file mode 100644 index 0000000..d35d1dc --- /dev/null +++ b/splorm/src/main.rs @@ -0,0 +1,39 @@ +use axum::{Extension, Router, routing::get}; +use camino::Utf8PathBuf; +use tokio::{net::TcpListener, runtime::Builder}; + +use crate::{config::Config, state::SplormState}; + +mod config; +pub mod fs; +mod handler; +mod runtime_error; +mod state; + +pub use runtime_error::{InvalidPathReason, RuntimeError}; + +fn main() { + let config = Utf8PathBuf::from("splorm.conf"); + let conf = Config::get(&config); + + // Create and spawn the tokio multithreaded runtime. Execution stays + // here until async_main() returns. + let rt = Builder::new_multi_thread().enable_all().build().unwrap(); + rt.block_on(async_main(conf)) +} + +async fn async_main(conf: Config) { + // Setup the routing + let bare_routes = Router::new() + .route("/", get(handler::index)) + .route("/{*path}", get(handler::static_path)); + + // Apply Extensions and state + let routes = bare_routes + .layer(Extension(conf.settings().clone())) + .with_state(SplormState {}); + + // Create our listener and start axum serving and routing + let listener = TcpListener::bind(conf.listen_address()).await.unwrap(); + axum::serve(listener, routes).await.unwrap(); +} diff --git a/splorm/src/runtime_error.rs b/splorm/src/runtime_error.rs new file mode 100644 index 0000000..99aca45 --- /dev/null +++ b/splorm/src/runtime_error.rs @@ -0,0 +1,190 @@ +use std::{fmt, io::ErrorKind}; + +use axum::{ + body::Body, + response::{IntoResponse, Response}, +}; +use camino::Utf8PathBuf; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum InvalidPathReason { + OutsideRoot, + NotFound, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum FileErrorReason { + PermissionDenied, + NotFound, + Unknown, +} + +impl fmt::Display for FileErrorReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotFound => write!(f, "file not found"), + Self::PermissionDenied => write!(f, "permission denied"), + Self::Unknown => write!(f, "unknown file error"), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum TemplateErrorReason { + NotATemplate, + ParseError, +} + +#[derive(Debug)] +pub enum RuntimeError { + InvalidPath { + reason: InvalidPathReason, + request_path: String, + system_path: Utf8PathBuf, + }, + FileError { + reason: FileErrorReason, + source: std::io::Error, + system_path: Utf8PathBuf, + }, + TemplateError { + reason: TemplateErrorReason, + source: Option<bempline::ParseError>, + system_path: Utf8PathBuf, + }, + Unknown, +} + +impl RuntimeError { + pub fn outside_root(request_path: String, system_path: Utf8PathBuf) -> Self { + Self::InvalidPath { + reason: InvalidPathReason::OutsideRoot, + request_path, + system_path, + } + } + + pub fn not_found(request_path: String, system_path: Utf8PathBuf) -> Self { + Self::InvalidPath { + reason: InvalidPathReason::NotFound, + request_path, + system_path, + } + } + + pub fn file_error(err: std::io::Error, system_path: Utf8PathBuf) -> Self { + let reason = match err.kind() { + ErrorKind::PermissionDenied => FileErrorReason::PermissionDenied, + ErrorKind::NotFound => FileErrorReason::NotFound, + _ => FileErrorReason::Unknown, + }; + + Self::FileError { + reason, + source: err, + system_path, + } + } + + pub fn not_a_template(system_path: Utf8PathBuf) -> Self { + Self::TemplateError { + reason: TemplateErrorReason::NotATemplate, + source: None, + system_path, + } + } + + pub fn template_parse_error(err: bempline::ParseError, system_path: Utf8PathBuf) -> Self { + Self::TemplateError { + reason: TemplateErrorReason::ParseError, + source: Some(err), + system_path, + } + } + + pub fn unknown() -> Self { + RuntimeError::Unknown + } + + pub fn status_code(&self) -> u16 { + match self { + Self::InvalidPath { .. } => 404, + Self::FileError { reason, .. } => match reason { + FileErrorReason::NotFound => 404, + FileErrorReason::PermissionDenied => 404, + FileErrorReason::Unknown => 500, + }, + Self::TemplateError { .. } => 500, + Self::Unknown => 500, + } + } +} + +struct PresentableRuntimeError(RuntimeError); +impl fmt::Display for PresentableRuntimeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + macro_rules! estr { + (404) => { + write!(f, "the requested file could not be found") + }; + (404 $webpath:ident) => { + write!(f, "file not found: '{}'", $webpath) + }; + (500) => { + write!(f, "an unknown error occured!") + }; + } + + match &self.0 { + RuntimeError::InvalidPath { + request_path, + reason, + system_path, + } => { + println!("invalid_path = {request_path} ({system_path}) // {reason:?}"); + estr!(404 request_path) + } + RuntimeError::FileError { + reason, + system_path, + .. + } => match reason { + FileErrorReason::NotFound | FileErrorReason::PermissionDenied => { + println!("404 = {system_path}"); + estr!(404) + } + FileErrorReason::Unknown => estr!(500), + }, + RuntimeError::TemplateError { .. } => estr!(500), + RuntimeError::Unknown => estr!(500), + } + } +} + +impl IntoResponse for RuntimeError { + fn into_response(self) -> Response { + let status = self.status_code(); + let presentable = PresentableRuntimeError(self); + make_basic_error(status, presentable) + } +} + +fn make_basic_error<S: std::fmt::Display>(status: u16, msg: S) -> Response { + let error_str = format!( + "<html>\n\ + \t<head><title>{status}</title></head>\n\ + \t<body style='width: 20rem; padding: 0px; margin: 2rem;'>\n\ + \t\t<h1>{status}</h1>\n\ + \t\t<hr/>\n\ + \t\t<p>{msg}</p>\n\ + \t</body>\n\ + </html>" + ); + + Response::builder() + .status(status) + .header("content-type", "text/html") + .body(Body::from(error_str)) + .unwrap() + .into_response() +} diff --git a/splorm/src/state.rs b/splorm/src/state.rs new file mode 100644 index 0000000..12ef5d9 --- /dev/null +++ b/splorm/src/state.rs @@ -0,0 +1,2 @@ +#[derive(Clone, Debug)] +pub struct SplormState {} |
