Skip to content

Commit 7830d8e

Browse files
authored
feat: Add Tid::now and Tid::from_datetime constructors (#277)
* Add `Tid::now()` constructor * Add `Tid::from_datetime` * Address review comments * Rewrite `clkid` documentation to remove reference to clocks 0-31 * Add a warning about `Tid::now` returning the same value more than once
1 parent ceac515 commit 7830d8e

File tree

1 file changed

+63
-1
lines changed

1 file changed

+63
-1
lines changed

atrium-api/src/types/string.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ use regex::Regex;
99
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
1010
use std::{cmp, ops::Deref, str::FromStr, sync::OnceLock};
1111

12+
use super::LimitedU32;
13+
14+
// Reference: https://github.com/bluesky-social/indigo/blob/9e3b84fdbb20ca4ac397a549e1c176b308f7a6e1/repo/tid.go#L11-L19
15+
fn s32_encode(mut i: u64) -> String {
16+
const S32_CHAR: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
17+
18+
let mut s = String::with_capacity(13);
19+
for _ in 0..13 {
20+
let c = i & 0x1F;
21+
s.push(S32_CHAR[c as usize] as char);
22+
23+
i >>= 5;
24+
}
25+
26+
// Reverse the string to convert it to big-endian format.
27+
s.chars().rev().collect()
28+
}
29+
1230
/// Common trait implementations for Lexicon string formats that are newtype wrappers
1331
/// around `String`.
1432
macro_rules! string_newtype {
@@ -410,7 +428,7 @@ impl Serialize for Language {
410428

411429
/// A [Timestamp Identifier].
412430
///
413-
/// [Timestamp Identifier]: https://atproto.com/specs/record-key#record-key-type-tid
431+
/// [Timestamp Identifier]: https://atproto.com/specs/tid
414432
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
415433
#[serde(transparent)]
416434
pub struct Tid(String);
@@ -436,6 +454,35 @@ impl Tid {
436454
}
437455
}
438456

457+
/// Construct a new timestamp with the specified clock ID.
458+
///
459+
/// If you have multiple clock sources, you can use `clkid` to distinguish between them
460+
/// and hint to other implementations that the timestamp cannot be compared with other
461+
/// timestamps from other sources.
462+
/// If you are only using a single clock source, you can just specify `0` for `clkid`.
463+
pub fn from_datetime(clkid: LimitedU32<1023>, time: chrono::DateTime<chrono::Utc>) -> Self {
464+
let time = time.timestamp_micros() as u64;
465+
466+
// The TID is laid out as follows:
467+
// 0TTTTTTTTTTTTTTT TTTTTTTTTTTTTTTT TTTTTTTTTTTTTTTT TTTTTTCCCCCCCCCC
468+
let tid = (time << 10) & 0x7FFF_FFFF_FFFF_FC00 | (Into::<u32>::into(clkid) as u64 & 0x3FF);
469+
Self(s32_encode(tid))
470+
}
471+
472+
/// Construct a new [Tid] that represents the current time.
473+
///
474+
/// If you have multiple clock sources, you can use `clkid` to distinguish between them
475+
/// and hint to other implementations that the timestamp cannot be compared with other
476+
/// timestamps from other sources.
477+
/// If you are only using a single clock source, you can just specify `0` for `clkid`.
478+
///
479+
/// _Warning:_ It's possible that this function will return the same time more than once.
480+
/// If it's important that these values be unique, you will want to repeatedly call this
481+
/// function until a different time is returned.
482+
pub fn now(clkid: LimitedU32<1023>) -> Self {
483+
Self::from_datetime(clkid, chrono::Utc::now())
484+
}
485+
439486
/// Returns the TID as a string slice.
440487
pub fn as_str(&self) -> &str {
441488
self.0.as_str()
@@ -766,6 +813,21 @@ mod tests {
766813
}
767814
}
768815

816+
#[test]
817+
fn tid_encode() {
818+
assert_eq!(s32_encode(0), "2222222222222");
819+
assert_eq!(s32_encode(1), "2222222222223");
820+
}
821+
822+
#[test]
823+
fn tid_construct() {
824+
let tid = Tid::from_datetime(
825+
0.try_into().unwrap(),
826+
chrono::DateTime::from_timestamp(1738430999, 0).unwrap(),
827+
);
828+
assert_eq!(tid.as_str(), "3lh5234mwy222");
829+
}
830+
769831
#[test]
770832
fn valid_tid() {
771833
for valid in ["3jzfcijpj2z2a", "7777777777777", "3zzzzzzzzzzzz"] {

0 commit comments

Comments
 (0)