use std::env; use std::path::PathBuf; use std::process::ExitCode; use std::time::Duration; use clap::{Args, Parser, Subcommand, ValueEnum}; use futures_util::StreamExt; use mxgateway_client::generated::galaxy_repository::v1::DeployEvent; use mxgateway_client::generated::mxaccess_gateway::v1::{ CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, OpenSessionRequest, PingCommand, StreamEventsRequest, }; use mxgateway_client::{ ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue, CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION, }; use serde_json::json; use serde_json::Value; const MAX_AGGREGATE_EVENTS: usize = 10_000; #[derive(Debug, Parser)] #[command(name = "mxgw")] #[command(about = "MXAccess Gateway Rust test CLI")] struct Cli { #[command(subcommand)] command: Command, } #[derive(Debug, Subcommand)] enum Command { Version { #[arg(long)] json: bool, #[arg(long)] jsonl: bool, }, Ping { #[command(flatten)] connection: ConnectionArgs, #[arg(long, default_value = "ping")] message: String, #[arg(long)] json: bool, }, OpenSession { #[command(flatten)] connection: ConnectionArgs, #[arg(long, default_value = "mxgw-rust-cli")] client_name: String, #[arg(long)] json: bool, }, CloseSession { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] session_id: String, #[arg(long)] json: bool, }, Register { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] session_id: String, #[arg(long, default_value = "mxgw-rust-cli")] client_name: String, #[arg(long)] json: bool, }, AddItem { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] session_id: String, #[arg(long)] server_handle: i32, #[arg(long)] item: String, #[arg(long)] json: bool, }, Advise { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] session_id: String, #[arg(long)] server_handle: i32, #[arg(long)] item_handle: i32, #[arg(long)] json: bool, }, SubscribeBulk { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] session_id: String, #[arg(long)] server_handle: i32, #[arg(long, value_delimiter = ',')] items: Vec, #[arg(long)] json: bool, }, UnsubscribeBulk { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] session_id: String, #[arg(long)] server_handle: i32, #[arg(long, value_delimiter = ',')] item_handles: Vec, #[arg(long)] json: bool, }, StreamEvents { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] session_id: String, #[arg(long, default_value_t = 0)] after_worker_sequence: u64, #[arg(long, default_value_t = 1)] max_events: usize, #[arg(long)] json: bool, #[arg(long)] jsonl: bool, }, Write { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] session_id: String, #[arg(long)] server_handle: i32, #[arg(long)] item_handle: i32, #[arg(long, value_enum)] value_type: CliValueType, #[arg(long)] value: String, #[arg(long, default_value_t = 0)] user_id: i32, #[arg(long)] json: bool, }, Write2 { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] session_id: String, #[arg(long)] server_handle: i32, #[arg(long)] item_handle: i32, #[arg(long, value_enum)] value_type: CliValueType, #[arg(long)] value: String, #[arg(long)] timestamp: String, #[arg(long, default_value_t = 0)] user_id: i32, #[arg(long)] json: bool, }, Smoke { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] item: String, #[arg(long, default_value = "mxgw-rust-smoke")] client_name: String, #[arg(long)] json: bool, }, #[command(subcommand)] Galaxy(GalaxyCommand), } #[derive(Debug, Subcommand)] enum GalaxyCommand { TestConnection { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] json: bool, }, LastDeployTime { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] json: bool, }, DiscoverHierarchy { #[command(flatten)] connection: ConnectionArgs, #[arg(long)] json: bool, }, /// Subscribe to the WatchDeployEvents server stream. /// /// Prints one line per received event (or one JSON object with `--json`). /// Runs until the stream ends, the server fails the call, or the /// process is interrupted (Ctrl+C). #[command(alias = "watch-deploy-events")] Watch { #[command(flatten)] connection: ConnectionArgs, /// Optional RFC3339 timestamp (e.g. `2026-04-28T15:30:00Z`). When /// supplied, the server suppresses the bootstrap event if its /// cached deploy time matches this value. #[arg(long)] last_seen_deploy_time: Option, /// Optional cap on the number of events to print before exiting. /// 0 (the default) means run until cancelled or the stream ends. #[arg(long, default_value_t = 0)] max_events: usize, #[arg(long)] json: bool, }, } #[derive(Debug, Args, Clone)] struct ConnectionArgs { #[arg(long, default_value = "http://127.0.0.1:5000")] endpoint: String, #[arg(long)] api_key: Option, #[arg(long, default_value = "MXGATEWAY_API_KEY")] api_key_env: String, #[arg(long)] plaintext: bool, #[arg(long)] tls: bool, #[arg(long)] ca_file: Option, #[arg(long)] server_name_override: Option, #[arg(long, default_value_t = 10)] connect_timeout_seconds: u64, #[arg(long, default_value_t = 30)] call_timeout_seconds: u64, } impl ConnectionArgs { fn options(&self) -> ClientOptions { let mut options = ClientOptions::new(self.endpoint.clone()) .with_plaintext(!self.tls || self.plaintext) .with_connect_timeout(Duration::from_secs(self.connect_timeout_seconds)) .with_call_timeout(Duration::from_secs(self.call_timeout_seconds)); if let Some(api_key) = self .api_key .clone() .or_else(|| env::var(&self.api_key_env).ok()) .filter(|value| !value.is_empty()) { options = options.with_api_key(ApiKey::new(api_key)); } if let Some(ca_file) = &self.ca_file { options = options.with_ca_file(ca_file); } if let Some(server_name_override) = &self.server_name_override { options = options.with_server_name_override(server_name_override); } options } } #[derive(Clone, Copy, Debug, ValueEnum)] enum CliValueType { Bool, Int32, Int64, Float, Double, String, } #[tokio::main] async fn main() -> ExitCode { let cli = Cli::parse(); match run(cli).await { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("{error}"); ExitCode::FAILURE } } } async fn run(cli: Cli) -> Result<(), Error> { match cli.command { Command::Version { json, .. } => print_version(json), Command::Ping { connection, message, json, } => { let client = connect(connection).await?; let reply = client .invoke(MxCommandRequest { client_correlation_id: "rust-cli-ping".to_owned(), command: Some(MxCommand { kind: MxCommandKind::Ping as i32, payload: Some(mxgateway_client::generated::mxaccess_gateway::v1::mx_command::Payload::Ping( PingCommand { message }, )), }), ..MxCommandRequest::default() }) .await?; print_command_reply("ping", &reply, json); } Command::OpenSession { connection, client_name, json, } => { let client = connect(connection).await?; let reply = client .open_session_raw(OpenSessionRequest { client_session_name: client_name, ..OpenSessionRequest::default() }) .await?; if json { println!( "{}", json!({ "sessionId": reply.session_id, "backendName": reply.backend_name, "gatewayProtocolVersion": reply.gateway_protocol_version, "workerProtocolVersion": reply.worker_protocol_version, }) ); } else { println!("{}", reply.session_id); } } Command::CloseSession { connection, session_id, json, } => { let client = connect(connection).await?; let reply = client .close_session_raw(CloseSessionRequest { session_id, client_correlation_id: "rust-cli-close-session".to_owned(), }) .await?; if json { println!("{}", json!({ "sessionId": reply.session_id })); } else { println!("closed {}", reply.session_id); } } Command::Register { connection, session_id, client_name, json, } => { let session = session_for(connection, session_id).await?; let server_handle = session.register(&client_name).await?; print_handle("serverHandle", server_handle, json); } Command::AddItem { connection, session_id, server_handle, item, json, } => { let session = session_for(connection, session_id).await?; let item_handle = session.add_item(server_handle, &item).await?; print_handle("itemHandle", item_handle, json); } Command::Advise { connection, session_id, server_handle, item_handle, json, } => { let session = session_for(connection, session_id).await?; session.advise(server_handle, item_handle).await?; print_ok("advise", json); } Command::SubscribeBulk { connection, session_id, server_handle, items, json, } => { let session = session_for(connection, session_id).await?; let results = session.subscribe_bulk(server_handle, items).await?; print_bulk_results("subscribe-bulk", &results, json); } Command::UnsubscribeBulk { connection, session_id, server_handle, item_handles, json, } => { let session = session_for(connection, session_id).await?; let results = session .unsubscribe_bulk(server_handle, item_handles) .await?; print_bulk_results("unsubscribe-bulk", &results, json); } Command::StreamEvents { connection, session_id, after_worker_sequence, max_events, json, jsonl, } => { if max_events > MAX_AGGREGATE_EVENTS { return Err(Error::InvalidArgument { name: "max-events".to_owned(), detail: format!("must be less than or equal to {MAX_AGGREGATE_EVENTS}"), }); } let client = connect(connection).await?; let mut stream = client .stream_events(StreamEventsRequest { session_id, after_worker_sequence, }) .await?; let mut events = Vec::new(); let mut event_count = 0usize; while event_count < max_events { let Some(event) = stream.next().await else { break; }; let event = event?; event_count += 1; if jsonl { println!( "{}", json!({ "workerSequence": event.worker_sequence, "family": event.family, }) ); } else if json { events.push(event); } else { println!("{} {}", event.worker_sequence, event.family); } } if json { println!("{}", json!({ "eventCount": event_count })); } } Command::Write { connection, session_id, server_handle, item_handle, value_type, value, user_id, json, } => { let session = session_for(connection, session_id).await?; session .write( server_handle, item_handle, parse_value(value_type, &value)?, user_id, ) .await?; print_ok("write", json); } Command::Write2 { connection, session_id, server_handle, item_handle, value_type, value, timestamp, user_id, json, } => { let session = session_for(connection, session_id).await?; session .write2( server_handle, item_handle, parse_value(value_type, &value)?, MxValue::string(timestamp), user_id, ) .await?; print_ok("write2", json); } Command::Galaxy(galaxy_command) => run_galaxy(galaxy_command).await?, Command::Smoke { connection, item, client_name, json, } => { let client = connect(connection).await?; let session = client .open_session(OpenSessionRequest { client_session_name: client_name.clone(), ..OpenSessionRequest::default() }) .await?; let result = async { let server_handle = session.register(&client_name).await?; let item_handle = session.add_item(server_handle, &item).await?; session.advise(server_handle, item_handle).await?; Ok::<_, Error>((server_handle, item_handle)) } .await; let close_result = session.close().await; let (server_handle, item_handle) = result?; close_result?; if json { println!( "{}", json!({ "sessionId": session.id(), "serverHandle": server_handle, "itemHandle": item_handle, "closed": true, }) ); } else { println!( "session {} registered server {server_handle}, item {item_handle}, closed", session.id() ); } } } Ok(()) } async fn connect(connection: ConnectionArgs) -> Result { GatewayClient::connect(connection.options()).await } async fn connect_galaxy(connection: ConnectionArgs) -> Result { GalaxyClient::connect(connection.options()).await } async fn run_galaxy(command: GalaxyCommand) -> Result<(), Error> { match command { GalaxyCommand::TestConnection { connection, json } => { let mut client = connect_galaxy(connection).await?; let ok = client.test_connection().await?; if json { println!("{}", json!({ "ok": ok })); } else if ok { println!("ok"); } else { println!("not ok"); } } GalaxyCommand::LastDeployTime { connection, json } => { let mut client = connect_galaxy(connection).await?; let timestamp = client.get_last_deploy_time().await?; match (json, timestamp) { (true, Some(ts)) => { println!( "{}", json!({ "present": true, "seconds": ts.seconds, "nanos": ts.nanos, }) ); } (true, None) => { println!("{}", json!({ "present": false })); } (false, Some(ts)) => println!("{}.{:09}", ts.seconds, ts.nanos), (false, None) => println!("(absent)"), } } GalaxyCommand::Watch { connection, last_seen_deploy_time, max_events, json, } => { let mut client = connect_galaxy(connection).await?; let last_seen = last_seen_deploy_time .as_deref() .map(parse_rfc3339_timestamp) .transpose()?; let mut stream = client.watch_deploy_events(last_seen).await?; let mut count = 0usize; loop { tokio::select! { biased; signal = tokio::signal::ctrl_c() => { signal.map_err(|err| Error::InvalidArgument { name: "ctrl_c".to_owned(), detail: err.to_string(), })?; // Drop the stream below by breaking; tonic tears the // gRPC call down cooperatively. break; } next = stream.next() => { let Some(event) = next else { break; }; let event = event?; count += 1; print_deploy_event(&event, json); if max_events != 0 && count >= max_events { break; } } } } } GalaxyCommand::DiscoverHierarchy { connection, json } => { let mut client = connect_galaxy(connection).await?; let objects = client.discover_hierarchy().await?; if json { let payload: Vec<_> = objects .iter() .map(|object| { json!({ "gobjectId": object.gobject_id, "tagName": object.tag_name, "containedName": object.contained_name, "browseName": object.browse_name, "parentGobjectId": object.parent_gobject_id, "isArea": object.is_area, "categoryId": object.category_id, "hostedByGobjectId": object.hosted_by_gobject_id, "templateChain": object.template_chain, "attributes": object.attributes.iter().map(|attribute| json!({ "attributeName": attribute.attribute_name, "fullTagReference": attribute.full_tag_reference, "mxDataType": attribute.mx_data_type, "dataTypeName": attribute.data_type_name, "isArray": attribute.is_array, "arrayDimension": attribute.array_dimension, "arrayDimensionPresent": attribute.array_dimension_present, "mxAttributeCategory": attribute.mx_attribute_category, "securityClassification": attribute.security_classification, "isHistorized": attribute.is_historized, "isAlarm": attribute.is_alarm, })).collect::>(), }) }) .collect(); println!("{}", json!({ "objects": payload })); } else { println!("{}", objects.len()); for object in &objects { println!( "{} {} {} ({} attribute(s))", object.gobject_id, object.tag_name, object.browse_name, object.attributes.len() ); } } } } Ok(()) } async fn session_for( connection: ConnectionArgs, session_id: String, ) -> Result { let client = connect(connection).await?; Ok(client.session(session_id)) } fn print_version(use_json: bool) { if use_json { println!("{}", version_json()); return; } println!("mxgw {CLIENT_VERSION}"); println!("gateway protocol {GATEWAY_PROTOCOL_VERSION}"); println!("worker protocol {WORKER_PROTOCOL_VERSION}"); } fn version_json() -> Value { json!({ "clientVersion": CLIENT_VERSION, "gatewayProtocolVersion": GATEWAY_PROTOCOL_VERSION, "workerProtocolVersion": WORKER_PROTOCOL_VERSION, }) } fn print_command_reply( operation: &str, reply: &mxgateway_client::generated::mxaccess_gateway::v1::MxCommandReply, use_json: bool, ) { if use_json { println!( "{}", json!({ "operation": operation, "sessionId": reply.session_id, "correlationId": reply.correlation_id, "kind": reply.kind, }) ); } else { println!("{operation} completed"); } } fn print_handle(name: &str, handle: i32, use_json: bool) { if use_json { println!("{}", json!({ name: handle })); } else { println!("{handle}"); } } fn print_ok(operation: &str, use_json: bool) { if use_json { println!("{}", json!({ "operation": operation, "ok": true })); } else { println!("{operation} completed"); } } fn print_bulk_results( operation: &str, results: &[mxgateway_client::generated::mxaccess_gateway::v1::SubscribeResult], use_json: bool, ) { if use_json { let results_json: Vec<_> = results .iter() .map(|result| { json!({ "serverHandle": result.server_handle, "tagAddress": result.tag_address, "itemHandle": result.item_handle, "wasSuccessful": result.was_successful, "errorMessage": result.error_message, }) }) .collect(); println!( "{}", json!({ "operation": operation, "results": results_json }) ); } else { println!("{}", results.len()); } } fn parse_value(value_type: CliValueType, value: &str) -> Result { let parsed = match value_type { CliValueType::Bool => MxValue::bool(parse_cli_value(value)?), CliValueType::Int32 => MxValue::int32(parse_cli_value(value)?), CliValueType::Int64 => MxValue::int64(parse_cli_value(value)?), CliValueType::Float => MxValue::float(parse_cli_value(value)?), CliValueType::Double => MxValue::double(parse_cli_value(value)?), CliValueType::String => MxValue::string(value), }; Ok(parsed) } fn print_deploy_event(event: &DeployEvent, use_json: bool) { if use_json { println!( "{}", json!({ "sequence": event.sequence, "observedAt": event.observed_at.as_ref().map(|ts| json!({ "seconds": ts.seconds, "nanos": ts.nanos, })), "timeOfLastDeploy": event.time_of_last_deploy.as_ref().map(|ts| json!({ "seconds": ts.seconds, "nanos": ts.nanos, })), "timeOfLastDeployPresent": event.time_of_last_deploy_present, "objectCount": event.object_count, "attributeCount": event.attribute_count, }) ); } else { let observed = event .observed_at .as_ref() .map(|ts| format!("{}.{:09}", ts.seconds, ts.nanos)) .unwrap_or_else(|| "(absent)".to_owned()); let last_deploy = if event.time_of_last_deploy_present { event .time_of_last_deploy .as_ref() .map(|ts| format!("{}.{:09}", ts.seconds, ts.nanos)) .unwrap_or_else(|| "(absent)".to_owned()) } else { "(absent)".to_owned() }; println!( "seq={} observed={} lastDeploy={} objects={} attributes={}", event.sequence, observed, last_deploy, event.object_count, event.attribute_count, ); } } /// Parse a small but practically-complete subset of RFC3339: /// `YYYY-MM-DDTHH:MM:SS[.fffffffff][Z|+HH:MM|-HH:MM]`. Returns the /// corresponding `prost_types::Timestamp` (Unix seconds + nanoseconds). fn parse_rfc3339_timestamp(input: &str) -> Result { fn invalid(detail: impl Into) -> Error { Error::InvalidArgument { name: "last-seen-deploy-time".to_owned(), detail: detail.into(), } } let bytes = input.as_bytes(); if bytes.len() < 20 || (bytes[10] != b'T' && bytes[10] != b't') { return Err(invalid(format!( "expected RFC3339 timestamp like 2026-04-28T15:30:00Z, got {input:?}" ))); } let read_u32 = |start: usize, len: usize| -> Result { std::str::from_utf8(&bytes[start..start + len]) .ok() .and_then(|slice| slice.parse::().ok()) .ok_or_else(|| invalid(format!("non-numeric digits at byte {start}"))) }; let year = read_u32(0, 4)? as i32; if bytes[4] != b'-' { return Err(invalid("expected '-' after year")); } let month = read_u32(5, 2)?; if bytes[7] != b'-' { return Err(invalid("expected '-' after month")); } let day = read_u32(8, 2)?; let hour = read_u32(11, 2)?; if bytes[13] != b':' { return Err(invalid("expected ':' after hour")); } let minute = read_u32(14, 2)?; if bytes[16] != b':' { return Err(invalid("expected ':' after minute")); } let second = read_u32(17, 2)?; let mut cursor = 19usize; let mut nanos: u32 = 0; if cursor < bytes.len() && bytes[cursor] == b'.' { cursor += 1; let frac_start = cursor; while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { cursor += 1; } let frac_len = cursor - frac_start; if frac_len == 0 { return Err(invalid("expected fractional digits after '.'")); } let take = frac_len.min(9); let frac = std::str::from_utf8(&bytes[frac_start..frac_start + take]) .ok() .and_then(|slice| slice.parse::().ok()) .ok_or_else(|| invalid("invalid fractional digits"))?; nanos = frac * 10u32.pow(9u32.saturating_sub(take as u32)); } let mut offset_seconds: i64 = 0; if cursor >= bytes.len() { return Err(invalid("missing timezone designator (Z or +HH:MM)")); } match bytes[cursor] { b'Z' | b'z' => cursor += 1, sign @ (b'+' | b'-') => { cursor += 1; if cursor + 5 > bytes.len() { return Err(invalid("offset must be ±HH:MM")); } let oh = std::str::from_utf8(&bytes[cursor..cursor + 2]) .ok() .and_then(|slice| slice.parse::().ok()) .ok_or_else(|| invalid("invalid offset hour"))?; if bytes[cursor + 2] != b':' { return Err(invalid("offset must contain ':' between HH and MM")); } let om = std::str::from_utf8(&bytes[cursor + 3..cursor + 5]) .ok() .and_then(|slice| slice.parse::().ok()) .ok_or_else(|| invalid("invalid offset minute"))?; cursor += 5; let signed = if sign == b'-' { -1 } else { 1 }; offset_seconds = signed * (oh * 3600 + om * 60); } other => { return Err(invalid(format!( "unexpected timezone designator byte {other:?}" ))); } } if cursor != bytes.len() { return Err(invalid("trailing characters after timezone")); } let unix = ymdhms_to_unix(year, month, day, hour, minute, second)?; let seconds = unix - offset_seconds; Ok(prost_types::Timestamp { seconds, nanos: nanos as i32, }) } fn ymdhms_to_unix( year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32, ) -> Result { if !(1..=12).contains(&month) || day < 1 || hour > 23 || minute > 59 || second > 60 { return Err(Error::InvalidArgument { name: "last-seen-deploy-time".to_owned(), detail: "calendar component out of range".to_owned(), }); } fn is_leap(year: i32) -> bool { (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 } const DAYS: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; let mut max = DAYS[(month - 1) as usize]; if month == 2 && is_leap(year) { max = 29; } if day > max { return Err(Error::InvalidArgument { name: "last-seen-deploy-time".to_owned(), detail: format!("day {day} out of range for month {month}/{year}"), }); } // Days from 1970-01-01 to year-month-day. let mut total_days: i64 = 0; if year >= 1970 { for y in 1970..year { total_days += if is_leap(y) { 366 } else { 365 }; } } else { for y in year..1970 { total_days -= if is_leap(y) { 366 } else { 365 }; } } for m in 1..month { let mut dim = DAYS[(m - 1) as usize]; if m == 2 && is_leap(year) { dim = 29; } total_days += dim as i64; } total_days += (day - 1) as i64; Ok(total_days * 86_400 + hour as i64 * 3600 + minute as i64 * 60 + second as i64) } fn parse_cli_value(value: &str) -> Result where T: std::str::FromStr, T::Err: std::fmt::Display, { value.parse::().map_err(|source| Error::InvalidArgument { name: "value".to_owned(), detail: source.to_string(), }) } #[cfg(test)] mod tests { use clap::Parser; use super::Cli; #[test] fn parses_version_json_command() { let parsed = Cli::try_parse_from(["mxgw", "version", "--json"]); assert!(parsed.is_ok()); } #[test] fn parses_write_command() { let parsed = Cli::try_parse_from([ "mxgw", "write", "--session-id", "session-1", "--server-handle", "12", "--item-handle", "34", "--value-type", "int32", "--value", "123", ]); assert!(parsed.is_ok()); } #[test] fn version_json_output_has_protocol_versions() { let value = super::version_json(); assert_eq!(value["gatewayProtocolVersion"], 2); assert_eq!(value["workerProtocolVersion"], 1); } #[test] fn parses_galaxy_watch_command_with_last_seen_and_max_events() { let parsed = Cli::try_parse_from([ "mxgw", "galaxy", "watch", "--last-seen-deploy-time", "2026-04-28T15:30:00Z", "--max-events", "5", "--json", ]); assert!(parsed.is_ok(), "parse failed: {parsed:?}"); } #[test] fn parses_galaxy_watch_deploy_events_alias() { let parsed = Cli::try_parse_from(["mxgw", "galaxy", "watch-deploy-events"]); assert!(parsed.is_ok(), "parse failed: {parsed:?}"); } #[test] fn rfc3339_parser_round_trips_z_and_offset_inputs() { // 2026-04-28T15:30:00Z = 1_777_995_000 (sanity-checked once below) let utc = super::parse_rfc3339_timestamp("2026-04-28T15:30:00Z").unwrap(); let plus = super::parse_rfc3339_timestamp("2026-04-28T16:30:00+01:00").unwrap(); let frac = super::parse_rfc3339_timestamp("2026-04-28T15:30:00.250Z").unwrap(); assert_eq!(utc.seconds, plus.seconds); assert_eq!(utc.nanos, 0); assert_eq!(frac.seconds, utc.seconds); assert_eq!(frac.nanos, 250_000_000); } }