aion_package/
namespace.rs1use std::collections::BTreeSet;
4use std::str::FromStr;
5
6use crate::{BeamSet, ContentHash, hash::ContentHashParseError};
7
8pub const DEPLOYED_NAME_SEPARATOR: char = '$';
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct ParsedDeployedName {
18 logical: String,
19 hash: ContentHash,
20}
21
22impl ParsedDeployedName {
23 #[must_use]
25 pub fn new(logical: String, hash: ContentHash) -> Self {
26 Self { logical, hash }
27 }
28
29 #[must_use]
31 pub fn logical(&self) -> &str {
32 &self.logical
33 }
34
35 #[must_use]
37 pub const fn hash(&self) -> &ContentHash {
38 &self.hash
39 }
40
41 #[must_use]
43 pub fn into_parts(self) -> (String, ContentHash) {
44 (self.logical, self.hash)
45 }
46}
47
48#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)]
50pub enum NamespaceError {
51 #[error("deployed module name is missing the '$' namespace separator")]
53 MissingSeparator,
54
55 #[error("deployed module name has an empty logical module component")]
57 EmptyLogicalName,
58
59 #[error(
61 "deployed module name has a logical module component containing the '$' namespace separator"
62 )]
63 SeparatorInLogicalName,
64
65 #[error("deployed module name has an invalid content hash: {source}")]
67 InvalidHash {
68 source: ContentHashParseError,
70 },
71}
72
73#[must_use]
80pub fn deployed_name(logical: &str, hash: &ContentHash) -> String {
81 format!("{logical}{DEPLOYED_NAME_SEPARATOR}{hash}")
82}
83
84pub fn parse_deployed_name(deployed: &str) -> Result<ParsedDeployedName, NamespaceError> {
99 let Some((logical, hash_text)) = deployed.split_once(DEPLOYED_NAME_SEPARATOR) else {
100 return Err(NamespaceError::MissingSeparator);
101 };
102
103 if logical.is_empty() {
104 return Err(NamespaceError::EmptyLogicalName);
105 }
106 if hash_text.contains(DEPLOYED_NAME_SEPARATOR) {
107 return Err(NamespaceError::SeparatorInLogicalName);
108 }
109
110 let hash = ContentHash::from_str(hash_text)
111 .map_err(|source| NamespaceError::InvalidHash { source })?;
112
113 Ok(ParsedDeployedName::new(logical.to_owned(), hash))
114}
115
116#[must_use]
123pub fn deployed_names(beams: &BeamSet, hash: &ContentHash) -> BTreeSet<String> {
124 beams
125 .iter()
126 .map(|module| deployed_name(module.name(), hash))
127 .collect()
128}
129
130#[cfg(test)]
131mod tests {
132 use std::collections::BTreeSet;
133
134 use super::{
135 DEPLOYED_NAME_SEPARATOR, NamespaceError, deployed_name, deployed_names, parse_deployed_name,
136 };
137 use crate::{BeamModule, BeamSet, ContentHash, hash::ContentHashParseError};
138
139 fn hash(byte: u8) -> ContentHash {
140 ContentHash::from_bytes([byte; 32])
141 }
142
143 fn beam_set() -> Result<BeamSet, crate::PackageError> {
144 BeamSet::new(vec![
145 BeamModule::new("workflow/b", vec![2]),
146 BeamModule::new("workflow/a", vec![1]),
147 BeamModule::new("stdlib/list", vec![3]),
148 ])
149 }
150
151 #[test]
152 fn forward_transform_uses_mandated_separator_and_hash_text() {
153 let hash = hash(0xab);
154 let deployed = deployed_name("order_workflow", &hash);
155
156 assert_eq!(
157 deployed,
158 "order_workflow$abababababababababababababababababababababababababababababababab"
159 );
160 assert!(deployed.contains(DEPLOYED_NAME_SEPARATOR));
161 }
162
163 #[test]
164 fn forward_then_inverse_round_trips_many_pairs() -> Result<(), NamespaceError> {
165 let cases = [
166 ("order_workflow", hash(0x00)),
167 ("workflow_with_underscores", hash(0x11)),
168 ("nested/module/name", hash(0x7f)),
169 ("workflow_123", hash(0xff)),
170 ];
171
172 for (logical, hash) in cases {
173 let parsed = parse_deployed_name(&deployed_name(logical, &hash))?;
174 assert_eq!(parsed.logical(), logical);
175 assert_eq!(parsed.hash(), &hash);
176 }
177
178 Ok(())
179 }
180
181 #[test]
182 fn inverse_then_forward_recovers_deployed_name() -> Result<(), NamespaceError> {
183 let original = deployed_name("workflow_with_underscores", &hash(0x42));
184 let parsed = parse_deployed_name(&original)?;
185 let recovered = deployed_name(parsed.logical(), parsed.hash());
186
187 assert_eq!(recovered, original);
188 Ok(())
189 }
190
191 #[test]
192 fn parse_preserves_separator_neighbouring_chars() -> Result<(), NamespaceError> {
193 let original = deployed_name("logical_name_with_underscores", &hash(0x33));
194 let parsed = parse_deployed_name(&original)?;
195
196 assert_eq!(parsed.logical(), "logical_name_with_underscores");
197 assert_eq!(deployed_name(parsed.logical(), parsed.hash()), original);
198 Ok(())
199 }
200
201 #[test]
202 fn malformed_deployed_names_return_typed_errors() {
203 assert_eq!(
204 parse_deployed_name("workflow_without_hash"),
205 Err(NamespaceError::MissingSeparator)
206 );
207 assert_eq!(
208 parse_deployed_name(
209 "$0000000000000000000000000000000000000000000000000000000000000000"
210 ),
211 Err(NamespaceError::EmptyLogicalName)
212 );
213 assert_eq!(
214 parse_deployed_name("workflow$not-a-hash"),
215 Err(NamespaceError::InvalidHash {
216 source: ContentHashParseError::InvalidLength { found: 10 }
217 })
218 );
219 assert_eq!(
220 parse_deployed_name(
221 "workflow$nested$0000000000000000000000000000000000000000000000000000000000000000"
222 ),
223 Err(NamespaceError::SeparatorInLogicalName)
224 );
225 }
226
227 #[test]
228 fn deployed_name_sets_for_different_hashes_are_disjoint() -> Result<(), crate::PackageError> {
229 let beams = beam_set()?;
230 let first = deployed_names(&beams, &hash(0x01));
231 let second = deployed_names(&beams, &hash(0x02));
232
233 assert!(first.is_disjoint(&second));
234 Ok(())
235 }
236
237 #[test]
238 fn same_logical_module_under_same_hash_is_idempotent() {
239 let hash = hash(0x55);
240
241 assert_eq!(
242 deployed_name("order_workflow", &hash),
243 deployed_name("order_workflow", &hash)
244 );
245 }
246
247 #[test]
248 fn deployed_names_follow_beam_set_canonical_order() -> Result<(), crate::PackageError> {
249 let beams = beam_set()?;
250 let names = deployed_names(&beams, &hash(0x09));
251 let expected = BTreeSet::from([
252 deployed_name("stdlib/list", &hash(0x09)),
253 deployed_name("workflow/a", &hash(0x09)),
254 deployed_name("workflow/b", &hash(0x09)),
255 ]);
256
257 assert_eq!(names, expected);
258 Ok(())
259 }
260}