mirror of
https://github.com/Xevion/time-banner.git
synced 2025-12-06 01:16:36 -06:00
feat: re-implement route rendering, use duration parsing, absolute timestamps
This commit is contained in:
132
src/routes.rs
132
src/routes.rs
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user