localtime: localise the localtime dependency

The `localtime` crate was defined by cloudhead, and is a minimal
repository with a single `lib.rs`.

Instead of using it as an external dependency, copy the code directly
into a new workspace crate `radicle-localtime`.

The default `serde` implementation goes through the `LocalTime`'s
seconds values rather than milliseconds, since this is the more common
format. This allows the removal of the `serde_ext` functions.
The one place milliseconds was used was for the
`radicle::cob::common::Timestamp` type, where the `Serialize` and
`Deserialize` implementations are manually written.

It also adds a `schemars` feature to remove `schemars_ext` functions
in `radicle` as well.
This commit is contained in:
Fintan Halpenny 2025-12-28 18:08:54 +00:00 committed by Lorenz Leutgeb
parent 3168107df9
commit d98033a1ff
21 changed files with 400 additions and 216 deletions

26
Cargo.lock generated
View File

@ -2168,15 +2168,6 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
[[package]]
name = "localtime"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016a009e0bb8ba6e3229fb74bf11a8fe6ef24542cc6ef35ef38863ac13f96d87"
dependencies = [
"serde",
]
[[package]]
name = "lock_api"
version = "0.4.14"
@ -2796,7 +2787,6 @@ dependencies = [
"indexmap",
"jsonschema",
"libc",
"localtime",
"log",
"multibase",
"nonempty",
@ -2807,6 +2797,7 @@ dependencies = [
"radicle-crypto",
"radicle-git-metadata",
"radicle-git-ref-format",
"radicle-localtime",
"radicle-oid",
"radicle-ssh",
"schemars",
@ -2832,7 +2823,6 @@ dependencies = [
"dunce",
"human-panic",
"itertools",
"localtime",
"log",
"nonempty",
"pretty_assertions",
@ -2841,6 +2831,7 @@ dependencies = [
"radicle-cob",
"radicle-crypto",
"radicle-git-ref-format",
"radicle-localtime",
"radicle-node",
"radicle-surf",
"radicle-term",
@ -2981,6 +2972,15 @@ dependencies = [
"git-ref-format-core",
]
[[package]]
name = "radicle-localtime"
version = "0.1.0"
dependencies = [
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "radicle-node"
version = "0.16.0"
@ -2994,7 +2994,6 @@ dependencies = [
"cyphernet",
"fastrand",
"lexopt",
"localtime",
"log",
"mio 1.0.4",
"nonempty",
@ -3003,6 +3002,7 @@ dependencies = [
"radicle",
"radicle-crypto",
"radicle-fetch",
"radicle-localtime",
"radicle-protocol",
"radicle-signals",
"radicle-systemd",
@ -3041,7 +3041,6 @@ dependencies = [
"crossbeam-channel",
"cyphernet",
"fastrand",
"localtime",
"log",
"nonempty",
"paste",
@ -3050,6 +3049,7 @@ dependencies = [
"radicle",
"radicle-crypto",
"radicle-fetch",
"radicle-localtime",
"scrypt",
"serde",
"serde_json",

View File

@ -34,7 +34,6 @@ human-panic = "2"
itertools = "0.14"
lexopt = "0.3.0"
libc = "0.2.137"
localtime = "1.2.0"
log = "0.4.17"
multibase = "0.9.1"
nonempty = "0.9.0"
@ -50,6 +49,7 @@ radicle-dag = { version = "0.10", path = "crates/radicle-dag" }
radicle-fetch = { version = "0.16", path = "crates/radicle-fetch" }
radicle-git-metadata = { version = "0.1.0", path = "crates/radicle-git-metadata", default-features = false }
radicle-git-ref-format = { version = "0.1.0", path = "crates/radicle-git-ref-format", default-features = false }
radicle-localtime = { version = "0.1", path = "crates/radicle-localtime" }
radicle-node = { version = "0.16", path = "crates/radicle-node" }
radicle-oid = { version = "0.1.0", path = "crates/radicle-oid", default-features = false }
radicle-protocol = { version = "0.4", path = "crates/radicle-protocol" }

View File

@ -21,13 +21,13 @@ clap_complete = "4.5"
dunce = { workspace = true }
human-panic.workspace = true
itertools.workspace = true
localtime = { workspace = true }
log = { workspace = true, features = ["std"] }
nonempty = { workspace = true }
radicle = { workspace = true, features = ["logger", "schemars"] }
radicle-cob = { workspace = true }
radicle-crypto = { workspace = true }
radicle-git-ref-format = { workspace = true, features = ["macro"] }
radicle-localtime = { workspace = true }
radicle-surf = { workspace = true }
radicle-term = { workspace = true }
schemars = { workspace = true }
@ -57,6 +57,7 @@ zeroize = { workspace = true }
pretty_assertions = { workspace = true }
radicle = { workspace = true, features = ["test"] }
radicle-cli-test = { workspace = true }
radicle-localtime = { workspace = true }
radicle-node = { workspace = true, features = ["test"] }
[lints]

View File

@ -9,3 +9,5 @@ pub mod project;
pub mod terminal;
mod warning;
extern crate radicle_localtime as localtime;

View File

@ -20,6 +20,7 @@ use radicle::profile::Home;
use radicle::storage::{ReadStorage, RefUpdate, RemoteRepository};
use radicle::test::fixtures;
use radicle_localtime::LocalTime;
#[allow(unused_imports)]
use radicle_node::test::logger;
use radicle_node::test::node::Node;
@ -806,7 +807,7 @@ fn rad_node_connect_without_address() {
&Alias::new("bob"),
0,
&UserAgent::default(),
localtime::LocalTime::now().into(),
LocalTime::now().into(),
[node::KnownAddress::new(
node::Address::from(bob.addr),
node::address::Source::Imported,
@ -1351,7 +1352,7 @@ fn rad_clone_partial_fail() {
&Alias::new("carol"),
0,
&UserAgent::default(),
localtime::LocalTime::now().into(),
LocalTime::now().into(),
[node::KnownAddress::new(
// Eve will fail to connect to this address.
node::Address::from(net::SocketAddr::from(([0, 0, 0, 0], 19873))),
@ -1361,7 +1362,7 @@ fn rad_clone_partial_fail() {
.unwrap();
eve.db
.routing_mut()
.add_inventory([&acme], carol, localtime::LocalTime::now().into())
.add_inventory([&acme], carol, LocalTime::now().into())
.unwrap();
eve.config.peers = node::config::PeerConfig::Static;
@ -1395,7 +1396,7 @@ fn rad_clone_connect() {
let mut eve = environment.node("eve");
let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
let ua = UserAgent::default();
let now = localtime::LocalTime::now().into();
let now = LocalTime::now().into();
fixtures::repository(working.join("acme"));

View File

@ -1,7 +1,6 @@
use std::path::PathBuf;
use std::str::FromStr;
use localtime::LocalTime;
use radicle::cob::cache::COBS_DB_FILE;
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
use radicle::crypto::{KeyPair, Seed};
@ -13,6 +12,7 @@ use radicle::profile::Home;
use radicle::profile::{self};
use radicle::storage::git::transport;
use radicle::{Profile, Storage};
use radicle_localtime::LocalTime;
use radicle_node::test::node::{Node, NodeHandle};

View File

@ -0,0 +1,18 @@
[package]
name = "radicle-localtime"
version = "0.1.0"
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dependencies]
serde = { workspace = true, optional = true, features = ["derive"] }
schemars = { workspace = true, optional = true, features = ["derive", "std"] }
[dev-dependencies]
serde_json = { workspace = true }

View File

@ -0,0 +1,325 @@
//! Minimal, zero-dependency, monotonic, unix time library for rust.
//!
//! Taken from <https://github.com/cloudhead/localtime>
use std::sync::atomic;
use std::time::{SystemTime, UNIX_EPOCH};
/// Local time.
///
/// This clock is monotonic.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Ord, PartialOrd, Default)]
#[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(description = "A timestamp measured locally in seconds.")
)]
pub struct LocalTime {
/// Milliseconds since Epoch.
millis: u128,
}
impl std::fmt::Display for LocalTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_secs())
}
}
impl LocalTime {
/// Construct a local time from the current system time.
pub fn now() -> Self {
static LAST: atomic::AtomicU64 = atomic::AtomicU64::new(0);
let now = Self::from(SystemTime::now()).as_secs();
let last = LAST.load(atomic::Ordering::SeqCst);
// If the current time is in the past, return the last recorded time instead.
if now < last {
Self::from_secs(last)
} else {
LAST.store(now, atomic::Ordering::SeqCst);
LocalTime::from_secs(now)
}
}
/// Construct a local time from whole seconds since Epoch.
pub const fn from_secs(secs: u64) -> Self {
Self {
millis: secs as u128 * 1000,
}
}
/// Construct a local time from milliseconds since Epoch.
pub const fn from_millis(millis: u128) -> Self {
Self { millis }
}
/// Return whole seconds since Epoch.
pub fn as_secs(&self) -> u64 {
(self.millis / 1000).try_into().unwrap()
}
/// Return milliseconds since Epoch.
pub fn as_millis(&self) -> u64 {
self.millis.try_into().unwrap()
}
/// Get the duration since the given time.
///
/// # Panics
///
/// This function will panic if `earlier` is later than `self`.
pub fn duration_since(&self, earlier: LocalTime) -> LocalDuration {
LocalDuration::from_millis(
self.millis
.checked_sub(earlier.millis)
.expect("supplied time is later than self"),
)
}
/// Get the difference between two times.
pub fn diff(&self, other: LocalTime) -> LocalDuration {
if self > &other {
self.duration_since(other)
} else {
other.duration_since(*self)
}
}
/// Elapse time.
///
/// Adds the given duration to the time.
pub fn elapse(&mut self, duration: LocalDuration) {
self.millis += duration.as_millis()
}
}
/// Convert a `SystemTime` into a local time.
impl From<SystemTime> for LocalTime {
fn from(system: SystemTime) -> Self {
let millis = system.duration_since(UNIX_EPOCH).unwrap().as_millis();
Self { millis }
}
}
/// Subtract two local times. Yields a duration.
impl std::ops::Sub<LocalTime> for LocalTime {
type Output = LocalDuration;
fn sub(self, other: LocalTime) -> LocalDuration {
LocalDuration(self.millis.saturating_sub(other.millis))
}
}
/// Subtract a duration from a local time. Yields a local time.
impl std::ops::Sub<LocalDuration> for LocalTime {
type Output = LocalTime;
fn sub(self, other: LocalDuration) -> LocalTime {
LocalTime {
millis: self.millis - other.0,
}
}
}
/// Add a duration to a local time. Yields a local time.
impl std::ops::Add<LocalDuration> for LocalTime {
type Output = LocalTime;
fn add(self, other: LocalDuration) -> LocalTime {
LocalTime {
millis: self.millis + other.0,
}
}
}
/// Time duration as measured locally.
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)]
#[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(description = "A time duration measured locally in seconds.")
)]
pub struct LocalDuration(u128);
impl LocalDuration {
/// The time interval between blocks. The "block time".
pub const BLOCK_INTERVAL: LocalDuration = Self::from_mins(10);
/// Maximum duration.
pub const MAX: LocalDuration = LocalDuration(u128::MAX);
/// Create a new duration from whole seconds.
pub const fn from_secs(secs: u64) -> Self {
Self(secs as u128 * 1000)
}
/// Create a new duration from whole minutes.
pub const fn from_mins(mins: u64) -> Self {
Self::from_secs(mins * 60)
}
/// Construct a new duration from milliseconds.
pub const fn from_millis(millis: u128) -> Self {
Self(millis)
}
/// Return the number of minutes in this duration.
pub const fn as_mins(&self) -> u64 {
self.as_secs() / 60
}
/// Return the number of seconds in this duration.
pub const fn as_secs(&self) -> u64 {
(self.0 / 1000) as u64
}
/// Return the number of milliseconds in this duration.
pub const fn as_millis(&self) -> u128 {
self.0
}
}
impl std::fmt::Display for LocalDuration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.as_millis() < 1000 {
write!(f, "{} millisecond(s)", self.as_millis())
} else if self.as_secs() < 60 {
let fraction = self.as_millis() % 1000;
if fraction > 0 {
write!(f, "{}.{} second(s)", self.as_secs(), fraction)
} else {
write!(f, "{} second(s)", self.as_secs())
}
} else if self.as_mins() < 60 {
let fraction = self.as_secs() % 60;
if fraction > 0 {
write!(
f,
"{:.2} minute(s)",
self.as_mins() as f64 + (fraction as f64 / 60.)
)
} else {
write!(f, "{} minute(s)", self.as_mins())
}
} else {
let fraction = self.as_mins() % 60;
if fraction > 0 {
write!(f, "{:.2} hour(s)", self.as_mins() as f64 / 60.)
} else {
write!(f, "{} hour(s)", self.as_mins() / 60)
}
}
}
}
impl<'a> std::iter::Sum<&'a LocalDuration> for LocalDuration {
fn sum<I: Iterator<Item = &'a LocalDuration>>(iter: I) -> LocalDuration {
let mut total: u128 = 0;
for entry in iter {
total = total
.checked_add(entry.0)
.expect("iter::sum should not overflow");
}
Self(total)
}
}
impl std::ops::Add<LocalDuration> for LocalDuration {
type Output = LocalDuration;
fn add(self, other: LocalDuration) -> LocalDuration {
LocalDuration(self.0 + other.0)
}
}
impl std::ops::Div<u32> for LocalDuration {
type Output = LocalDuration;
fn div(self, other: u32) -> LocalDuration {
LocalDuration(self.0 / other as u128)
}
}
impl std::ops::Mul<u64> for LocalDuration {
type Output = LocalDuration;
fn mul(self, other: u64) -> LocalDuration {
LocalDuration(self.0 * other as u128)
}
}
impl From<LocalDuration> for std::time::Duration {
fn from(other: LocalDuration) -> Self {
std::time::Duration::from_millis(other.0 as u64)
}
}
#[cfg(feature = "serde")]
mod serde_impls {
use super::{LocalDuration, LocalTime};
impl serde::Serialize for LocalTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(self.as_secs())
}
}
impl<'de> serde::Deserialize<'de> for LocalTime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
u64::deserialize(deserializer).map(LocalTime::from_secs)
}
}
impl serde::Serialize for LocalDuration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(self.as_secs())
}
}
impl<'de> serde::Deserialize<'de> for LocalDuration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
u64::deserialize(deserializer).map(LocalDuration::from_secs)
}
}
#[cfg(test)]
mod test {
use crate::LocalTime;
#[test]
fn test_localtime() {
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq)]
struct Test {
time: LocalTime,
}
let value = Test {
time: LocalTime::from_secs(1699636852107),
};
assert_eq!(
serde_json::from_str::<Test>(r#"{"time":1699636852107}"#).unwrap(),
value
);
assert_eq!(
serde_json::from_str::<Test>(serde_json::to_string(&value).unwrap().as_str())
.unwrap(),
value
);
}
}
}

