feat: re-implement route rendering, use duration parsing, absolute timestamps

This commit is contained in:
2025-07-10 17:27:20 -05:00
parent 279dc043d4
commit 4694fd6632

View File

@@ -1,10 +1,13 @@
use crate::duration::parse_duration;
use crate::error::{get_error_response, TimeBannerError}; use crate::error::{get_error_response, TimeBannerError};
use axum::body::Bytes; use axum::body::Bytes;
use axum::extract::Path; use axum::extract::Path;
use axum::response::{IntoResponse, Redirect}; use axum::http::{header, StatusCode};
use chrono::{DateTime, Utc}; use axum::response::IntoResponse;
use chrono::{DateTime, FixedOffset, Utc};
use crate::raster::Rasterizer; 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('.')?;
@@ -33,7 +36,7 @@ fn handle_rasterize(data: String, extension: &str) -> Result<(&str, Bytes), Time
)); ));
} }
Ok(("image/x-png", Bytes::from(raw_image.unwrap()))) Ok(("image/png", Bytes::from(raw_image.unwrap())))
} }
_ => Err(TimeBannerError::RasterizeError(format!( _ => Err(TimeBannerError::RasterizeError(format!(
"Unsupported extension: {}", "Unsupported extension: {}",
@@ -42,35 +45,118 @@ fn handle_rasterize(data: String, extension: &str) -> Result<(&str, Bytes), Time
} }
} }
fn parse_epoch_into_datetime(epoch: i64) -> Option<DateTime<Utc>> {
DateTime::from_timestamp(epoch, 0)
}
fn parse_time_value(raw_time: &str) -> Result<DateTime<Utc>, TimeBannerError> {
// Handle relative time values (starting with + or -, or duration strings like "1y2d")
if raw_time.starts_with('+') || raw_time.starts_with('-') {
let now = Utc::now();
// Try parsing as simple offset seconds first
if let Ok(offset_seconds) = raw_time.parse::<i64>() {
return Ok(now + chrono::Duration::seconds(offset_seconds));
}
// Try parsing as duration string (e.g., "+1y2d", "-3h30m")
if let Ok(duration) = parse_duration(raw_time) {
return Ok(now + duration);
}
return Err(TimeBannerError::ParseError(format!(
"Could not parse relative time: {}",
raw_time
)));
}
// Try to parse as epoch timestamp
if let Ok(epoch) = raw_time.parse::<i64>() {
return parse_epoch_into_datetime(epoch)
.ok_or_else(|| TimeBannerError::ParseError("Invalid timestamp".to_string()));
}
Err(TimeBannerError::ParseError(format!(
"Could not parse time value: {}",
raw_time
)))
}
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();
Redirect::temporary(&format!("/relative/{epoch_now}")).into_response() axum::response::Redirect::temporary(&format!("/relative/{epoch_now}")).into_response()
} }
pub async fn relative_handler(Path(path): Path<String>) -> impl IntoResponse { pub async fn relative_handler(Path(path): Path<String>) -> impl IntoResponse {
let (_raw_time, _extension) = parse_path(path.as_str()); let (raw_time, extension) = parse_path(&path);
get_error_response(TimeBannerError::NotFound).into_response() let time = match parse_time_value(raw_time) {
Ok(t) => t,
Err(e) => return get_error_response(e).into_response(),
};
render_time_response(time, OutputForm::Relative, extension).into_response()
}
pub async fn absolute_handler(Path(path): Path<String>) -> impl IntoResponse {
let (raw_time, extension) = parse_path(&path);
let time = match parse_time_value(raw_time) {
Ok(t) => t,
Err(e) => return get_error_response(e).into_response(),
};
render_time_response(time, OutputForm::Absolute, extension).into_response()
}
// Handler for implicit absolute time (no /absolute/ prefix)
pub async fn implicit_handler(Path(path): Path<String>) -> impl IntoResponse {
let (raw_time, extension) = parse_path(&path);
let time = match parse_time_value(raw_time) {
Ok(t) => t,
Err(e) => return get_error_response(e).into_response(),
};
render_time_response(time, OutputForm::Absolute, extension).into_response()
} }
pub async fn fallback_handler() -> impl IntoResponse { pub async fn fallback_handler() -> impl IntoResponse {
get_error_response(TimeBannerError::NotFound).into_response() get_error_response(TimeBannerError::NotFound).into_response()
} }
pub async fn absolute_handler(Path(path): Path<String>) -> impl IntoResponse {
let (_raw_time, _extension) = parse_path(path.as_str());
get_error_response(TimeBannerError::NotFound).into_response()
}
// basic handler that responds with a static string
pub async fn implicit_handler(Path(path): Path<String>) -> impl IntoResponse {
let (_raw_time, _extension) = parse_path(path.as_str());
get_error_response(TimeBannerError::NotFound).into_response()
}
fn parse_epoch_into_datetime(epoch: i64) -> Option<DateTime<Utc>> {
DateTime::from_timestamp(epoch, 0)
}