diff --git a/crates/harness/runner/src/prepare.rs b/crates/harness/runner/src/prepare.rs index 50c73512..98cbbfed 100644 --- a/crates/harness/runner/src/prepare.rs +++ b/crates/harness/runner/src/prepare.rs @@ -23,7 +23,6 @@ use std::fmt::{self, Write as _}; use std::io; use std::path::{Path, PathBuf}; use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; use harness_capabilities::{CapabilityRegistry, RunServices, activate}; use harness_log::{LogError, Record, RecordKind, RunId, RunMeta, RunOutcome}; @@ -331,13 +330,7 @@ fn prompt_hash(source: &str) -> String { } /// The system clock now as the engine's `Timestamp`: the host's stamp for -/// a run's `started_at`, since the engine reads no clock of its own. A -/// clock before the epoch or beyond `i64` milliseconds (neither reachable -/// on a real host) saturates to the epoch rather than refusing the launch. +/// a run's `started_at`, since the engine reads no clock of its own. fn now_timestamp() -> Timestamp { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .and_then(|elapsed| i64::try_from(elapsed.as_millis()).ok()) - .map_or(Timestamp::UNIX_EPOCH, Timestamp::from_unix_millis) + Timestamp::now() } diff --git a/crates/promptforge-api-types/src/capabilities-tests.rs b/crates/promptforge-api-types/src/capabilities-tests.rs index c05c0b96..a8d1520c 100644 --- a/crates/promptforge-api-types/src/capabilities-tests.rs +++ b/crates/promptforge-api-types/src/capabilities-tests.rs @@ -62,3 +62,18 @@ fn capability_id_serializes_as_its_string_form() { "a 3-segment string is a tool id, never a capability id" ); } + +#[test] +fn capability_id_implements_from_str() { + use std::str::FromStr; + let id = CapabilityId::from_str("promptforge/web").expect("valid capability id"); + assert_eq!(id.to_string(), "promptforge/web"); + assert_eq!( + "promptforge/web" + .parse::() + .expect("valid capability id") + .to_string(), + "promptforge/web" + ); + assert!(CapabilityId::from_str("promptforge/web/fetch").is_err()); +} diff --git a/crates/promptforge-api-types/src/capabilities.rs b/crates/promptforge-api-types/src/capabilities.rs index e72d5eac..5cdba7af 100644 --- a/crates/promptforge-api-types/src/capabilities.rs +++ b/crates/promptforge-api-types/src/capabilities.rs @@ -160,6 +160,14 @@ impl std::fmt::Display for CapabilityId { } } +impl std::str::FromStr for CapabilityId { + type Err = CapabilityIdError; + + fn from_str(s: &str) -> Result { + CapabilityId::parse(s) + } +} + impl serde::Serialize for CapabilityId { /// Serializes the identity as its one `namespace/pack` string. fn serialize(&self, serializer: S) -> Result { diff --git a/crates/promptforge-api-types/src/names-tests.rs b/crates/promptforge-api-types/src/names-tests.rs index ab4e9fc7..399930b5 100644 --- a/crates/promptforge-api-types/src/names-tests.rs +++ b/crates/promptforge-api-types/src/names-tests.rs @@ -110,3 +110,18 @@ fn non_ascii_is_rejected_as_a_control_error() { GlobalNameErrorKind::Control ); } + +#[test] +fn global_name_implements_from_str() { + use std::str::FromStr; + let name = GlobalName::from_str("promptforge/web/fetch").expect("valid global name"); + assert_eq!(name.to_string(), "promptforge/web/fetch"); + assert_eq!( + "promptforge/web" + .parse::() + .expect("valid global name") + .to_string(), + "promptforge/web" + ); + assert!(GlobalName::from_str("invalid").is_err()); +} diff --git a/crates/promptforge-api-types/src/names.rs b/crates/promptforge-api-types/src/names.rs index 835717a1..9213590b 100644 --- a/crates/promptforge-api-types/src/names.rs +++ b/crates/promptforge-api-types/src/names.rs @@ -108,6 +108,14 @@ impl fmt::Display for GlobalName { } } +impl std::str::FromStr for GlobalName { + type Err = GlobalNameError; + + fn from_str(s: &str) -> Result { + GlobalName::parse(s) + } +} + /// Validates one segment against the charset rule. /// /// A segment must be non-empty and contain only lowercase ASCII diff --git a/crates/promptforge-api-types/src/timestamp-tests.rs b/crates/promptforge-api-types/src/timestamp-tests.rs index 99c6fc60..d68543bc 100644 --- a/crates/promptforge-api-types/src/timestamp-tests.rs +++ b/crates/promptforge-api-types/src/timestamp-tests.rs @@ -79,3 +79,24 @@ fn a_timestamp_is_its_millisecond_count_on_the_wire() { assert_eq!(stamp.to_string(), stamp.to_rfc3339()); assert!(Timestamp::UNIX_EPOCH < stamp); } + +#[test] +fn from_system_time_converts_and_saturates() { + use std::time::{Duration, SystemTime}; + assert_eq!( + Timestamp::from(SystemTime::UNIX_EPOCH), + Timestamp::UNIX_EPOCH + ); + assert_eq!( + Timestamp::from_system_time(SystemTime::UNIX_EPOCH), + Timestamp::UNIX_EPOCH + ); + let after = SystemTime::UNIX_EPOCH + Duration::from_millis(5_000); + assert_eq!( + Timestamp::from_system_time(after), + Timestamp::from_unix_millis(5_000) + ); + let before = SystemTime::UNIX_EPOCH - Duration::from_secs(10); + assert_eq!(Timestamp::from_system_time(before), Timestamp::UNIX_EPOCH); + assert!(Timestamp::now() >= Timestamp::UNIX_EPOCH); +} diff --git a/crates/promptforge-api-types/src/timestamp.rs b/crates/promptforge-api-types/src/timestamp.rs index b7bb22ab..1531c269 100644 --- a/crates/promptforge-api-types/src/timestamp.rs +++ b/crates/promptforge-api-types/src/timestamp.rs @@ -52,6 +52,19 @@ impl Timestamp { self.0 } + /// The instant `time` represents, saturated to [`Timestamp::UNIX_EPOCH`] + /// if `time` is before the epoch or beyond `i64` milliseconds. + #[must_use] + pub fn from_system_time(time: std::time::SystemTime) -> Self { + Self::from(time) + } + + /// The current system clock as a [`Timestamp`]. + #[must_use] + pub fn now() -> Self { + Self::from(std::time::SystemTime::now()) + } + /// The instant as an RFC 3339 UTC string: `2024-02-29T12:34:56.789Z`. /// /// The fraction is omitted when the millisecond count is zero and @@ -88,6 +101,18 @@ impl fmt::Display for Timestamp { } } +impl From for Timestamp { + /// Converts a [`std::time::SystemTime`] into a [`Timestamp`], saturating to + /// [`Timestamp::UNIX_EPOCH`] if `time` is before the Unix epoch or beyond + /// `i64` milliseconds. + fn from(time: std::time::SystemTime) -> Self { + time.duration_since(std::time::UNIX_EPOCH) + .ok() + .and_then(|elapsed| i64::try_from(elapsed.as_millis()).ok()) + .map_or(Timestamp::UNIX_EPOCH, Timestamp::from_unix_millis) + } +} + /// Proleptic Gregorian `(year, month, day)` for a count of days since /// `1970-01-01`, valid for any `i64` day count that keeps the arithmetic in /// range. This is Howard Hinnant's `civil_from_days`: the calendar is diff --git a/crates/promptforge-api-types/src/tools/ids.rs b/crates/promptforge-api-types/src/tools/ids.rs index 5403e036..1e313e32 100644 --- a/crates/promptforge-api-types/src/tools/ids.rs +++ b/crates/promptforge-api-types/src/tools/ids.rs @@ -113,6 +113,14 @@ impl std::fmt::Display for ToolId { } } +impl std::str::FromStr for ToolId { + type Err = ToolIdError; + + fn from_str(s: &str) -> Result { + ToolId::parse(s) + } +} + impl serde::Serialize for ToolId { /// Serializes the identity as its one `namespace/pack/name` string. fn serialize(&self, serializer: S) -> Result { diff --git a/crates/promptforge-api-types/src/tools/tests.rs b/crates/promptforge-api-types/src/tools/tests.rs index 96d6093d..c3dc49fb 100644 --- a/crates/promptforge-api-types/src/tools/tests.rs +++ b/crates/promptforge-api-types/src/tools/tests.rs @@ -292,3 +292,18 @@ fn catalog_rejects_illegal_wire_name() { assert_eq!(error.kind(), ToolCatalogErrorKind::InvalidWireName); assert!(error.duplicate_id().is_none()); } + +#[test] +fn tool_id_implements_from_str() { + use std::str::FromStr; + let id = ToolId::from_str("promptforge/web/fetch").expect("valid tool id"); + assert_eq!(id.to_string(), "promptforge/web/fetch"); + assert_eq!( + "promptforge/web/fetch" + .parse::() + .expect("valid tool id") + .to_string(), + "promptforge/web/fetch" + ); + assert!(ToolId::from_str("promptforge/web").is_err()); +}