View File

@ -25,12 +25,12 @@ cyphernet = { workspace = true, features = ["tor", "dns", "ed25519", "p2p-ed2551
fastrand = { workspace = true }
lexopt = { workspace = true }
log = { workspace = true, features = ["kv", "std"] }
localtime = { workspace = true }
mio = { version = "1", features = ["net", "os-poll"] }
nonempty = { workspace = true, features = ["serialize"] }
qcheck = { workspace = true, optional = true }
radicle = { workspace = true, features = ["logger"] }
radicle-fetch = { workspace = true }
radicle-localtime = { workspace = true }
radicle-protocol = { workspace = true }
radicle-signals = { workspace = true }
sqlite = { workspace = true, features = ["bundled"] }

View File

@ -17,6 +17,8 @@ pub mod test;
#[cfg(test)]
pub mod tests;
extern crate radicle_localtime as localtime;
use std::str::FromStr;
use std::sync::LazyLock;

View File

@ -18,11 +18,11 @@ crossbeam-channel = { workspace = true }
cyphernet = { workspace = true, features = ["tor"] }
fastrand = { workspace = true }
log = { workspace = true, features = ["std"] }
localtime = { workspace = true }
nonempty = { workspace = true, features = ["serialize"] }
qcheck = { workspace = true, optional = true }
radicle = { workspace = true, features = ["logger"] }
radicle-fetch = { workspace = true }
radicle-localtime = { workspace = true }
sqlite = { workspace = true, features = ["bundled"] }
scrypt = { version = "0.11.0", default-features = false }
serde = { workspace = true, features = ["derive"] }

View File

@ -6,3 +6,5 @@ pub mod worker;
/// Peer-to-peer protocol version.
pub const PROTOCOL_VERSION: u8 = 1;
extern crate radicle_localtime as localtime;

View File

@ -13,7 +13,7 @@ rust-version.workspace = true
default = []
test = ["tempfile", "qcheck", "radicle-crypto/test", "radicle-cob/test"]
logger = ["colored", "chrono"]
schemars = ["radicle-oid/schemars", "dep:schemars"]
schemars = ["radicle-oid/schemars", "radicle-localtime/schemars", "dep:schemars"]
[dependencies]
amplify = { workspace = true, features = ["std"] }
@ -28,7 +28,6 @@ fast-glob = { version = "0.3.2" }
fastrand = { workspace = true, features = ["std"] }
git2 = { workspace = true, features = ["vendored-libgit2"] }
indexmap = { version = "2", features = ["serde"] }
localtime = { workspace = true, features = ["serde"] }
log = { workspace = true, features = ["std"] }
multibase = { workspace = true }
nonempty = { workspace = true, features = ["serialize"] }
@ -36,6 +35,7 @@ qcheck = { workspace = true, optional = true }
radicle-cob = { workspace = true, features = ["git2"] }
radicle-crypto = { workspace = true, features = ["git-ref-format-core", "ssh", "sqlite", "cyphernet"] }
radicle-git-ref-format = { workspace = true, features = ["macro", "serde"] }
radicle-localtime = { workspace = true, features = ["serde"] }
radicle-oid = { workspace = true, features = ["git2", "serde", "std", "sha1"] }
radicle-ssh = { workspace = true }
schemars = { workspace = true, optional = true, features = ["derive", "std"] }

View File

@ -13,10 +13,29 @@ use crate::git::Oid;
use crate::prelude::{Did, PublicKey};
/// Timestamp used for COB operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Timestamp(LocalTime);
impl Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(self.0.as_millis())
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
u128::deserialize(deserializer)
.map(LocalTime::from_millis)
.map(Self)
}
}
impl Timestamp {
pub fn from_secs(secs: u64) -> Self {
Self(LocalTime::from_secs(secs))

View File

@ -8,6 +8,8 @@ pub extern crate radicle_crypto as crypto;
#[macro_use]
extern crate amplify;
extern crate radicle_localtime as localtime;
mod canonical;
pub mod cli;

View File

@ -102,11 +102,6 @@ pub enum State {
#[serde(rename_all = "camelCase")]
Connected {
/// Connected since this time.
#[serde(with = "crate::serde_ext::localtime::time")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDurationInSeconds")
)]
since: LocalTime,
/// Ping state.
#[serde(skip)]
@ -124,18 +119,8 @@ pub enum State {
#[serde(rename_all = "camelCase")]
Disconnected {
/// Since when has this peer been disconnected.
#[serde(with = "crate::serde_ext::localtime::time")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDurationInSeconds")
)]
since: LocalTime,
/// When to retry the connection.
#[serde(with = "crate::serde_ext::localtime::time")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDurationInSeconds")
)]
retry_at: LocalTime,
},
}

