1use std::{convert::AsRef, fmt::Display, ops::Deref, str::FromStr};
4
5use anyhow::{bail, Result};
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Error, Debug, Eq, PartialEq)]
11pub enum ParseError {
12 #[error(r#"found the prefix "{found}", but expected "{expected}""#)]
14 InvalidKeyType { found: String, expected: String },
15 #[error("the key should be {expected} characters, but was {found} characters")]
17 InvalidLength { found: usize, expected: usize },
18}
19
20pub type ModuleId = Id<'M'>;
22pub type ServerId = Id<'N'>;
24pub type ServiceId = Id<'V'>;
26pub type ClusterSeed = Seed<'C'>;
28
29#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
32pub struct Id<const PREFIX: char>(String);
33
34impl<const PREFIX: char> FromStr for Id<PREFIX> {
35 type Err = ParseError;
36
37 fn from_str(s: &str) -> Result<Self, Self::Err> {
38 Ok(Self(parse(s, PREFIX, false)?))
39 }
40}
41
42impl<const PREFIX: char> AsRef<str> for Id<PREFIX> {
43 fn as_ref(&self) -> &str {
44 self.0.as_ref()
45 }
46}
47
48impl<const PREFIX: char> Deref for Id<PREFIX> {
49 type Target = str;
50
51 fn deref(&self) -> &Self::Target {
52 &self.0
53 }
54}
55
56impl<const PREFIX: char> Display for Id<PREFIX> {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 self.0.fmt(f)
59 }
60}
61
62impl<const PREFIX: char> Id<PREFIX> {
63 #[must_use]
65 pub fn into_string(self) -> String {
66 self.0
67 }
68
69 #[must_use]
70 pub fn prefix() -> char {
71 PREFIX
72 }
73}
74
75#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
78pub struct Seed<const PREFIX: char>(String);
79
80impl<const PREFIX: char> Display for Seed<PREFIX> {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 self.0.fmt(f)
85 }
86}
87
88impl<const PREFIX: char> Default for Seed<PREFIX> {
89 fn default() -> Self {
90 Self(Default::default())
91 }
92}
93
94impl<const PREFIX: char> AsRef<str> for Seed<PREFIX> {
95 fn as_ref(&self) -> &str {
96 self.0.as_ref()
97 }
98}
99
100impl<const PREFIX: char> Deref for Seed<PREFIX> {
101 type Target = str;
102
103 fn deref(&self) -> &Self::Target {
104 &self.0
105 }
106}
107
108impl<const PREFIX: char> FromStr for Seed<PREFIX> {
109 type Err = ParseError;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 Ok(Self(parse(s, PREFIX, true)?))
113 }
114}
115
116impl<const PREFIX: char> Seed<PREFIX> {
117 #[must_use]
119 pub fn into_string(self) -> String {
120 self.0
121 }
122
123 #[must_use]
124 pub fn prefix() -> char {
125 PREFIX
126 }
127}
128
129fn parse(value: &str, prefix: char, is_seed: bool) -> Result<String, ParseError> {
130 let (len, prefix) = if is_seed {
131 (58, format!("S{prefix}"))
132 } else {
133 (56, prefix.to_string())
134 };
135
136 let count = value.chars().count();
137 if count != len {
138 return Err(ParseError::InvalidLength {
139 found: count,
140 expected: len,
141 });
142 }
143
144 if value.starts_with(&prefix) {
145 Ok(value.to_string())
146 } else {
147 Err(ParseError::InvalidKeyType {
148 found: value.chars().take(prefix.chars().count()).collect(),
149 expected: prefix,
150 })
151 }
152}
153
154pub fn validate_contract_id(contract_id: &str) -> Result<()> {
160 if contract_id.len() == 56
161 && contract_id
162 .chars()
163 .all(|c| c.is_ascii_digit() || c.is_ascii_uppercase())
164 {
165 bail!("It looks like you used a Component or Provider ID (e.g. VABC...) instead of a contract ID (e.g. wasmcloud:httpserver)")
166 } else {
167 Ok(())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use test_case::test_case;
175
176 #[test_case(
177 "SC00000000000000000000000000000000000000000000000000000000", 'C', true
178 => Ok("SC00000000000000000000000000000000000000000000000000000000".to_string());
179 "valid cluster seed")]
180 #[test_case(
181 "SC000000000000000000000000000000000000000000000000", 'C', true
182 => Err(ParseError::InvalidLength { found: 50, expected: 58 });
183 "short cluster seed")]
184 #[test_case(
185 "SM00000000000000000000000000000000000000000000000000000000", 'C', true
186 => Err(ParseError::InvalidKeyType { expected: "SC".to_string(), found: "SM".to_string() });
187 "cluster seed has wrong prefix")]
188 #[test_case(
189 "M0000000000000000000000000000000000000000000000000000000", 'M', false
190 => Ok("M0000000000000000000000000000000000000000000000000000000".to_string());
191 "valid module id")]
192 #[test_case(
193 "M0000000000000000000000000000000000000000000000000", 'M', false
194 => Err(ParseError::InvalidLength { found: 50, expected: 56 });
195 "short module id")]
196 #[test_case(
197 "V0000000000000000000000000000000000000000000000000000000", 'M', false
198 => Err(ParseError::InvalidKeyType { expected: "M".to_string(), found: "V".to_string() });
199 "module id has wrong prefix")]
200 fn test_parse(value: &str, prefix: char, is_seed: bool) -> Result<String, ParseError> {
201 parse(value, prefix, is_seed)
202 }
203
204 #[test]
205 fn seed_default() {
206 assert_eq!(ClusterSeed::default(), Seed::<'C'>(String::new()));
207 assert_eq!(Seed::<'M'>::default(), Seed::<'M'>(String::new()));
208 }
209
210 #[test]
211 fn module_id_round_trip() {
212 let a = "M0000000000000000000000000000000000000000000000000000000";
213 let b = a.parse::<ModuleId>().unwrap();
214 assert_eq!(a.to_string(), b.to_string());
215 }
216
217 #[test]
218 fn service_id_round_trip() {
219 let a = "V0000000000000000000000000000000000000000000000000000000";
220 let b = a.parse::<ServiceId>().unwrap();
221 assert_eq!(a.to_string(), b.to_string());
222 }
223
224 #[test]
225 fn server_id_round_trip() {
226 let a = "N0000000000000000000000000000000000000000000000000000000";
227 let b = a.parse::<ServerId>().unwrap();
228 assert_eq!(a.to_string(), b.to_string());
229 }
230
231 #[test]
232 fn cluster_seed_round_trip() {
233 let a = "SC00000000000000000000000000000000000000000000000000000000";
234 let b = a.parse::<ClusterSeed>().unwrap();
235 assert_eq!(a.to_string(), b.to_string());
236 }
237}