nula_core/event/
coordinate.rs1use std::fmt;
19use std::str::FromStr;
20
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22use thiserror::Error;
23
24use super::kind::Kind;
25use crate::key::{PublicKey, PublicKeyError};
26
27#[derive(Debug, Clone, Error)]
29#[non_exhaustive]
30pub enum CoordinateError {
31 #[error("expected `<kind>:<author>:<identifier>`, got `{0}`")]
33 Malformed(String),
34 #[error("invalid kind segment `{0}`")]
36 InvalidKind(String),
37 #[error(transparent)]
39 InvalidAuthor(#[from] PublicKeyError),
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
44pub struct Coordinate {
45 pub kind: Kind,
47 pub author: PublicKey,
49 pub identifier: String,
51}
52
53impl Coordinate {
54 #[must_use]
56 pub fn new(kind: Kind, author: PublicKey, identifier: impl Into<String>) -> Self {
57 Self {
58 kind,
59 author,
60 identifier: identifier.into(),
61 }
62 }
63
64 pub fn parse(input: impl AsRef<str>) -> Result<Self, CoordinateError> {
75 input.as_ref().parse()
76 }
77
78 #[must_use]
80 pub fn to_wire(&self) -> String {
81 format!(
82 "{}:{}:{}",
83 self.kind.as_u16(),
84 self.author.to_hex(),
85 self.identifier
86 )
87 }
88}
89
90impl fmt::Display for Coordinate {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 f.write_str(&self.to_wire())
93 }
94}
95
96impl FromStr for Coordinate {
97 type Err = CoordinateError;
98
99 fn from_str(s: &str) -> Result<Self, Self::Err> {
100 let mut parts = s.splitn(3, ':');
101 let kind_str = parts.next();
102 let author_str = parts.next();
103 let identifier = parts.next();
104 match (kind_str, author_str, identifier) {
105 (Some(k), Some(a), Some(id)) => {
106 let kind: u16 = k
107 .parse()
108 .map_err(|_| CoordinateError::InvalidKind(k.to_owned()))?;
109 let author = PublicKey::parse(a)?;
110 Ok(Self::new(Kind::from(kind), author, id))
111 }
112 _ => Err(CoordinateError::Malformed(s.to_owned())),
113 }
114 }
115}
116
117impl Serialize for Coordinate {
118 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
119 where
120 S: Serializer,
121 {
122 serializer.serialize_str(&self.to_wire())
123 }
124}
125
126impl<'de> Deserialize<'de> for Coordinate {
127 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
128 where
129 D: Deserializer<'de>,
130 {
131 let raw = String::deserialize(deserializer)?;
132 raw.parse().map_err(serde::de::Error::custom)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::Keys;
140
141 fn pk() -> PublicKey {
142 let keys = Keys::parse("0000000000000000000000000000000000000000000000000000000000000003")
143 .unwrap();
144 *keys.public_key()
145 }
146
147 #[test]
148 fn display_round_trip() {
149 let coord = Coordinate::new(Kind::from(30_023_u16), pk(), "long-form-1");
150 let wire = coord.to_string();
151 let parsed: Coordinate = wire.parse().unwrap();
152 assert_eq!(parsed, coord);
153 }
154
155 #[test]
156 fn allows_colon_in_identifier() {
157 let coord = Coordinate::new(Kind::from(30_023_u16), pk(), "weird:id:with:colons");
168 let wire = coord.to_string();
169 let parsed: Coordinate = wire.parse().unwrap();
170 assert_eq!(parsed, coord);
171 assert_eq!(parsed.identifier, "weird:id:with:colons");
172
173 let tail_wire = format!("30023:{}:a:b:c", pk().to_hex());
177 let tail_parsed: Coordinate = tail_wire.parse().unwrap();
178 assert_eq!(tail_parsed.identifier, "a:b:c");
179 }
180
181 #[test]
182 fn rejects_missing_components() {
183 let err1 = "30023".parse::<Coordinate>().unwrap_err();
184 assert!(matches!(err1, CoordinateError::Malformed(_)));
185 let err2 = "30023:not-hex".parse::<Coordinate>().unwrap_err();
186 assert!(matches!(err2, CoordinateError::Malformed(_)));
187 }
188
189 #[test]
190 fn rejects_bad_kind() {
191 let value = format!("not-a-number:{}:foo", pk().to_hex());
192 let err: CoordinateError = value.parse::<Coordinate>().unwrap_err();
193 assert!(matches!(err, CoordinateError::InvalidKind(_)));
194 }
195
196 #[test]
197 fn parse_method_matches_fromstr() {
198 let coord = Coordinate::new(Kind::from(30_023_u16), pk(), "alpha");
202 let wire = coord.to_string();
203 let via_inherent = Coordinate::parse(&wire).unwrap();
204 let via_fromstr: Coordinate = wire.parse().unwrap();
205 assert_eq!(via_inherent, via_fromstr);
206 assert_eq!(via_inherent, coord);
207 }
208
209 #[test]
210 fn serde_uses_wire_form() {
211 let coord = Coordinate::new(Kind::from(30_023_u16), pk(), "alpha");
212 let json = serde_json::to_string(&coord).unwrap();
213 assert!(json.starts_with('"'));
214 assert!(json.contains(":alpha\""));
215 let parsed: Coordinate = serde_json::from_str(&json).unwrap();
216 assert_eq!(parsed, coord);
217 }
218}