View File

@ -150,18 +150,8 @@ pub struct KnownAddress {
/// Address of the peer who sent us this address.
pub source: Source,
/// Last time this address was used to successfully connect to a peer.
#[serde(with = "crate::serde_ext::localtime::option::time")]
#[cfg_attr(
feature = "schemars",
schemars(with = "Option<crate::schemars_ext::localtime::LocalDurationInSeconds>")
)]
pub last_success: Option<LocalTime>,
/// Last time this address was tried.
#[serde(with = "crate::serde_ext::localtime::option::time")]
#[cfg_attr(
feature = "schemars",
schemars(with = "Option<crate::schemars_ext::localtime::LocalDurationInSeconds>")
)]
pub last_attempt: Option<LocalTime>,
/// Whether this address has been banned.
pub banned: bool,

View File

@ -548,14 +548,7 @@ impl From<LogLevel> for log::Level {
#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LimitRoutingMaxAge(
#[serde(with = "crate::serde_ext::localtime::duration")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDuration")
)]
localtime::LocalDuration,
);
pub struct LimitRoutingMaxAge(localtime::LocalDuration);
impl Default for LimitRoutingMaxAge {
fn default() -> Self {
@ -578,14 +571,7 @@ impl From<LocalDuration> for LimitRoutingMaxAge {
#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LimitGossipMaxAge(
#[serde(with = "crate::serde_ext::localtime::duration")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDuration")
)]
localtime::LocalDuration,
);
pub struct LimitGossipMaxAge(localtime::LocalDuration);
impl Default for LimitGossipMaxAge {
fn default() -> Self {

View File

@ -15,11 +15,6 @@ pub struct SyncedAt {
/// Head of `rad/sigrefs`.
pub oid: crate::git::Oid,
/// When these refs were synced.
#[serde(with = "crate::serde_ext::localtime::time")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDurationInSeconds")
)]
pub timestamp: LocalTime,
}

