1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
use std::hash::Hash;
use std::sync::Arc;

use serde::{de, Deserialize, Deserializer, Serialize, Serializer};

use crate::error::ErrorKind;

/// A [UFO Object Identifier][identifier].
///
/// Identifiers are optional attributes of several objects in the UFO.
/// These identifiers are required to be unique within certain contexts
/// as defined on a per object basis throughout this specification.
/// Identifiers are specified as a string between one and 100 characters long.
/// All characters must be in the printable ASCII range, 0x20 to 0x7E.
///
/// [identifier]: https://unifiedfontobject.org/versions/ufo3/conventions/#identifiers
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct Identifier(Arc<str>);

impl Identifier {
    /// Create a new [`Identifier`] from a string, if it is valid.
    ///
    /// A valid identifier must have between 0 and 100 characters, and each
    /// character must be in the printable ASCII range, 0x20 to 0x7E.
    pub fn new(string: &str) -> Result<Self, ErrorKind> {
        if is_valid_identifier(string) {
            Ok(Identifier(string.into()))
        } else {
            Err(ErrorKind::BadIdentifier)
        }
    }

    /// Creates a new `Identifier`, panicking if the given identifier is invalid.
    #[cfg(test)]
    pub(crate) fn new_raw(string: &str) -> Self {
        assert!(is_valid_identifier(string));
        Self(string.into())
    }

    /// Create a new [`Identifier`] from a UUID v4 identifier.
    pub fn from_uuidv4() -> Self {
        Self::new(uuid::Uuid::new_v4().to_string().as_ref()).unwrap()
    }

    /// Return the raw identifier, as a `&str`.
    pub fn as_str(&self) -> &str {
        self.as_ref()
    }
}

fn is_valid_identifier(s: &str) -> bool {
    s.len() <= 100 && s.bytes().all(|b| (0x20..=0x7E).contains(&b))
}

impl AsRef<str> for Identifier {
    fn as_ref(&self) -> &str {
        self.0.as_ref()
    }
}

impl std::ops::Deref for Identifier {
    type Target = str;
    fn deref(&self) -> &Self::Target {
        self.0.as_ref()
    }
}

// so that assert_eq! macros work
impl<'a> PartialEq<&'a str> for Identifier {
    fn eq(&self, other: &&'a str) -> bool {
        self.0.as_ref() == *other
    }
}

impl<'a> PartialEq<Identifier> for &'a str {
    fn eq(&self, other: &Identifier) -> bool {
        other == self
    }
}

impl std::fmt::Display for Identifier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        std::fmt::Display::fmt(&self.0, f)
    }
}

impl std::borrow::Borrow<str> for Identifier {
    fn borrow(&self) -> &str {
        self.0.as_ref()
    }
}

impl Serialize for Identifier {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        debug_assert!(
            is_valid_identifier(&self.0),
            "all identifiers are validated on construction"
        );
        serializer.serialize_str(&self.0)
    }
}

impl<'de> Deserialize<'de> for Identifier {
    fn deserialize<D>(deserializer: D) -> Result<Identifier, D::Error>
    where
        D: Deserializer<'de>,
    {
        let string = String::deserialize(deserializer)?;
        Identifier::new(string.as_str()).map_err(de::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn identifier_parsing() {
        let valid_chars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
        assert!(Identifier::new(valid_chars).is_ok());

        let i2 = Identifier::new("0aAä");
        assert!(i2.is_err());
        let i3 = Identifier::new("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        assert!(i3.is_err());
    }
}