diff --git a/src/config.rs b/src/config.rs index 7f0dbb4..a66511e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use serde::Deserialize; use tracing::Level; +/// Application environment configuration. #[derive(Deserialize, Debug)] #[serde(rename_all = "lowercase")] pub enum Environment { @@ -8,6 +9,11 @@ pub enum Environment { Development, } +/// Main configuration struct parsed from environment variables. +/// +/// Environment variables: +/// - `ENV`: "production" or "development" (default: development) +/// - `PORT`: TCP port number (default: 3000) #[derive(Deserialize, Debug)] pub struct Configuration { #[serde(default = "default_env")] @@ -26,6 +32,10 @@ fn default_env() -> Environment { } impl Configuration { + /// Returns the socket address to bind to based on environment. + /// + /// - Production: 0.0.0.0 (all interfaces) + /// - Development: 127.0.0.1 (localhost only) pub fn socket_addr(&self) -> [u8; 4] { match self.env { Environment::Production => { @@ -49,6 +59,10 @@ impl Configuration { } } + /// Returns the appropriate log level for the environment. + /// + /// - Production: INFO + /// - Development: DEBUG pub fn log_level(&self) -> Level { match self.env { Environment::Production => Level::INFO, diff --git a/src/duration.rs b/src/duration.rs index a4a9d15..9526d7a 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -1,14 +1,22 @@ +//! Human-readable duration parsing with support for mixed time units. +//! +//! Parses strings like "1y2mon3w4d5h6m7s", "+1year", or "-3h30m" into chrono Duration objects. +//! Time units can appear in any order and use various abbreviations. + use chrono::{DateTime, Duration, Utc}; use lazy_static::lazy_static; use regex::Regex; use crate::error::TimeBannerError; +/// Extends chrono::Duration with month support using approximate calendar math. pub trait Months { fn months(count: i32) -> Self; } impl Months for Duration { + /// Creates a duration representing the given number of months. + /// Uses 365.25/12 ≈ 30.44 days per month for approximation. fn months(count: i32) -> Self { Duration::milliseconds( (Duration::days(1).num_milliseconds() as f64 * (365.25f64 / 12f64)) as i64, @@ -17,6 +25,19 @@ impl Months for Duration { } lazy_static! { + /// Regex pattern matching duration strings with flexible ordering and abbreviations. + /// + /// Supports: + /// - Optional +/- sign + /// - Years: y, yr, yrs, year, years + /// - Months: mon, month, months + /// - Weeks: w, wk, wks, week, weeks + /// - Days: d, day, days + /// - Hours: h, hr, hrs, hour, hours + /// - Minutes: m, min, mins, minute, minutes + /// - Seconds: s, sec, secs, second, seconds + /// + /// Time units must appear in descending order of magnitude, e.g. "1y2d" is valid, "1d2y" is not. static ref FULL_RELATIVE_PATTERN: Regex = Regex::new(concat!( "(?[-+])?", r"(?:(?\d+)\s?(?:years?|yrs?|y)\s*)?", @@ -30,6 +51,16 @@ lazy_static! { .unwrap(); } +/// Parses a human-readable duration string into a chrono Duration. +/// +/// Examples: +/// - `"1y2d"` → 1 year + 2 days +/// - `"+3h30m"` → +3.5 hours +/// - `"-1week"` → -7 days +/// - `"2months4days"` → ~2.03 months +/// +/// Years include leap year compensation (+6 hours per year). +/// Empty strings return zero duration. pub fn parse_duration(str: &str) -> Result { let capture = FULL_RELATIVE_PATTERN.captures(str).unwrap(); let mut value = Duration::zero(); @@ -143,10 +174,17 @@ pub fn parse_duration(str: &str) -> Result { Ok(value) } +/// Converts Unix epoch timestamp to UTC DateTime. pub fn parse_epoch_into_datetime(epoch: i64) -> Option> { DateTime::from_timestamp(epoch, 0) } +/// Parses various time value formats into a UTC datetime. +/// +/// Supports: +/// - Relative offsets: "+3600", "-1800" (seconds from now) +/// - Duration strings: "+1y2d", "-3h30m" (using duration parser) +/// - Epoch timestamps: "1752170474" (Unix timestamp) pub fn parse_time_value(raw_time: &str) -> Result, TimeBannerError> { // Handle relative time values (starting with + or -, or duration strings like "1y2d") if raw_time.starts_with('+') || raw_time.starts_with('-') { diff --git a/src/error.rs b/src/error.rs index f08e140..0183e13 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,20 +1,32 @@ use axum::{http::StatusCode, response::Json}; use serde::Serialize; +/// Application-specific errors that can occur during request processing. #[derive(Debug)] pub enum TimeBannerError { + /// Input parsing errors (invalid time formats, bad parameters, etc.) ParseError(String), + /// Template rendering failures RenderError(String), + /// SVG to PNG conversion failures RasterizeError(String), + /// 404 Not Found NotFound, } +/// JSON error response format for HTTP clients. #[derive(Serialize)] pub struct ErrorResponse { error: String, message: String, } +/// Converts application errors into standardized HTTP responses with JSON bodies. +/// +/// Returns appropriate status codes: +/// - 400 Bad Request: ParseError +/// - 500 Internal Server Error: RenderError, RasterizeError +/// - 404 Not Found: NotFound pub fn get_error_response(error: TimeBannerError) -> (StatusCode, Json) { match error { TimeBannerError::ParseError(msg) => ( diff --git a/src/raster.rs b/src/raster.rs index 2c12e6c..d4694ab 100644 --- a/src/raster.rs +++ b/src/raster.rs @@ -1,6 +1,7 @@ use resvg::usvg::fontdb; use resvg::{tiny_skia, usvg}; +/// Errors that can occur during SVG rasterization. #[derive(Debug, Clone)] pub struct RenderError { pub message: Option, @@ -21,6 +22,7 @@ pub struct Rasterizer { } impl Rasterizer { + /// Creates a new rasterizer and loads available fonts. pub fn new() -> Self { let mut fontdb = fontdb::Database::new(); fontdb.load_system_fonts(); @@ -33,6 +35,7 @@ impl Rasterizer { Self { font_db: fontdb } } + /// Converts SVG data to PNG. pub fn render(&self, svg_data: Vec) -> Result, RenderError> { let tree = { let opt = usvg::Options { diff --git a/src/render.rs b/src/render.rs index f4f5a17..e818f04 100644 --- a/src/render.rs +++ b/src/render.rs @@ -6,6 +6,7 @@ use axum::http::{header, StatusCode}; use axum::response::IntoResponse; use chrono::{DateTime, Utc}; +/// Output format for rendered time banners. #[derive(Debug, Clone)] pub enum OutputFormat { Svg, @@ -13,6 +14,7 @@ pub enum OutputFormat { } impl OutputFormat { + /// Determines output format from file extension. Defaults to SVG for unknown extensions. pub fn from_extension(ext: &str) -> Self { match ext { "png" => OutputFormat::Png, @@ -29,6 +31,7 @@ impl OutputFormat { } } + /// Returns the appropriate MIME type for HTTP responses. pub fn mime_type(&self) -> &'static str { match self { OutputFormat::Svg => "image/svg+xml", @@ -37,6 +40,7 @@ impl OutputFormat { } } +/// Converts SVG to the requested format. PNG requires rasterization. fn handle_rasterize(data: String, format: &OutputFormat) -> Result { match format { OutputFormat::Svg => Ok(Bytes::from(data)), @@ -53,6 +57,12 @@ fn handle_rasterize(data: String, format: &OutputFormat) -> Result, output_form: OutputForm, diff --git a/src/routes.rs b/src/routes.rs index 27cce47..4c7d616 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -6,12 +6,14 @@ use crate::utils::parse_path; use axum::extract::Path; use axum::response::IntoResponse; +/// Root handler - redirects to current time in relative format. pub async fn index_handler() -> impl IntoResponse { let epoch_now = chrono::Utc::now().timestamp(); axum::response::Redirect::temporary(&format!("/relative/{epoch_now}")).into_response() } +/// Handles `/relative/{time}` - displays time in relative format ("2 hours ago"). pub async fn relative_handler(Path(path): Path) -> impl IntoResponse { let (raw_time, extension) = parse_path(&path); @@ -23,6 +25,7 @@ pub async fn relative_handler(Path(path): Path) -> impl IntoResponse { render_time_response(time, OutputForm::Relative, extension).into_response() } +/// Handles `/absolute/{time}` - displays time in absolute format ("2025-01-17 14:30:00 UTC"). pub async fn absolute_handler(Path(path): Path) -> impl IntoResponse { let (raw_time, extension) = parse_path(&path); @@ -34,7 +37,7 @@ pub async fn absolute_handler(Path(path): Path) -> impl IntoResponse { render_time_response(time, OutputForm::Absolute, extension).into_response() } -// Handler for implicit absolute time (no /absolute/ prefix) +/// Handles `/{time}` - implicit absolute time display (same as absolute_handler). pub async fn implicit_handler(Path(path): Path) -> impl IntoResponse { let (raw_time, extension) = parse_path(&path); @@ -46,6 +49,7 @@ pub async fn implicit_handler(Path(path): Path) -> impl IntoResponse { render_time_response(time, OutputForm::Absolute, extension).into_response() } +/// Fallback handler for unmatched routes. pub async fn fallback_handler() -> impl IntoResponse { get_error_response(TimeBannerError::NotFound).into_response() } diff --git a/src/template.rs b/src/template.rs index 74deffa..ca554ed 100644 --- a/src/template.rs +++ b/src/template.rs @@ -6,6 +6,7 @@ use timeago::Formatter; use crate::render::OutputFormat; lazy_static! { + /// Global Tera template engine instance. static ref TEMPLATES: Tera = { let template_pattern = if cfg!(debug_assertions) { // Development: templates are in src/templates @@ -29,26 +30,35 @@ lazy_static! { }; } +/// Display format for time values. pub enum OutputForm { + /// Relative display: "2 hours ago", "in 3 days" Relative, + /// Absolute display: "2025-01-17 14:30:00 UTC" Absolute, } +/// Timezone specification formats. pub enum TzForm { Abbreviation(String), // e.g. "CST" Iso(String), // e.g. "America/Chicago" Offset(i32), // e.g. "-0600" as -21600 } +/// Context passed to template renderer containing all necessary data. pub struct RenderContext { pub value: DateTime, pub output_form: OutputForm, pub output_format: OutputFormat, + /// Target timezone (not yet implemented - defaults to UTC) pub timezone: Option, + /// Custom time format string (not yet implemented) pub format: Option, + /// Reference time for relative calculations (not yet implemented - uses current time) pub now: Option, } +/// Renders a time value using the appropriate template. pub fn render_template(context: RenderContext) -> Result { let mut template_context = Context::new(); let formatter = Formatter::new();