mirror of
https://github.com/Xevion/time-banner.git
synced 2025-12-06 11:16:41 -06:00
feat: add rendering module and integrate into routes
This commit is contained in:
@@ -12,6 +12,7 @@ mod config;
|
|||||||
mod duration;
|
mod duration;
|
||||||
mod error;
|
mod error;
|
||||||
mod raster;
|
mod raster;
|
||||||
|
mod render;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod template;
|
mod template;
|
||||||
|
|
||||||
|
|||||||
86
src/render.rs
Normal file
86
src/render.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
use crate::duration::parse_duration;
|
use crate::duration::parse_duration;
|
||||||
use crate::error::{get_error_response, TimeBannerError};
|
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::extract::Path;
|
||||||
use axum::http::{header, StatusCode};
|
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use chrono::{DateTime, FixedOffset, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use crate::raster::Rasterizer;
|
|
||||||
use crate::template::{render_template, OutputForm, RenderContext};
|
|
||||||
|
|
||||||
pub fn split_on_extension(path: &str) -> Option<(&str, &str)> {
|
pub fn split_on_extension(path: &str) -> Option<(&str, &str)> {
|
||||||
let split = path.rsplit_once('.')?;
|
let split = path.rsplit_once('.')?;
|
||||||
@@ -24,27 +21,6 @@ fn parse_path(path: &str) -> (&str, &str) {
|
|||||||
split_on_extension(path).unwrap_or((path, "svg"))
|
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>> {
|
fn parse_epoch_into_datetime(epoch: i64) -> Option<DateTime<Utc>> {
|
||||||
DateTime::from_timestamp(epoch, 0)
|
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 {
|
pub async fn index_handler() -> impl IntoResponse {
|
||||||
let epoch_now = Utc::now().timestamp();
|
let epoch_now = Utc::now().timestamp();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use chrono::{DateTime, FixedOffset, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera};
|
||||||
use timeago::Formatter;
|
use timeago::Formatter;
|
||||||
|
|
||||||
|
use crate::render::OutputFormat;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref TEMPLATES: Tera = {
|
static ref TEMPLATES: Tera = {
|
||||||
let template_pattern = if cfg!(debug_assertions) {
|
let template_pattern = if cfg!(debug_assertions) {
|
||||||
@@ -24,6 +26,7 @@ lazy_static! {
|
|||||||
::std::process::exit(1);
|
::std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_tera
|
_tera
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -33,12 +36,19 @@ pub enum OutputForm {
|
|||||||
Absolute,
|
Absolute,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RenderContext<'a> {
|
pub enum TzForm {
|
||||||
pub output_form: OutputForm,
|
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 value: DateTime<Utc>,
|
||||||
pub tz_offset: FixedOffset,
|
pub output_form: OutputForm,
|
||||||
pub tz_name: &'a str,
|
pub output_format: OutputFormat,
|
||||||
pub view: &'a str,
|
pub timezone: Option<TzForm>,
|
||||||
|
pub format: Option<String>,
|
||||||
|
pub now: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_template(context: RenderContext) -> Result<String, tera::Error> {
|
pub fn render_template(context: RenderContext) -> Result<String, tera::Error> {
|
||||||
|
|||||||
Reference in New Issue
Block a user