diff --git a/src/main.rs b/src/main.rs index 543c89b..baf893c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod config; mod duration; mod error; mod raster; +mod render; mod routes; mod template; diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..bfbafa6 --- /dev/null +++ b/src/render.rs @@ -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 { + 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, + 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(), + } +} diff --git a/src/routes.rs b/src/routes.rs index 7dec278..57c435f 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -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::from_timestamp(epoch, 0) } @@ -82,41 +58,6 @@ fn parse_time_value(raw_time: &str) -> Result, TimeBannerError> { ))) } -fn render_time_response( - time: DateTime, - 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(); diff --git a/src/template.rs b/src/template.rs index d492947..bd9b3cc 100644 --- a/src/template.rs +++ b/src/template.rs @@ -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, - pub tz_offset: FixedOffset, - pub tz_name: &'a str, - pub view: &'a str, + pub output_form: OutputForm, + pub output_format: OutputFormat, + pub timezone: Option, + pub format: Option, + pub now: Option, } pub fn render_template(context: RenderContext) -> Result {