use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
macro_rules! add_id {
(
$(#[$meta:meta])*
$name:ident,
$id_matches:expr
) => {
#[doc = concat!("An ID of a VRC ", stringify!($name))]
$(#[$meta])*
#[doc = concat!("use vrc::id::", stringify!($name), ";")]
#[doc = concat!("let parse_res = \"an-id-that-is-of-an-invalid-format\".parse::<", stringify!($name), ">();")]
#[doc = concat!("use vrc::id::", stringify!($name), ";")]
#[doc = concat!("let id1 = ", stringify!($name), "::from(\"totally-legit-id\");")]
#[doc = concat!("let id2 = ", stringify!($name), "::from(\"other-totally-legit-id\");")]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[repr(transparent)]
pub struct $name(String);
impl $name {
#[must_use]
pub fn is_valid(&self) -> bool {
$id_matches(&self.0)
}
}
impl AsRef<str> for $name {
#[must_use]
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for $name {
type Err = &'static str;
fn from_str(id: &str) -> Result<Self, Self::Err> {
if !$id_matches(&id) {
return Err(concat!("ID doesn't match expected format"))
}
Ok(Self(id.to_owned()))
}
}
impl From<&str> for $name {
fn from(id: &str) -> Self {
Self(id.to_owned())
}
}
impl From<String> for $name {
fn from(id: String) -> Self {
Self(id)
}
}
impl From<$name> for String {
fn from(id: $name) -> String {
id.0
}
}
impl From<$name> for Any {
fn from(id: $name) -> Any {
Any::$name(id)
}
}
impl<'de> serde::de::Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct IdVisitor;
impl<'de> Visitor<'de> for IdVisitor {
type Value = $name;
fn expecting(
&self, formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
formatter
.write_str(concat!("a string ID of ", stringify!($name), ", that is of the correct format"))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if !$id_matches(&v) {
return Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(v),
&concat!("matching the ID format of ", stringify!($name)),
));
}
Ok($name(v.to_string()))
}
}
deserializer.deserialize_str(IdVisitor)
}
}
};
}
add_id!(Avatar, |v: &str| v.starts_with("avtr_") || v.len() == 10);
add_id!(Group, |v: &str| v.starts_with("grp_"));
add_id!(
Instance,
|_v: &str| true
);
add_id!(UnityPackage, |v: &str| v.starts_with("unp_") || v.len() == 10);
add_id!(User, |v: &str| v.starts_with("usr_") || v.len() == 10);
add_id!(GroupMember, |v: &str| v.starts_with("gmem_"));
add_id!(World, |v: &str| v.starts_with("wrld_") || v.len() == 10);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum OfflineOr<T> {
#[serde(rename = "offline")]
Offline,
#[serde(untagged)]
Id(T),
}
#[cfg(test)]
#[test]
fn offline_or_id() {
assert_eq!(
&serde_json::to_string(&OfflineOr::<String>::Offline).unwrap(),
"\"offline\""
);
let user = crate::id::User::from(
"usr_c1644b5b-3ca4-45b4-97c6-a2a0de70d469".to_owned(),
);
assert_eq!(
&serde_json::to_string(&OfflineOr::<crate::id::User>::Id(user)).unwrap(),
"\"usr_c1644b5b-3ca4-45b4-97c6-a2a0de70d469\""
);
let id = "\"private\"";
assert!(serde_json::from_str::<OfflineOr<crate::id::User>>(id).is_err());
let id = "\"offline\"";
serde_json::from_str::<OfflineOr<crate::id::User>>(id).unwrap();
let id = "\"invalid\"";
assert!(serde_json::from_str::<OfflineOr<crate::id::User>>(id).is_err());
let id = "\"usr_c1644b5b-3ca4-45b4-97c6-a2a0de70d469\"";
serde_json::from_str::<OfflineOr<crate::id::User>>(id).unwrap();
let id = "\"invalid\"";
serde_json::from_str::<OfflineOr<String>>(id).unwrap();
}
impl<T> OfflineOr<T> {
pub const fn as_option(&self) -> Option<&T> {
match &self {
Self::Offline => None,
Self::Id(id) => Some(id),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum OfflineOrPrivateOr<T> {
#[serde(rename = "private")]
Private,
#[serde(rename = "offline")]
Offline,
#[serde(untagged)]
Id(T),
}
impl<T> OfflineOrPrivateOr<T> {
pub const fn as_option(&self) -> Option<&T> {
match &self {
Self::Offline | Self::Private => None,
Self::Id(id) => Some(id),
}
}
}
#[cfg(test)]
#[test]
fn offline_or_private_or_id() {
assert_eq!(
&serde_json::to_string(&OfflineOrPrivateOr::<String>::Offline).unwrap(),
"\"offline\""
);
assert_eq!(
&serde_json::to_string(&OfflineOrPrivateOr::<String>::Private).unwrap(),
"\"private\""
);
let user = crate::id::User::from(
"usr_c1644b5b-3ca4-45b4-97c6-a2a0de70d469".to_owned(),
);
assert_eq!(
&serde_json::to_string(&OfflineOrPrivateOr::<crate::id::User>::Id(user))
.unwrap(),
"\"usr_c1644b5b-3ca4-45b4-97c6-a2a0de70d469\""
);
let id = "\"private\"";
serde_json::from_str::<OfflineOrPrivateOr<crate::id::User>>(id).unwrap();
let id = "\"offline\"";
serde_json::from_str::<OfflineOrPrivateOr<crate::id::User>>(id).unwrap();
let id = "\"invalid\"";
assert!(
serde_json::from_str::<OfflineOrPrivateOr<crate::id::User>>(id).is_err()
);
let id = "\"usr_c1644b5b-3ca4-45b4-97c6-a2a0de70d469\"";
serde_json::from_str::<OfflineOrPrivateOr<crate::id::User>>(id).unwrap();
let id = "\"invalid\"";
serde_json::from_str::<OfflineOrPrivateOr<String>>(id).unwrap();
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Any {
Avatar(Avatar),
Group(Group),
Instance(Instance),
UnityPackage(UnityPackage),
User(User),
World(World),
GroupMember(GroupMember),
}
impl AsRef<str> for Any {
#[must_use]
fn as_ref(&self) -> &str {
match self {
Self::Avatar(v) => v.as_ref(),
Self::Group(v) => v.as_ref(),
Self::Instance(v) => v.as_ref(),
Self::UnityPackage(v) => v.as_ref(),
Self::User(v) => v.as_ref(),
Self::World(v) => v.as_ref(),
Self::GroupMember(v) => v.as_ref(),
}
}
}
impl std::fmt::Display for Any {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.as_ref())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WorldInstance {
pub instance: Instance,
pub world: World,
}
impl fmt::Display for WorldInstance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.world.as_ref(), self.instance.as_ref())
}
}
impl Serialize for WorldInstance {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> serde::de::Deserialize<'de> for WorldInstance {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct IdVisitor;
impl<'de> Visitor<'de> for IdVisitor {
type Value = WorldInstance;
fn expecting(
&self, formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
formatter
.write_str(concat!("a string, WorldInstance, that is of format `{vrc::id:World}:{vrc::id:Instance}`"))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let (world, instance) = v.split_once(':').ok_or_else(|| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(v),
&"should be able to be split at a `:` character",
)
})?;
let world: World = world.parse().map_err(|e| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &e)
})?;
let instance: Instance = instance.parse().map_err(|e| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &e)
})?;
Ok(WorldInstance { instance, world })
}
}
deserializer.deserialize_str(IdVisitor)
}
}
#[cfg(test)]
#[test]
fn user_id_parsing() {
let id = "\"usr_c1644b5b-3ca4-45b4-97c6-a2a0de70d469\"";
serde_json::from_str::<crate::id::User>(id).unwrap();
let id = "\"grp_c1644b5b-3ca4-45b4-97c6-a2a0de70d469\"";
assert!(serde_json::from_str::<crate::id::User>(id).is_err());
let id = "\"qYZJsbJRqA\"";
serde_json::from_str::<crate::id::User>(id).unwrap();
let id = "\"qYZJsbJRqA1\"";
assert!(serde_json::from_str::<crate::id::User>(id).is_err());
}
#[cfg(test)]
#[test]
fn world_and_instance() {
let original_id = "\"wrld_ba913a96-fac4-4048-a062-9aa5db092812:12345~hidden(usr_c1644b5b-3ca4-45b4-97c6-a2a0de70d469)~region(eu)~nonce(27e8414a-59a0-4f3d-af1f-f27557eb49a2)\"";
let id: crate::id::WorldInstance = serde_json::from_str(original_id)
.expect("to be able to deserialize WorldInstance");
let id: String =
serde_json::to_string(&id).expect("to be able to serialize WorldInstance");
assert_eq!(original_id, id);
}
#[cfg(test)]
#[test]
fn strict_from_string() {
use std::str::FromStr;
let original_id = "\"grp_93451756-8327-4ecc-b978-3e60aa9f64a9\"";
let id: crate::id::Group =
serde_json::from_str(original_id).expect("to be able to deserialize Group");
let id: String =
serde_json::to_string(&id).expect("to be able to serialize a valid Group");
assert_eq!(original_id, id);
let original_id = "\"93451756-8327-4ecc-b978-3e60aa9f64a9\"";
assert!(
crate::id::Group::from_str(original_id).is_err(),
"from_str for an invalid Group ID errors"
);
let id = crate::id::Group::from(original_id);
assert!(
!id.is_valid(),
"Force converted group ID can be detected as invalid"
);
let _id: String = serde_json::to_string(&id)
.expect("to be able to serialize an invalid Group");
}
#[cfg(test)]
#[test]
fn instance_id() {
let original_id = "\"12345\"";
let id: crate::id::Instance = serde_json::from_str(original_id)
.expect("to be able to deserialize instance ID");
let id: String = serde_json::to_string(&id)
.expect("to be able to serialize a valid instance");
assert_eq!(original_id, id);
serde_json::from_str::<crate::id::Instance>("\"59422~region(eu)\"").unwrap();
}