//! Strongly-typed identifiers.

/// Define a UUID newtype identifier with the standard constructors and conversions.
macro_rules! id_newtype {
    ($(#[$meta:meta])* $name:ident) => {
        $(#[$meta])*
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
        #[serde(transparent)]
        pub struct $name(uuid::Uuid);

        impl $name {
            /// Generate a fresh random id.
            #[must_use = "generating an id and discarding it is almost certainly a mistake"]
            pub fn new() -> Self {
                Self(uuid::Uuid::new_v4())
            }

            /// Wrap an existing [`uuid::Uuid`].
            pub fn from_uuid(uuid: uuid::Uuid) -> Self {
                Self(uuid)
            }

            /// The underlying [`uuid::Uuid`].
            pub fn to_uuid(&self) -> uuid::Uuid {
                self.0
            }
        }

        impl Default for $name {
            fn default() -> Self {
                Self::new()
            }
        }

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

        impl std::str::FromStr for $name {
            type Err = uuid::Error;

            fn from_str(s: &str) -> Result<Self, Self::Err> {
                Ok(Self(uuid::Uuid::parse_str(s)?))
            }
        }
    };
}

id_newtype!(
    /// Identifier for an organization (tenant).
    OrgId
);
id_newtype!(
    /// Identifier for a controlled vocabulary (term source).
    VocabularyId
);
id_newtype!(
    /// Identifier for a term within a vocabulary.
    TermId
);
id_newtype!(
    /// Identifier for an authority record (person, organisation, or place).
    AuthorityId
);
id_newtype!(
    /// Identifier for a catalogue object (or group of objects).
    ObjectId
);
id_newtype!(
    /// Identifier for a flexible-field definition.
    FieldDefinitionId
);
id_newtype!(
    /// Identifier for a user of this organization's instance.
    UserId
);

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

    #[test]
    fn parses_and_displays_round_trip() {
        let text = "550e8400-e29b-41d4-a716-446655440000";
        let id: OrgId = text.parse().expect("valid uuid should parse");
        assert_eq!(id.to_string(), text);
    }

    #[test]
    fn rejects_invalid_uuid() {
        assert!("not-a-uuid".parse::<OrgId>().is_err());
    }

    #[test]
    fn distinct_id_types_parse_independently() {
        let text = "550e8400-e29b-41d4-a716-446655440000";
        assert_eq!(text.parse::<VocabularyId>().unwrap().to_string(), text);
        assert_eq!(text.parse::<TermId>().unwrap().to_string(), text);
        assert_eq!(text.parse::<AuthorityId>().unwrap().to_string(), text);
    }
}
