Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
localtime: localise the localtime dependency
Merged fintohaps opened 4 months ago

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.

21 files changed +400 -216 3168107d d98033a1
modified Cargo.lock
@@ -2169,15 +2169,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -2982,6 +2973,15 @@ dependencies = [
]

[[package]]
+
name = "radicle-localtime"
+
version = "0.1.0"
+
dependencies = [
+
 "schemars",
+
 "serde",
+
 "serde_json",
+
]
+

+
[[package]]
name = "radicle-node"
version = "0.16.0"
dependencies = [
@@ -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",
modified Cargo.toml
@@ -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" }
modified crates/radicle-cli/Cargo.toml
@@ -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]
modified crates/radicle-cli/src/lib.rs
@@ -9,3 +9,5 @@ pub mod project;
pub mod terminal;

mod warning;
+

+
extern crate radicle_localtime as localtime;
modified crates/radicle-cli/tests/commands.rs
@@ -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"));

modified crates/radicle-cli/tests/util/environment.rs
@@ -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};

added crates/radicle-localtime/Cargo.toml
@@ -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 }

\ No newline at end of file
added crates/radicle-localtime/src/lib.rs
@@ -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
+
            );
+
        }
+
    }
+
}
modified crates/radicle-node/Cargo.toml
@@ -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"] }
modified crates/radicle-node/src/lib.rs
@@ -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;

modified crates/radicle-protocol/Cargo.toml
@@ -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"] }
modified crates/radicle-protocol/src/lib.rs
@@ -6,3 +6,5 @@ pub mod worker;

/// Peer-to-peer protocol version.
pub const PROTOCOL_VERSION: u8 = 1;
+

+
extern crate radicle_localtime as localtime;
modified crates/radicle/Cargo.toml
@@ -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"] }
modified crates/radicle/src/cob/common.rs
@@ -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))
modified crates/radicle/src/lib.rs
@@ -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;
modified crates/radicle/src/node.rs
@@ -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,
    },
}
modified crates/radicle/src/node/address.rs
@@ -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,
modified crates/radicle/src/node/config.rs
@@ -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 {
modified crates/radicle/src/node/seed.rs
@@ -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,
}

modified crates/radicle/src/schemars_ext.rs
@@ -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`]
modified crates/radicle/src/serde_ext.rs
@@ -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
-
        );
-
    }
-
}