auxon_sdk/api/
types.rs

1use std::{borrow::Cow, cmp::Ordering, ops::Deref, str::FromStr};
2
3use ordered_float::OrderedFloat;
4pub use uuid::Uuid;
5
6#[derive(
7    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
8)]
9pub struct AttrKey(Cow<'static, str>);
10
11impl AttrKey {
12    pub const fn new(k: String) -> Self {
13        Self(Cow::Owned(k))
14    }
15
16    pub const fn new_static(k: &'static str) -> Self {
17        Self(Cow::Borrowed(k))
18    }
19}
20
21impl From<&str> for AttrKey {
22    fn from(s: &str) -> Self {
23        AttrKey(Cow::from(s.to_owned()))
24    }
25}
26
27impl From<String> for AttrKey {
28    fn from(s: String) -> Self {
29        AttrKey(Cow::from(s))
30    }
31}
32
33impl AsRef<str> for AttrKey {
34    fn as_ref(&self) -> &str {
35        self.0.as_ref()
36    }
37}
38
39impl From<AttrKey> for String {
40    fn from(k: AttrKey) -> Self {
41        match k.0 {
42            Cow::Borrowed(b) => b.to_owned(),
43            Cow::Owned(o) => o,
44        }
45    }
46}
47
48impl std::fmt::Display for AttrKey {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
50        write!(f, "{}", self.0)
51    }
52}
53
54////////////
55// BigInt //
56////////////
57
58/// Newtype wrapper to get correct-by-construction promises
59/// about minimal AttrVal variant selection.
60#[derive(
61    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
62)]
63pub struct BigInt(Box<i128>);
64
65impl BigInt {
66    pub fn new_attr_val(big_i: i128) -> AttrVal {
67        // Store it as an Integer if it's small enough
68        if big_i < (i64::MIN as i128) || big_i > (i64::MAX as i128) {
69            AttrVal::BigInt(BigInt(Box::new(big_i)))
70        } else {
71            AttrVal::Integer(big_i as i64)
72        }
73    }
74}
75impl std::fmt::Display for BigInt {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        self.0.fmt(f)
78    }
79}
80
81impl AsRef<i128> for BigInt {
82    fn as_ref(&self) -> &i128 {
83        self.0.as_ref()
84    }
85}
86
87impl Deref for BigInt {
88    type Target = i128;
89
90    fn deref(&self) -> &Self::Target {
91        self.0.as_ref()
92    }
93}
94
95/////////////////
96// Nanoseconds //
97/////////////////
98
99/// A timestamp in nanoseconds
100#[derive(
101    Copy, Clone, Eq, PartialEq, Debug, Ord, PartialOrd, Hash, serde::Serialize, serde::Deserialize,
102)]
103#[repr(transparent)]
104#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
105pub struct Nanoseconds(u64);
106
107impl Nanoseconds {
108    pub fn get_raw(&self) -> u64 {
109        self.0
110    }
111}
112
113impl From<u64> for Nanoseconds {
114    fn from(n: u64) -> Self {
115        Nanoseconds(n)
116    }
117}
118
119impl std::fmt::Display for Nanoseconds {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        write!(f, "{}ns", self.0)
122    }
123}
124
125impl FromStr for Nanoseconds {
126    type Err = std::num::ParseIntError;
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        Ok(Nanoseconds(s.parse::<u64>()?))
129    }
130}
131
132/////////////////
133// LogicalTime //
134/////////////////
135
136/// A segmented logical clock
137#[derive(Eq, PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)]
138#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
139#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
140pub struct LogicalTime(Box<[u64; 4]>);
141
142impl LogicalTime {
143    pub fn unary<A: Into<u64>>(a: A) -> Self {
144        LogicalTime(Box::new([0, 0, 0, a.into()]))
145    }
146
147    pub fn binary<A: Into<u64>, B: Into<u64>>(a: A, b: B) -> Self {
148        LogicalTime(Box::new([0, 0, a.into(), b.into()]))
149    }
150
151    pub fn trinary<A: Into<u64>, B: Into<u64>, C: Into<u64>>(a: A, b: B, c: C) -> Self {
152        LogicalTime(Box::new([0, a.into(), b.into(), c.into()]))
153    }
154
155    pub fn quaternary<A: Into<u64>, B: Into<u64>, C: Into<u64>, D: Into<u64>>(
156        a: A,
157        b: B,
158        c: C,
159        d: D,
160    ) -> Self {
161        LogicalTime(Box::new([a.into(), b.into(), c.into(), d.into()]))
162    }
163
164    pub fn get_raw(&self) -> &[u64; 4] {
165        &self.0
166    }
167}
168
169#[cfg(feature = "pyo3")]
170#[pyo3::pymethods]
171impl LogicalTime {
172    #[staticmethod]
173    #[pyo3(name = "unary")]
174    pub fn unary_py(a: u64) -> Self {
175        LogicalTime(Box::new([0, 0, 0, a]))
176    }
177
178    #[staticmethod]
179    #[pyo3(name = "binary")]
180    pub fn binary_py(a: u64, b: u64) -> Self {
181        LogicalTime(Box::new([0, 0, a, b]))
182    }
183
184    #[staticmethod]
185    #[pyo3(name = "trinary")]
186    pub fn trinary_py(a: u64, b: u64, c: u64) -> Self {
187        LogicalTime(Box::new([0, a, b, c]))
188    }
189
190    #[staticmethod]
191    #[pyo3(name = "quaternary")]
192    pub fn quaternary_py(a: u64, b: u64, c: u64, d: u64) -> Self {
193        LogicalTime(Box::new([a, b, c, d]))
194    }
195
196    pub fn as_tuple(&self) -> (u64, u64, u64, u64) {
197        (self.0[0], self.0[1], self.0[2], self.0[3])
198    }
199
200    pub fn as_array(&self) -> [u64; 4] {
201        *(self.0)
202    }
203}
204
205impl Ord for LogicalTime {
206    fn cmp(&self, other: &Self) -> Ordering {
207        for (a, b) in self.0.iter().zip(other.0.iter()) {
208            match a.cmp(b) {
209                Ordering::Equal => (), // continue to later segments
210                Ordering::Less => return Ordering::Less,
211                Ordering::Greater => return Ordering::Greater,
212            }
213        }
214
215        Ordering::Equal
216    }
217}
218
219impl PartialOrd for LogicalTime {
220    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
221        Some(self.cmp(other))
222    }
223}
224
225impl std::fmt::Display for LogicalTime {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        write!(f, "{}:{}:{}:{}", self.0[0], self.0[1], self.0[2], self.0[3])
228    }
229}
230
231impl FromStr for LogicalTime {
232    type Err = ();
233    fn from_str(s: &str) -> Result<Self, Self::Err> {
234        let mut segments = s.rsplit(':');
235
236        if let Ok(mut time) = segments.try_fold(Vec::new(), |mut acc, segment| {
237            segment.parse::<u64>().map(|t| {
238                acc.insert(0, t);
239                acc
240            })
241        }) {
242            while time.len() < 4 {
243                time.insert(0, 0)
244            }
245
246            let time_array = time.into_boxed_slice().try_into().map_err(|_| ())?;
247
248            Ok(LogicalTime(time_array))
249        } else {
250            Err(())
251        }
252    }
253}
254
255////////////////
256// TimelineId //
257////////////////
258
259pub const TIMELINE_ID_SIGIL: char = '%';
260
261/// Timelines are identified by a UUID. These are timeline *instances*; a given location (identified
262/// by its name) is associated with many timelines.
263#[derive(
264    Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Debug, serde::Serialize, serde::Deserialize,
265)]
266#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
267#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
268pub struct TimelineId(Uuid);
269
270#[cfg(feature = "pyo3")]
271#[pyo3::pymethods]
272impl TimelineId {
273    #[staticmethod]
274    pub fn zero_py() -> Self {
275        TimelineId(Uuid::nil())
276    }
277
278    #[staticmethod]
279    #[pyo3(name = "allocate")]
280    pub fn allocate_py() -> Self {
281        TimelineId(Uuid::new_v4())
282    }
283
284    fn __eq__(&self, other: &Self) -> bool {
285        self.0 == other.0
286    }
287
288    fn __hash__(&self) -> u64 {
289        use std::hash::Hash as _;
290        use std::hash::Hasher as _;
291        let mut hasher = std::hash::DefaultHasher::new();
292        self.hash(&mut hasher);
293        hasher.finish()
294    }
295}
296
297impl TimelineId {
298    pub fn zero() -> Self {
299        TimelineId(Uuid::nil())
300    }
301
302    pub fn allocate() -> Self {
303        TimelineId(Uuid::new_v4())
304    }
305
306    pub fn get_raw(&self) -> &Uuid {
307        &self.0
308    }
309}
310
311impl From<Uuid> for TimelineId {
312    fn from(uuid: Uuid) -> Self {
313        TimelineId(uuid)
314    }
315}
316
317impl std::fmt::Display for TimelineId {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        self.0.fmt(f)
320    }
321}
322
323/////////////////////
324// EventCoordinate //
325/////////////////////
326
327pub type OpaqueEventId = [u8; 16];
328
329#[derive(
330    Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
331)]
332#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
333#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
334pub struct EventCoordinate {
335    pub timeline_id: TimelineId,
336    pub id: OpaqueEventId,
337}
338
339impl EventCoordinate {
340    pub fn as_bytes(&self) -> [u8; 32] {
341        let mut bytes = [0u8; 32];
342        bytes[0..16].copy_from_slice(self.timeline_id.0.as_bytes());
343        bytes[16..32].copy_from_slice(&self.id);
344        bytes
345    }
346
347    pub fn from_byte_slice(bytes: &[u8]) -> Option<Self> {
348        if bytes.len() != 32 {
349            return None;
350        }
351
352        Some(EventCoordinate {
353            timeline_id: Uuid::from_slice(&bytes[0..16]).ok()?.into(),
354            id: bytes[16..32].try_into().ok()?,
355        })
356    }
357}
358
359impl std::fmt::Display for EventCoordinate {
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        write!(f, "{TIMELINE_ID_SIGIL}")?;
362
363        // print the uuid as straight hex, for compactness
364        for byte in self.timeline_id.0.as_bytes() {
365            write!(f, "{byte:02x}")?;
366        }
367
368        write!(f, ":{}", EncodeHexWithoutLeadingZeroes(&self.id))
369    }
370}
371
372pub struct EncodeHexWithoutLeadingZeroes<'a>(pub &'a [u8]);
373
374impl<'a> std::fmt::Display for EncodeHexWithoutLeadingZeroes<'a> {
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        let mut cursor = 0;
377        let bytes = self.0;
378        while bytes[cursor] == 0 && cursor < bytes.len() - 1 {
379            cursor += 1;
380        }
381
382        if cursor == bytes.len() {
383            write!(f, "0")?;
384        } else {
385            for byte in bytes.iter().skip(cursor) {
386                write!(f, "{byte:02x}")?;
387            }
388        }
389
390        Ok(())
391    }
392}
393
394/////////////
395// AttrVal //
396/////////////
397
398#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
399pub enum AttrVal {
400    TimelineId(Box<TimelineId>),
401    EventCoordinate(Box<EventCoordinate>),
402    String(Cow<'static, str>),
403    Integer(i64),
404    BigInt(BigInt),
405    Float(OrderedFloat<f64>),
406    Bool(bool),
407    Timestamp(Nanoseconds),
408    LogicalTime(LogicalTime),
409}
410
411impl AttrVal {
412    pub fn attr_type(&self) -> AttrType {
413        match self {
414            AttrVal::TimelineId(_) => AttrType::TimelineId,
415            AttrVal::EventCoordinate(_) => AttrType::EventCoordinate,
416            AttrVal::String(_) => AttrType::String,
417            AttrVal::Integer(_) => AttrType::Integer,
418            AttrVal::BigInt(_) => AttrType::BigInt,
419            AttrVal::Float(_) => AttrType::Float,
420            AttrVal::Bool(_) => AttrType::Bool,
421            AttrVal::Timestamp(_) => AttrType::Nanoseconds,
422            AttrVal::LogicalTime(_) => AttrType::LogicalTime,
423        }
424    }
425
426    pub fn as_timeline_id(self) -> std::result::Result<TimelineId, WrongAttrTypeError> {
427        self.try_into()
428    }
429
430    pub fn as_event_coordinate(self) -> std::result::Result<EventCoordinate, WrongAttrTypeError> {
431        self.try_into()
432    }
433
434    pub fn as_string(self) -> std::result::Result<Cow<'static, str>, WrongAttrTypeError> {
435        self.try_into()
436    }
437
438    pub fn as_int(self) -> std::result::Result<i64, WrongAttrTypeError> {
439        self.try_into()
440    }
441
442    pub fn as_bigint(self) -> std::result::Result<i128, WrongAttrTypeError> {
443        self.try_into()
444    }
445
446    pub fn as_float(self) -> std::result::Result<f64, WrongAttrTypeError> {
447        self.try_into()
448    }
449
450    pub fn as_bool(self) -> std::result::Result<bool, WrongAttrTypeError> {
451        self.try_into()
452    }
453
454    pub fn as_timestamp(self) -> std::result::Result<Nanoseconds, WrongAttrTypeError> {
455        self.try_into()
456    }
457
458    pub fn as_logical_time(self) -> std::result::Result<LogicalTime, WrongAttrTypeError> {
459        self.try_into()
460    }
461}
462
463impl std::fmt::Display for AttrVal {
464    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
465        match self {
466            AttrVal::String(s) => s.fmt(f),
467            AttrVal::Integer(i) => i.fmt(f),
468            AttrVal::BigInt(bi) => bi.fmt(f),
469            AttrVal::Float(fp) => fp.fmt(f),
470            AttrVal::Bool(b) => b.fmt(f),
471            AttrVal::Timestamp(ns) => ns.fmt(f),
472            AttrVal::LogicalTime(lt) => lt.fmt(f),
473            AttrVal::EventCoordinate(ec) => ec.fmt(f),
474            AttrVal::TimelineId(tid) => tid.fmt(f),
475        }
476    }
477}
478
479impl FromStr for AttrVal {
480    type Err = std::convert::Infallible;
481
482    fn from_str(s: &str) -> Result<Self, Self::Err> {
483        // N.B. Eventually we will want  parsing that is informed by the AttrKey, that will allow
484        // us to parse things like `AttrVal::Timestamp` or a uniary `AttrVal::LogicalTime` which
485        // are both currently parsed as (Big)Int
486        Ok(if let Ok(v) = s.to_lowercase().parse::<bool>() {
487            v.into()
488        } else if let Ok(v) = s.parse::<i128>() {
489            // this will decide if the number should be `Integer` or `BigInt` based on value
490            v.into()
491        } else if let Ok(v) = s.parse::<f64>() {
492            v.into()
493        } else if let Ok(v) = s.parse::<LogicalTime>() {
494            v.into()
495        } else if let Ok(v) = s.parse::<Uuid>() {
496            v.into()
497        } else {
498            // N.B. This will trim any number of leading and trailing single or double quotes, It
499            // does not have any ability to escape quote marks.
500            AttrVal::String(s.trim_matches(|c| c == '"' || c == '\'').to_owned().into())
501        })
502    }
503}
504
505impl From<String> for AttrVal {
506    fn from(s: String) -> AttrVal {
507        AttrVal::String(Cow::Owned(s))
508    }
509}
510
511impl From<&str> for AttrVal {
512    fn from(s: &str) -> AttrVal {
513        AttrVal::String(Cow::Owned(s.to_owned()))
514    }
515}
516
517impl From<Cow<'static, str>> for AttrVal {
518    fn from(s: Cow<'static, str>) -> Self {
519        AttrVal::String(s)
520    }
521}
522
523impl From<&String> for AttrVal {
524    fn from(s: &String) -> Self {
525        AttrVal::String(Cow::Owned(s.clone()))
526    }
527}
528
529impl From<bool> for AttrVal {
530    fn from(b: bool) -> AttrVal {
531        AttrVal::Bool(b)
532    }
533}
534
535impl From<Nanoseconds> for AttrVal {
536    fn from(ns: Nanoseconds) -> AttrVal {
537        AttrVal::Timestamp(ns)
538    }
539}
540
541impl From<LogicalTime> for AttrVal {
542    fn from(lt: LogicalTime) -> AttrVal {
543        AttrVal::LogicalTime(lt)
544    }
545}
546
547impl From<Uuid> for AttrVal {
548    fn from(u: Uuid) -> AttrVal {
549        AttrVal::TimelineId(Box::new(u.into()))
550    }
551}
552
553impl From<EventCoordinate> for AttrVal {
554    fn from(coord: EventCoordinate) -> Self {
555        AttrVal::EventCoordinate(Box::new(coord))
556    }
557}
558
559impl From<TimelineId> for AttrVal {
560    fn from(timeline_id: TimelineId) -> Self {
561        AttrVal::TimelineId(Box::new(timeline_id))
562    }
563}
564
565#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug, PartialOrd, Ord)]
566pub enum AttrType {
567    TimelineId,
568    EventCoordinate,
569    String,
570    Integer,
571    BigInt,
572    Float,
573    Bool,
574    Nanoseconds,
575    LogicalTime,
576    Any,
577}
578
579impl std::fmt::Display for AttrType {
580    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
581        match self {
582            AttrType::TimelineId => "TimelineId",
583            AttrType::String => "String",
584            AttrType::Integer => "Integer",
585            AttrType::BigInt => "BigInteger",
586            AttrType::Float => "Float",
587            AttrType::Bool => "Bool",
588            AttrType::Nanoseconds => "Nanoseconds",
589            AttrType::LogicalTime => "LogicalTime",
590            AttrType::Any => "Any",
591            AttrType::EventCoordinate => "Coordinate",
592        }
593        .fmt(f)
594    }
595}
596
597pub mod conversion {
598    use std::convert::TryFrom;
599
600    use super::*;
601
602    macro_rules! impl_from_integer {
603        ($ty:ty) => {
604            impl From<$ty> for AttrVal {
605                fn from(i: $ty) -> Self {
606                    AttrVal::Integer(i as i64)
607                }
608            }
609        };
610    }
611
612    impl_from_integer!(i8);
613    impl_from_integer!(i16);
614    impl_from_integer!(i32);
615    impl_from_integer!(i64);
616    impl_from_integer!(u8);
617    impl_from_integer!(u16);
618    impl_from_integer!(u32);
619
620    macro_rules! impl_from_bigint {
621        ($ty:ty) => {
622            impl From<$ty> for AttrVal {
623                fn from(i: $ty) -> Self {
624                    BigInt::new_attr_val(i as i128)
625                }
626            }
627        };
628    }
629
630    impl_from_bigint!(u64);
631    impl_from_bigint!(i128);
632
633    macro_rules! impl_from_float {
634        ($ty:ty) => {
635            impl From<$ty> for AttrVal {
636                fn from(f: $ty) -> Self {
637                    AttrVal::Float((f as f64).into())
638                }
639            }
640        };
641    }
642
643    impl_from_float!(f32);
644    impl_from_float!(f64);
645
646    macro_rules! impl_try_from_attr_val {
647        ($variant:path, $ty:ty, $expected:path) => {
648            impl TryFrom<AttrVal> for $ty {
649                type Error = WrongAttrTypeError;
650
651                fn try_from(value: AttrVal) -> std::result::Result<Self, Self::Error> {
652                    if let $variant(x) = value {
653                        Ok(x.into())
654                    } else {
655                        Err(WrongAttrTypeError {
656                            actual: value.attr_type(),
657                            expected: $expected,
658                        })
659                    }
660                }
661            }
662        };
663    }
664
665    macro_rules! impl_try_from_attr_val_deref {
666        ($variant:path, $ty:ty, $expected:path) => {
667            impl TryFrom<AttrVal> for $ty {
668                type Error = WrongAttrTypeError;
669
670                fn try_from(value: AttrVal) -> std::result::Result<Self, Self::Error> {
671                    if let $variant(x) = value {
672                        Ok((*x).clone())
673                    } else {
674                        Err(WrongAttrTypeError {
675                            actual: value.attr_type(),
676                            expected: $expected,
677                        })
678                    }
679                }
680            }
681        };
682    }
683
684    impl_try_from_attr_val_deref!(AttrVal::TimelineId, TimelineId, AttrType::TimelineId);
685    impl_try_from_attr_val_deref!(
686        AttrVal::EventCoordinate,
687        EventCoordinate,
688        AttrType::EventCoordinate
689    );
690
691    impl_try_from_attr_val!(AttrVal::Integer, i64, AttrType::Integer);
692    impl_try_from_attr_val!(AttrVal::String, Cow<'static, str>, AttrType::String);
693    impl_try_from_attr_val_deref!(AttrVal::BigInt, i128, AttrType::BigInt);
694    impl_try_from_attr_val!(AttrVal::Float, f64, AttrType::Float);
695    impl_try_from_attr_val!(AttrVal::Bool, bool, AttrType::Bool);
696    impl_try_from_attr_val!(AttrVal::LogicalTime, LogicalTime, AttrType::LogicalTime);
697    impl_try_from_attr_val!(AttrVal::Timestamp, Nanoseconds, AttrType::Nanoseconds);
698}
699
700#[derive(Debug, thiserror::Error, Eq, PartialEq)]
701#[error("Wrong attribute type: expected {expected:?}, found {actual:?}")]
702pub struct WrongAttrTypeError {
703    actual: AttrType,
704    expected: AttrType,
705}
706
707#[cfg(feature = "pyo3")]
708impl<'py> pyo3::FromPyObject<'py> for AttrVal {
709    fn extract_bound(
710        ob: &pyo3::prelude::Bound<'py, pyo3::prelude::PyAny>,
711    ) -> pyo3::prelude::PyResult<Self> {
712        use pyo3::prelude::*;
713
714        // Check the most common types first
715        if let Ok(i) = ob.extract::<i64>() {
716            return Ok(i.into());
717        }
718
719        if let Ok(f) = ob.extract::<f64>() {
720            return Ok(f.into());
721        }
722
723        if let Ok(s) = ob.extract::<String>() {
724            return Ok(s.into());
725        }
726
727        if let Ok(b) = ob.extract::<bool>() {
728            return Ok(b.into());
729        }
730
731        if let Ok(ts) = ob.extract::<std::time::SystemTime>() {
732            match ts.duration_since(std::time::UNIX_EPOCH) {
733                Ok(dur) => {
734                    let ns = dur.as_nanos();
735                    if ns > u64::MAX as u128 {
736                        return Err(pyo3::exceptions::PyValueError::new_err(
737                            "Timestamp value too large",
738                        ));
739                    } else {
740                        return Ok(Nanoseconds::from(ns as u64).into());
741                    }
742                }
743                Err(_) => {
744                    return Err(pyo3::exceptions::PyValueError::new_err(
745                        "Timestamp before UNIX epoch",
746                    ));
747                }
748            }
749        }
750
751        // Less common types are checked later
752        if let Ok(tl_id) = ob.extract::<TimelineId>() {
753            return Ok(tl_id.0.into());
754        }
755
756        if let Ok(lt) = ob.extract::<LogicalTime>() {
757            return Ok(lt.into());
758        }
759
760        if let Ok(ec) = ob.extract::<EventCoordinate>() {
761            return Ok(ec.into());
762        }
763
764        if let Ok(i) = ob.extract::<i128>() {
765            return Ok(i.into());
766        }
767
768        if let Ok(id) = ob.extract::<crate::mutation_plane::types::MutationId>() {
769            return Ok(i128::from_le_bytes(Uuid::from(id).into_bytes()).into());
770        }
771
772        if let Ok(id) = ob.extract::<crate::mutation_plane::types::MutatorId>() {
773            return Ok(i128::from_le_bytes(Uuid::from(id).into_bytes()).into());
774        }
775
776        Err(pyo3::exceptions::PyValueError::new_err(
777            "Cannot represent value as AttrVal",
778        ))
779    }
780}
781
782#[cfg(feature = "pyo3")]
783impl pyo3::IntoPy<pyo3::PyObject> for AttrVal {
784    fn into_py(self, py: pyo3::prelude::Python<'_>) -> pyo3::PyObject {
785        match self {
786            AttrVal::TimelineId(tid) => tid.into_py(py),
787            AttrVal::EventCoordinate(ec) => ec.into_py(py),
788            AttrVal::String(s) => s.into_py(py),
789            AttrVal::Integer(i) => i.into_py(py),
790            AttrVal::BigInt(bi) => bi.into_py(py),
791            AttrVal::Float(f) => f.into_py(py),
792            AttrVal::Bool(b) => b.into_py(py),
793            AttrVal::Timestamp(ns) => {
794                (std::time::UNIX_EPOCH + std::time::Duration::from_nanos(ns.get_raw())).into_py(py)
795            }
796            AttrVal::LogicalTime(lt) => lt.into_py(py),
797        }
798    }
799}
800
801#[cfg(test)]
802mod tests {
803    use super::*;
804
805    #[test]
806    fn parse_logical_time() {
807        let reference = Ok(LogicalTime::quaternary(0u64, 0u64, 0u64, 42u64));
808
809        // should parse
810        assert_eq!(reference, "42".parse());
811        assert_eq!(reference, "0:42".parse());
812        assert_eq!(reference, "0:0:42".parse());
813        assert_eq!(reference, "0:0:0:42".parse());
814
815        // should not parse
816        assert_eq!(Err(()), ":".parse::<LogicalTime>());
817        assert_eq!(Err(()), "::".parse::<LogicalTime>());
818        assert_eq!(Err(()), ":0".parse::<LogicalTime>());
819        assert_eq!(Err(()), "0:".parse::<LogicalTime>());
820        assert_eq!(Err(()), "127.0.0.1:8080".parse::<LogicalTime>());
821        assert_eq!(Err(()), "localhost:8080".parse::<LogicalTime>());
822        assert_eq!(Err(()), "example.com:8080".parse::<LogicalTime>());
823    }
824
825    #[test]
826    fn parse_attr_vals() {
827        // Bool
828        assert_eq!(Ok(AttrVal::Bool(false)), "false".parse());
829        assert_eq!(Ok(AttrVal::Bool(true)), "true".parse());
830
831        // Integer
832        assert_eq!(Ok(AttrVal::Integer(37)), "37".parse());
833
834        // BigInt
835        assert_eq!(
836            Ok(BigInt::new_attr_val(36893488147419103232i128)),
837            "36893488147419103232".parse()
838        );
839
840        // Float
841        assert_eq!(Ok(AttrVal::Float(76.37f64.into())), "76.37".parse());
842
843        // TimelineId
844        assert_eq!(
845            Ok(AttrVal::TimelineId(Box::new(
846                Uuid::parse_str("bec14bc0-1dea-4b68-b138-62f7b6827e35")
847                    .unwrap()
848                    .into()
849            ))),
850            "bec14bc0-1dea-4b68-b138-62f7b6827e35".parse()
851        );
852
853        // Timestamp
854        // N.B. This is impossible to parse as an `AttrVal` since it's just a number which will
855        // have already been parsed as a (Big)Int. Could try parsing more complex date strings?
856
857        // LogicalTime
858        // N.B. There is no way to specify a single segment logical time, again it will have
859        // already been parsed as a (Big)Int, try 2, 3, and 4 segment
860        let lt_ref = Ok(AttrVal::LogicalTime(LogicalTime::quaternary(
861            0u64, 0u64, 0u64, 42u64,
862        )));
863        assert_eq!(lt_ref, "0:42".parse());
864        assert_eq!(lt_ref, "0:0:42".parse());
865        assert_eq!(lt_ref, "0:0:0:42".parse());
866
867        // String
868        assert_eq!(
869            Ok(AttrVal::String("Hello, World!".into())),
870            "\"Hello, World!\"".parse()
871        );
872        assert_eq!(
873            Ok(AttrVal::String("Hello, World!".into())),
874            "'Hello, World!'".parse()
875        );
876        assert_eq!(
877            Ok(AttrVal::String("Hello, World!".into())),
878            "Hello, World!".parse()
879        );
880
881        assert_eq!(Ok(AttrVal::String("".into())), "\"\"".parse());
882        assert_eq!(Ok(AttrVal::String("".into())), "\"".parse());
883
884        assert_eq!(Ok(AttrVal::String("".into())), "''".parse());
885        assert_eq!(Ok(AttrVal::String("".into())), "'".parse());
886
887        assert_eq!(Ok(AttrVal::String("".into())), "".parse());
888    }
889}