View File

@ -69,26 +69,6 @@ pub(crate) mod bytesize {
);
}
pub(crate) mod localtime {
use super::*;
/// See [`::localtime::LocalDuration`]
#[derive(JsonSchema)]
#[schemars(
remote = "localtime::LocalDuration",
description = "A time duration measured locally in milliseconds."
)]
pub(crate) struct LocalDuration(u64);
/// See [`crate::serde_ext::localtime::time`]
#[derive(JsonSchema)]
#[schemars(
remote = "localtime::LocalDuration",
description = "A time duration measured locally in seconds."
)]
pub(crate) struct LocalDurationInSeconds(u64);
}
pub(crate) mod git {
pub(crate) mod fmt {
/// See [`crate::git::fmt::RefString`]

View File

@ -31,80 +31,6 @@ pub mod string {
}
}
/// Unlike the default `serde` instances from `localtime`, this encodes and decodes using seconds
/// instead of milliseconds.
pub mod localtime {
pub mod time {
use localtime::LocalTime;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(value: &LocalTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(value.as_secs())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalTime, D::Error>
where
D: Deserializer<'de>,
{
let seconds = u64::deserialize(deserializer)?;
Ok(LocalTime::from_secs(seconds))
}
}
pub mod option {
pub mod time {
use localtime::LocalTime;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(value: &Option<LocalTime>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(time) => serializer.serialize_some(&time.as_secs()),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<LocalTime>, D::Error>
where
D: Deserializer<'de>,
{
let option = Option::<u64>::deserialize(deserializer)?;
match option {
Some(seconds) => Ok(Some(LocalTime::from_secs(seconds))),
None => Ok(None),
}
}
}
}
pub mod duration {
use localtime::LocalDuration;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(value: &LocalDuration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(value.as_secs())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalDuration, D::Error>
where
D: Deserializer<'de>,
{
let seconds = u64::deserialize(deserializer)?;
Ok(LocalDuration::from_secs(seconds))
}
}
}
/// Return true if the given value is the default for that type.
pub fn is_default<T: Default + PartialEq>(t: &T) -> bool {
t == &T::default()
@ -119,53 +45,3 @@ where
let v: serde_json::Value = serde::Deserialize::deserialize(deserializer)?;
Ok(T::deserialize(v).unwrap_or_default())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
use ::localtime::LocalTime;
#[test]
fn test_localtime() {
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq)]
struct Test {
time: LocalTime,
}
let value = Test {
time: LocalTime::from_millis(1699636852107),
};
assert_eq!(
serde_json::from_str::<Test>(r#"{"time":1699636852107}"#).unwrap(),
value
);
assert_eq!(
serde_json::from_str::<Test>(serde_json::to_string(&value).unwrap().as_str()).unwrap(),
value
);
}
#[test]
// Tests serialization into seconds instead of milliseconds.
fn test_localtime_ext() {
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq)]
struct Test {
#[serde(with = "localtime::time")]
time: LocalTime,
}
let value = Test {
time: LocalTime::from_secs(1699636852107),
};
assert_eq!(
serde_json::from_str::<Test>(r#"{"time":1699636852107}"#).unwrap(),
value
);
assert_eq!(
serde_json::from_str::<Test>(serde_json::to_string(&value).unwrap().as_str()).unwrap(),
value
);
}
}