use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use super::kind::Kind;
use crate::key::{PublicKey, PublicKeyError};
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum CoordinateError {
#[error("expected `<kind>:<author>:<identifier>`, got `{0}`")]
Malformed(String),
#[error("invalid kind segment `{0}`")]
InvalidKind(String),
#[error(transparent)]
InvalidAuthor(#[from] PublicKeyError),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Coordinate {
pub kind: Kind,
pub author: PublicKey,
pub identifier: String,
}
impl Coordinate {
#[must_use]
pub fn new(kind: Kind, author: PublicKey, identifier: impl Into<String>) -> Self {
Self {
kind,
author,
identifier: identifier.into(),
}
}
pub fn parse(input: impl AsRef<str>) -> Result<Self, CoordinateError> {
input.as_ref().parse()
}
#[must_use]
pub fn to_wire(&self) -> String {
format!(
"{}:{}:{}",
self.kind.as_u16(),
self.author.to_hex(),
self.identifier
)
}
}
impl fmt::Display for Coordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_wire())
}
}
impl FromStr for Coordinate {
type Err = CoordinateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.splitn(3, ':');
let kind_str = parts.next();
let author_str = parts.next();
let identifier = parts.next();
match (kind_str, author_str, identifier) {
(Some(k), Some(a), Some(id)) => {
let kind: u16 = k
.parse()
.map_err(|_| CoordinateError::InvalidKind(k.to_owned()))?;
let author = PublicKey::parse(a)?;
Ok(Self::new(Kind::from(kind), author, id))
}
_ => Err(CoordinateError::Malformed(s.to_owned())),
}
}
}
impl Serialize for Coordinate {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_wire())
}
}
impl<'de> Deserialize<'de> for Coordinate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
raw.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Keys;
fn pk() -> PublicKey {
let keys = Keys::parse("0000000000000000000000000000000000000000000000000000000000000003")
.unwrap();
*keys.public_key()
}
#[test]
fn display_round_trip() {
let coord = Coordinate::new(Kind::from(30_023_u16), pk(), "long-form-1");
let wire = coord.to_string();
let parsed: Coordinate = wire.parse().unwrap();
assert_eq!(parsed, coord);
}
#[test]
fn allows_colon_in_identifier() {
let coord = Coordinate::new(Kind::from(30_023_u16), pk(), "weird:id:with:colons");
let wire = coord.to_string();
let parsed: Coordinate = wire.parse().unwrap();
assert_eq!(parsed, coord);
assert_eq!(parsed.identifier, "weird:id:with:colons");
let tail_wire = format!("30023:{}:a:b:c", pk().to_hex());
let tail_parsed: Coordinate = tail_wire.parse().unwrap();
assert_eq!(tail_parsed.identifier, "a:b:c");
}
#[test]
fn rejects_missing_components() {
let err1 = "30023".parse::<Coordinate>().unwrap_err();
assert!(matches!(err1, CoordinateError::Malformed(_)));
let err2 = "30023:not-hex".parse::<Coordinate>().unwrap_err();
assert!(matches!(err2, CoordinateError::Malformed(_)));
}
#[test]
fn rejects_bad_kind() {
let value = format!("not-a-number:{}:foo", pk().to_hex());
let err: CoordinateError = value.parse::<Coordinate>().unwrap_err();
assert!(matches!(err, CoordinateError::InvalidKind(_)));
}
#[test]
fn parse_method_matches_fromstr() {
let coord = Coordinate::new(Kind::from(30_023_u16), pk(), "alpha");
let wire = coord.to_string();
let via_inherent = Coordinate::parse(&wire).unwrap();
let via_fromstr: Coordinate = wire.parse().unwrap();
assert_eq!(via_inherent, via_fromstr);
assert_eq!(via_inherent, coord);
}
#[test]
fn serde_uses_wire_form() {
let coord = Coordinate::new(Kind::from(30_023_u16), pk(), "alpha");
let json = serde_json::to_string(&coord).unwrap();
assert!(json.starts_with('"'));
assert!(json.contains(":alpha\""));
let parsed: Coordinate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, coord);
}
}