feat: add rendering module and integrate into routes

This commit is contained in:
2025-07-10 17:41:22 -05:00
parent 4694fd6632
commit 5afffcaf07
4 changed files with 106 additions and 68 deletions

View File

@@ -12,6 +12,7 @@ mod config;
mod duration;
mod error;
mod raster;
mod render;
mod routes;
mod template;

86
src/render.rs Normal file
View File

@@ -0,0 +1,86 @@
use crate::error::{get_error_response, TimeBannerError};
use crate::raster::Rasterizer;
use crate::template::{render_template, OutputForm, RenderContext};
use axum::body::Bytes;
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub enum OutputFormat {
Svg,
Png,
}
impl OutputFormat {
pub fn from_extension(ext: &str) -> Self {
match ext {
"png" => OutputFormat::Png,
_ => OutputFormat::Svg, // Default to SVG
}
}
pub fn mime_type(&self) -> &'static str {
match self {
OutputFormat::Svg => "image/svg+xml",
OutputFormat::Png => "image/png",
}
}
}
fn handle_rasterize(data: String, format: &OutputFormat) -> Result<Bytes, TimeBannerError> {
match format {
OutputFormat::Svg => Ok(Bytes::from(data)),
OutputFormat::Png => {
let renderer = Rasterizer::new();
let raw_image = renderer.render(data.into_bytes());
if let Err(err) = raw_image {
return Err(TimeBannerError::RasterizeError(
err.message.unwrap_or_else(|| "Unknown error".to_string()),
));
}
Ok(Bytes::from(raw_image.unwrap()))
}
}
}
pub fn render_time_response(
time: DateTime<Utc>,
output_form: OutputForm,
extension: &str,
) -> impl IntoResponse {
let output_format = OutputFormat::from_extension(extension);
// Build context for rendering
let context = RenderContext {
value: time,
output_form,
output_format: output_format.clone(),
timezone: None, // Default to UTC for now
format: None, // Use default format
now: None, // Use current time
};
// Render template
let rendered_template = match render_template(context) {
Ok(template) => template,
Err(e) => {
return get_error_response(TimeBannerError::RenderError(format!(
"Template rendering failed: {}",
e
)))
.into_response()
}
};
// Handle rasterization
match handle_rasterize(rendered_template, &output_format) {
Ok(bytes) => (
StatusCode::OK,
[(header::CONTENT_TYPE, output_format.mime_type())],
bytes,
)
.into_response(),
Err(e) => get_error_response(e).into_response(),
}
}

View File

@@ -1,13 +1,10 @@
use crate::duration::parse_duration;
use crate::error::{get_error_response, TimeBannerError};
use axum::body::Bytes;
use crate::render::render_time_response;
use crate::template::OutputForm;
use axum::extract::Path;
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use chrono::{DateTime, FixedOffset, Utc};
use crate::raster::Rasterizer;
use crate::template::{render_template, OutputForm, RenderContext};
use chrono::{DateTime, Utc};
pub fn split_on_extension(path: &str) -> Option<(&str, &str)> {
let split = path.rsplit_once('.')?;
@@ -24,27 +21,6 @@ fn parse_path(path: &str) -> (&str, &str) {
split_on_extension(path).unwrap_or((path, "svg"))
}
fn handle_rasterize(data: String, extension: &str) -> Result<(&str, Bytes), TimeBannerError> {
match extension {
"svg" => Ok(("image/svg+xml", Bytes::from(data))),
"png" => {
let renderer = Rasterizer::new();
let raw_image = renderer.render(data.into_bytes());
if let Err(err) = raw_image {
return Err(TimeBannerError::RasterizeError(
err.message.unwrap_or_else(|| "Unknown error".to_string()),
));
}
Ok(("image/png", Bytes::from(raw_image.unwrap())))
}
_ => Err(TimeBannerError::RasterizeError(format!(
"Unsupported extension: {}",
extension
))),
}
}
fn parse_epoch_into_datetime(epoch: i64) -> Option<DateTime<Utc>> {
DateTime::from_timestamp(epoch, 0)
}
@@ -82,41 +58,6 @@ fn parse_time_value(raw_time: &str) -> Result<DateTime<Utc>, TimeBannerError> {
)))
}
fn render_time_response(
time: DateTime<Utc>,
output_form: OutputForm,
extension: &str,
) -> impl IntoResponse {
// Build context for rendering
let context = RenderContext {
output_form,
value: time,
tz_offset: FixedOffset::east_opt(0).unwrap(), // UTC offset
tz_name: "UTC",
view: "basic",
};
// Render template
let rendered_template = match render_template(context) {
Ok(template) => template,
Err(e) => {
return get_error_response(TimeBannerError::RenderError(format!(
"Template rendering failed: {}",
e
)))
.into_response()
}
};
// Handle rasterization
match handle_rasterize(rendered_template, extension) {
Ok((mime_type, bytes)) => {
(StatusCode::OK, [(header::CONTENT_TYPE, mime_type)], bytes).into_response()
}
Err(e) => get_error_response(e).into_response(),
}
}
pub async fn index_handler() -> impl IntoResponse {
let epoch_now = Utc::now().timestamp();

View File

@@ -1,8 +1,10 @@
use chrono::{DateTime, FixedOffset, Utc};
use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
use tera::{Context, Tera};
use timeago::Formatter;
use crate::render::OutputFormat;
lazy_static! {
static ref TEMPLATES: Tera = {
let template_pattern = if cfg!(debug_assertions) {
@@ -24,6 +26,7 @@ lazy_static! {
::std::process::exit(1);
}
};
_tera
};
}
@@ -33,12 +36,19 @@ pub enum OutputForm {
Absolute,
}
pub struct RenderContext<'a> {
pub output_form: OutputForm,
pub enum TzForm {
Abbreviation(String), // e.g. "CST"
Iso(String), // e.g. "America/Chicago"
Offset(i32), // e.g. "-0600" as -21600
}
pub struct RenderContext {
pub value: DateTime<Utc>,
pub tz_offset: FixedOffset,
pub tz_name: &'a str,
pub view: &'a str,
pub output_form: OutputForm,
pub output_format: OutputFormat,
pub timezone: Option<TzForm>,
pub format: Option<String>,
pub now: Option<i64>,
}
pub fn render_template(context: RenderContext) -> Result<String, tera::Error> {