diff --git a/src/core/time.rs b/src/core/time.rs index 376ea2d..9f59601 100644 --- a/src/core/time.rs +++ b/src/core/time.rs @@ -59,6 +59,22 @@ pub fn parse_since(input: &str) -> Option { iso_to_ms(input) } +/// Convert ISO 8601 timestamp to milliseconds with strict error handling. +/// Returns Err with a descriptive message if the timestamp is invalid. +pub fn iso_to_ms_strict(iso_string: &str) -> Result { + DateTime::parse_from_rfc3339(iso_string) + .map(|dt| dt.timestamp_millis()) + .map_err(|_| format!("Invalid timestamp: {}", iso_string)) +} + +/// Convert optional ISO 8601 timestamp to optional milliseconds (strict). +pub fn iso_to_ms_opt_strict(iso_string: &Option) -> Result, String> { + match iso_string { + Some(s) => iso_to_ms_strict(s).map(Some), + None => Ok(None), + } +} + /// Format milliseconds epoch to human-readable full datetime. pub fn format_full_datetime(ms: i64) -> String { DateTime::from_timestamp_millis(ms) diff --git a/src/gitlab/transformers/discussion.rs b/src/gitlab/transformers/discussion.rs index 78aceee..7123d2e 100644 --- a/src/gitlab/transformers/discussion.rs +++ b/src/gitlab/transformers/discussion.rs @@ -1,8 +1,6 @@ //! Discussion and note transformers: convert GitLab discussions to local schema. -use chrono::DateTime; - -use crate::core::time::now_ms; +use crate::core::time::{iso_to_ms, iso_to_ms_strict, now_ms}; use crate::gitlab::types::{GitLabDiscussion, GitLabNote}; /// Reference to the parent noteable (Issue or MergeRequest). @@ -60,16 +58,9 @@ pub struct NormalizedNote { pub position_head_sha: Option, // Head commit SHA for diff } -/// Parse ISO 8601 timestamp to milliseconds, returning None on failure. -fn parse_timestamp_opt(ts: &str) -> Option { - DateTime::parse_from_rfc3339(ts) - .ok() - .map(|dt| dt.timestamp_millis()) -} - /// Parse ISO 8601 timestamp to milliseconds, defaulting to 0 on failure. fn parse_timestamp(ts: &str) -> i64 { - parse_timestamp_opt(ts).unwrap_or(0) + iso_to_ms(ts).unwrap_or(0) } /// Transform a GitLab discussion into normalized schema. @@ -90,7 +81,7 @@ pub fn transform_discussion( let note_timestamps: Vec = gitlab_discussion .notes .iter() - .filter_map(|n| parse_timestamp_opt(&n.created_at)) + .filter_map(|n| iso_to_ms(&n.created_at)) .collect(); let first_note_at = note_timestamps.iter().min().copied(); @@ -191,7 +182,7 @@ fn transform_single_note( resolved_at: note .resolved_at .as_ref() - .and_then(|ts| parse_timestamp_opt(ts)), + .and_then(|ts| iso_to_ms(ts)), position_old_path, position_new_path, position_old_line, @@ -244,13 +235,6 @@ fn extract_position_fields( } } -/// Parse ISO 8601 timestamp to milliseconds with strict error handling. -/// Returns Err with the invalid timestamp in the error message. -fn parse_timestamp_strict(ts: &str) -> Result { - DateTime::parse_from_rfc3339(ts) - .map(|dt| dt.timestamp_millis()) - .map_err(|_| format!("Invalid timestamp: {}", ts)) -} /// Transform notes from a GitLab discussion with strict timestamp parsing. /// Returns Err if any timestamp is invalid - no silent fallback to 0. @@ -275,10 +259,10 @@ fn transform_single_note_strict( now: i64, ) -> Result { // Parse timestamps with strict error handling - let created_at = parse_timestamp_strict(¬e.created_at)?; - let updated_at = parse_timestamp_strict(¬e.updated_at)?; + let created_at = iso_to_ms_strict(¬e.created_at)?; + let updated_at = iso_to_ms_strict(¬e.updated_at)?; let resolved_at = match ¬e.resolved_at { - Some(ts) => Some(parse_timestamp_strict(ts)?), + Some(ts) => Some(iso_to_ms_strict(ts)?), None => None, }; diff --git a/src/gitlab/transformers/merge_request.rs b/src/gitlab/transformers/merge_request.rs index 650df40..3654c02 100644 --- a/src/gitlab/transformers/merge_request.rs +++ b/src/gitlab/transformers/merge_request.rs @@ -1,33 +1,8 @@ //! Merge request transformer: converts GitLabMergeRequest to local schema. -use chrono::DateTime; -use std::time::{SystemTime, UNIX_EPOCH}; - +use crate::core::time::{iso_to_ms_opt_strict, iso_to_ms_strict, now_ms}; use crate::gitlab::types::GitLabMergeRequest; -/// Get current time in milliseconds since Unix epoch. -fn now_ms() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_millis() as i64 -} - -/// Parse ISO 8601 timestamp to milliseconds since Unix epoch. -fn iso_to_ms(ts: &str) -> Result { - DateTime::parse_from_rfc3339(ts) - .map(|dt| dt.timestamp_millis()) - .map_err(|e| format!("Failed to parse timestamp '{}': {}", ts, e)) -} - -/// Parse optional ISO 8601 timestamp to optional milliseconds since Unix epoch. -fn iso_to_ms_opt(ts: &Option) -> Result, String> { - match ts { - Some(s) => iso_to_ms(s).map(Some), - None => Ok(None), - } -} - /// Local schema representation of a merge request row. #[derive(Debug, Clone)] pub struct NormalizedMergeRequest { @@ -77,12 +52,12 @@ pub fn transform_merge_request( local_project_id: i64, ) -> Result { // Parse required timestamps - let created_at = iso_to_ms(&gitlab_mr.created_at)?; - let updated_at = iso_to_ms(&gitlab_mr.updated_at)?; + let created_at = iso_to_ms_strict(&gitlab_mr.created_at)?; + let updated_at = iso_to_ms_strict(&gitlab_mr.updated_at)?; // Parse optional timestamps - let merged_at = iso_to_ms_opt(&gitlab_mr.merged_at)?; - let closed_at = iso_to_ms_opt(&gitlab_mr.closed_at)?; + let merged_at = iso_to_ms_opt_strict(&gitlab_mr.merged_at)?; + let closed_at = iso_to_ms_opt_strict(&gitlab_mr.closed_at)?; // Draft: prefer draft, fallback to work_in_progress let is_draft = gitlab_mr.draft || gitlab_mr.work_in_progress;