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