refactor(core): Centralize timestamp parsing in core::time
Duplicate ISO 8601 timestamp parsing functions existed in both discussion.rs and merge_request.rs transformers. This extracts iso_to_ms_strict() and iso_to_ms_opt_strict() into core::time as the single source of truth, and updates both transformer modules to use the shared implementations. Also removes the private now_ms() from merge_request.rs in favor of the existing core::time::now_ms(), and replaces the local parse_timestamp_opt() in discussion.rs with the public iso_to_ms() from core::time. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,22 @@ pub fn parse_since(input: &str) -> Option<i64> {
|
|||||||
iso_to_ms(input)
|
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<i64, String> {
|
||||||
|
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<String>) -> Result<Option<i64>, String> {
|
||||||
|
match iso_string {
|
||||||
|
Some(s) => iso_to_ms_strict(s).map(Some),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Format milliseconds epoch to human-readable full datetime.
|
/// Format milliseconds epoch to human-readable full datetime.
|
||||||
pub fn format_full_datetime(ms: i64) -> String {
|
pub fn format_full_datetime(ms: i64) -> String {
|
||||||
DateTime::from_timestamp_millis(ms)
|
DateTime::from_timestamp_millis(ms)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
//! Discussion and note transformers: convert GitLab discussions to local schema.
|
//! Discussion and note transformers: convert GitLab discussions to local schema.
|
||||||
|
|
||||||
use chrono::DateTime;
|
use crate::core::time::{iso_to_ms, iso_to_ms_strict, now_ms};
|
||||||
|
|
||||||
use crate::core::time::now_ms;
|
|
||||||
use crate::gitlab::types::{GitLabDiscussion, GitLabNote};
|
use crate::gitlab::types::{GitLabDiscussion, GitLabNote};
|
||||||
|
|
||||||
/// Reference to the parent noteable (Issue or MergeRequest).
|
/// Reference to the parent noteable (Issue or MergeRequest).
|
||||||
@@ -60,16 +58,9 @@ pub struct NormalizedNote {
|
|||||||
pub position_head_sha: Option<String>, // Head commit SHA for diff
|
pub position_head_sha: Option<String>, // Head commit SHA for diff
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse ISO 8601 timestamp to milliseconds, returning None on failure.
|
|
||||||
fn parse_timestamp_opt(ts: &str) -> Option<i64> {
|
|
||||||
DateTime::parse_from_rfc3339(ts)
|
|
||||||
.ok()
|
|
||||||
.map(|dt| dt.timestamp_millis())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse ISO 8601 timestamp to milliseconds, defaulting to 0 on failure.
|
/// Parse ISO 8601 timestamp to milliseconds, defaulting to 0 on failure.
|
||||||
fn parse_timestamp(ts: &str) -> i64 {
|
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.
|
/// Transform a GitLab discussion into normalized schema.
|
||||||
@@ -90,7 +81,7 @@ pub fn transform_discussion(
|
|||||||
let note_timestamps: Vec<i64> = gitlab_discussion
|
let note_timestamps: Vec<i64> = gitlab_discussion
|
||||||
.notes
|
.notes
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|n| parse_timestamp_opt(&n.created_at))
|
.filter_map(|n| iso_to_ms(&n.created_at))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let first_note_at = note_timestamps.iter().min().copied();
|
let first_note_at = note_timestamps.iter().min().copied();
|
||||||
@@ -191,7 +182,7 @@ fn transform_single_note(
|
|||||||
resolved_at: note
|
resolved_at: note
|
||||||
.resolved_at
|
.resolved_at
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|ts| parse_timestamp_opt(ts)),
|
.and_then(|ts| iso_to_ms(ts)),
|
||||||
position_old_path,
|
position_old_path,
|
||||||
position_new_path,
|
position_new_path,
|
||||||
position_old_line,
|
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<i64, String> {
|
|
||||||
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.
|
/// Transform notes from a GitLab discussion with strict timestamp parsing.
|
||||||
/// Returns Err if any timestamp is invalid - no silent fallback to 0.
|
/// Returns Err if any timestamp is invalid - no silent fallback to 0.
|
||||||
@@ -275,10 +259,10 @@ fn transform_single_note_strict(
|
|||||||
now: i64,
|
now: i64,
|
||||||
) -> Result<NormalizedNote, String> {
|
) -> Result<NormalizedNote, String> {
|
||||||
// Parse timestamps with strict error handling
|
// Parse timestamps with strict error handling
|
||||||
let created_at = parse_timestamp_strict(¬e.created_at)?;
|
let created_at = iso_to_ms_strict(¬e.created_at)?;
|
||||||
let updated_at = parse_timestamp_strict(¬e.updated_at)?;
|
let updated_at = iso_to_ms_strict(¬e.updated_at)?;
|
||||||
let resolved_at = match ¬e.resolved_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,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,8 @@
|
|||||||
//! Merge request transformer: converts GitLabMergeRequest to local schema.
|
//! Merge request transformer: converts GitLabMergeRequest to local schema.
|
||||||
|
|
||||||
use chrono::DateTime;
|
use crate::core::time::{iso_to_ms_opt_strict, iso_to_ms_strict, now_ms};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use crate::gitlab::types::GitLabMergeRequest;
|
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<i64, String> {
|
|
||||||
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<String>) -> Result<Option<i64>, String> {
|
|
||||||
match ts {
|
|
||||||
Some(s) => iso_to_ms(s).map(Some),
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Local schema representation of a merge request row.
|
/// Local schema representation of a merge request row.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct NormalizedMergeRequest {
|
pub struct NormalizedMergeRequest {
|
||||||
@@ -77,12 +52,12 @@ pub fn transform_merge_request(
|
|||||||
local_project_id: i64,
|
local_project_id: i64,
|
||||||
) -> Result<MergeRequestWithMetadata, String> {
|
) -> Result<MergeRequestWithMetadata, String> {
|
||||||
// Parse required timestamps
|
// Parse required timestamps
|
||||||
let created_at = iso_to_ms(&gitlab_mr.created_at)?;
|
let created_at = iso_to_ms_strict(&gitlab_mr.created_at)?;
|
||||||
let updated_at = iso_to_ms(&gitlab_mr.updated_at)?;
|
let updated_at = iso_to_ms_strict(&gitlab_mr.updated_at)?;
|
||||||
|
|
||||||
// Parse optional timestamps
|
// Parse optional timestamps
|
||||||
let merged_at = iso_to_ms_opt(&gitlab_mr.merged_at)?;
|
let merged_at = iso_to_ms_opt_strict(&gitlab_mr.merged_at)?;
|
||||||
let closed_at = iso_to_ms_opt(&gitlab_mr.closed_at)?;
|
let closed_at = iso_to_ms_opt_strict(&gitlab_mr.closed_at)?;
|
||||||
|
|
||||||
// Draft: prefer draft, fallback to work_in_progress
|
// Draft: prefer draft, fallback to work_in_progress
|
||||||
let is_draft = gitlab_mr.draft || gitlab_mr.work_in_progress;
|
let is_draft = gitlab_mr.draft || gitlab_mr.work_in_progress;
|
||||||
|
|||||||
Reference in New Issue
Block a user