use base32::{decode, Alphabet};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "openapi")]
use utoipa::{PartialSchema, ToSchema};
use crate::{ParsedUri, Resource};
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PubkyId {
z32: String,
#[cfg(not(target_arch = "wasm32"))]
public_key: pubky::PublicKey,
}
#[cfg(feature = "openapi")]
impl PartialSchema for PubkyId {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
String::schema()
}
}
#[cfg(feature = "openapi")]
impl ToSchema for PubkyId {}
impl PubkyId {
fn validate(s: &str) -> Result<(), String> {
if s.len() != 52 {
return Err("Validation Error: the string is not 52 utf chars".to_string());
}
match decode(Alphabet::Z, s) {
Some(_) => Ok(()),
None => Err("Validation Error: invalid public key encoding".to_string()),
}
}
#[cfg(target_arch = "wasm32")]
pub fn try_from(s: &str) -> Result<Self, String> {
Self::validate(s)?;
Ok(Self { z32: s.to_string() })
}
#[cfg(not(target_arch = "wasm32"))]
pub fn try_from(s: &str) -> Result<Self, String> {
Self::validate(s)?;
let public_key =
pubky::PublicKey::try_from(s).map_err(|e| format!("Validation Error: {e}"))?;
Ok(Self {
z32: public_key.to_z32(),
public_key,
})
}
pub fn to_uri(&self) -> ParsedUri {
ParsedUri {
user_id: self.clone(),
resource: Resource::User,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn to_public_key(&self) -> pubky::PublicKey {
self.public_key.clone()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl From<pubky::PublicKey> for PubkyId {
fn from(pk: pubky::PublicKey) -> Self {
Self {
z32: pk.to_z32(),
public_key: pk,
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl From<pubky::Keypair> for PubkyId {
fn from(keypair: pubky::Keypair) -> Self {
Self::from(keypair.public_key())
}
}
impl fmt::Display for PubkyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.z32)
}
}
impl std::ops::Deref for PubkyId {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.z32
}
}
impl AsRef<str> for PubkyId {
fn as_ref(&self) -> &str {
&self.z32
}
}
impl Serialize for PubkyId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.z32)
}
}
impl<'de> Deserialize<'de> for PubkyId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
PubkyId::try_from(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(target_arch = "wasm32"))]
use pubky::Keypair;
#[test]
fn test_try_from_valid() {
let valid_key = "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo";
let result = PubkyId::try_from(valid_key);
assert!(result.is_ok());
assert_eq!(result.unwrap().as_ref(), valid_key);
}
#[test]
fn test_try_from_invalid_length() {
let invalid_key = "short";
let result = PubkyId::try_from(invalid_key);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Validation Error: the string is not 52 utf chars"
);
}
#[test]
fn test_try_from_invalid_encoding() {
let invalid_key = "0perrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rd0";
let result = PubkyId::try_from(invalid_key);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Validation Error: invalid public key encoding"
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_from_public_key() {
let keypair = Keypair::random();
let public_key = keypair.public_key();
let expected_z32 = public_key.to_z32();
let pubky_id = PubkyId::from(public_key);
assert_eq!(pubky_id.as_ref(), expected_z32);
assert_eq!(pubky_id.to_string(), expected_z32);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_from_keypair() {
let keypair = Keypair::random();
let expected_z32 = keypair.public_key().to_z32();
let pubky_id = PubkyId::from(keypair);
assert_eq!(pubky_id.as_ref(), expected_z32);
assert_eq!(pubky_id.to_string(), expected_z32);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_from_public_key_produces_valid_pubky_id() {
let keypair = Keypair::random();
let public_key = keypair.public_key();
let pubky_id = PubkyId::from(public_key);
assert_eq!(pubky_id.as_ref().len(), 52);
let result = PubkyId::try_from(pubky_id.as_ref());
assert!(result.is_ok());
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_from_keypair_produces_valid_pubky_id() {
let keypair = Keypair::random();
let pubky_id = PubkyId::from(keypair);
assert_eq!(pubky_id.as_ref().len(), 52);
let result = PubkyId::try_from(pubky_id.as_ref());
assert!(result.is_ok());
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_from_keypair_and_public_key_produce_same_result() {
let keypair = Keypair::random();
let public_key = keypair.public_key();
let expected_z32 = public_key.to_z32();
let pubky_id_from_keypair = PubkyId::from(keypair);
let pubky_id_from_public_key = PubkyId::from(public_key);
assert_eq!(pubky_id_from_keypair, pubky_id_from_public_key);
assert_eq!(pubky_id_from_keypair.as_ref(), expected_z32);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_to_public_key_is_infallible_for_valid_pubky_id() {
let keypair = Keypair::random();
let expected_public_key = keypair.public_key();
let pubky_id = PubkyId::from(expected_public_key.clone());
let converted_public_key = pubky_id.to_public_key();
assert_eq!(converted_public_key, expected_public_key);
}
#[test]
fn test_serialization() {
let valid_key = "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo";
let pubky_id = PubkyId::try_from(valid_key).unwrap();
let json = serde_json::to_string(&pubky_id).unwrap();
assert_eq!(json, format!("\"{}\"", valid_key));
}
#[test]
fn test_deserialization() {
let valid_key = "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo";
let json = format!("\"{}\"", valid_key);
let pubky_id: PubkyId = serde_json::from_str(&json).unwrap();
assert_eq!(pubky_id.as_ref(), valid_key);
}
#[test]
fn test_deserialization_from_slice() {
let valid_key = "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo";
let json_str = format!("\"{}\"", valid_key);
let pubky_id: PubkyId = serde_json::from_slice(json_str.as_bytes()).unwrap();
assert_eq!(pubky_id.as_ref(), valid_key);
}
#[test]
fn test_deserialization_invalid_length() {
let json = "\"short\"";
let result: Result<PubkyId, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_deserialization_invalid_length_from_slice() {
let json = "\"short\"";
let result: Result<PubkyId, _> = serde_json::from_slice(json.as_bytes());
assert!(result.is_err());
}
#[test]
fn test_deserialization_invalid_encoding() {
let json = "\"0perrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rd0\"";
let result: Result<PubkyId, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_roundtrip() {
let valid_key = "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo";
let original = PubkyId::try_from(valid_key).unwrap();
let json = serde_json::to_string(&original).unwrap();
let deserialized: PubkyId = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
}