morphir_core/naming/
fqname.rs1use crate::ir::{Diagnostic, DiagnosticCode, DiagnosticError};
2use crate::naming::{name::Name, path::Path};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
13#[schemars(with = "String")]
14pub struct FQName {
15 pub package_path: Path,
16 pub module_path: Path,
17 pub local_name: Name,
18}
19
20impl FQName {
21 pub fn new(package_path: Path, module_path: Path, local_name: Name) -> Self {
22 Self {
23 package_path,
24 module_path,
25 local_name,
26 }
27 }
28
29 pub fn parse(s: &str) -> Option<Self> {
31 let parts: Vec<&str> = s.split(':').collect();
32 if parts.len() != 3 {
33 return None;
34 }
35 let pkg_params = parts[0];
36 let mod_params = parts[1];
37 let local_name = parts[2];
38 if local_name.is_empty() {
40 return None;
41 }
42
43 Some(Self::new(
44 Path::new(pkg_params),
45 Path::new(mod_params),
46 Name::from(local_name),
47 ))
48 }
49
50 pub fn to_canonical_string(&self) -> String {
52 format!(
53 "{}:{}#{}",
54 self.package_path, self.module_path, self.local_name
55 )
56 }
57
58 pub fn from_canonical_string(s: &str) -> Result<Self, String> {
60 let colon_pos = s
62 .find(':')
63 .ok_or_else(|| format!("missing ':' in FQName: {}", s))?;
64 let package_str = &s[..colon_pos];
65 let rest = &s[colon_pos + 1..];
66
67 let hash_pos = rest
68 .find('#')
69 .ok_or_else(|| format!("missing '#' in FQName: {}", s))?;
70 let module_str = &rest[..hash_pos];
71 let local_str = &rest[hash_pos + 1..];
72
73 Ok(Self::new(
74 Path::from_canonical_string(package_str)?,
75 Path::from_canonical_string(module_str)?,
76 Name::from_canonical_string(local_str)?,
77 ))
78 }
79}
80
81impl std::fmt::Display for FQName {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 write!(f, "{}", self.to_canonical_string())
84 }
85}
86
87impl From<FQName> for String {
88 fn from(fqname: FQName) -> String {
89 fqname.to_canonical_string()
90 }
91}
92
93impl TryFrom<String> for FQName {
94 type Error = String;
95 fn try_from(s: String) -> Result<Self, Self::Error> {
96 FQName::from_canonical_string(&s)
97 }
98}
99
100impl Serialize for FQName {
101 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102 where
103 S: serde::Serializer,
104 {
105 serializer.serialize_str(&self.to_canonical_string())
106 }
107}
108
109impl<'de> Deserialize<'de> for FQName {
118 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
119 where
120 D: serde::Deserializer<'de>,
121 {
122 use serde::de;
123
124 fn carry<E: de::Error>(code: DiagnosticCode, message: impl Into<String>) -> E {
125 E::custom(DiagnosticError(Diagnostic::normalization(
126 code, "/", message,
127 )))
128 }
129
130 let value = serde_json::Value::deserialize(deserializer)?;
131 match value {
132 serde_json::Value::String(text) => FQName::from_canonical_string(&text)
133 .map_err(|error| carry(DiagnosticCode::InvalidFqname, error)),
134 serde_json::Value::Array(items) => {
135 let [package, module, local]: [serde_json::Value; 3] =
136 items.try_into().map_err(|items: Vec<_>| {
137 carry::<D::Error>(
138 DiagnosticCode::InvalidFqname,
139 format!(
140 "a legacy fully qualified name is a package, a module and a local \
141 name, not {} elements",
142 items.len()
143 ),
144 )
145 })?;
146 Ok(FQName::new(
147 serde_json::from_value(package).map_err(de::Error::custom)?,
148 serde_json::from_value(module).map_err(de::Error::custom)?,
149 serde_json::from_value(local).map_err(de::Error::custom)?,
150 ))
151 }
152 _ => Err(carry(
153 DiagnosticCode::InvalidType,
154 "a fully qualified name is a canonical string or a legacy three-element array",
155 )),
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_fqname_parsing() {
166 let fq = FQName::parse("org/pkg:mod/sub:Func").unwrap();
167 assert_eq!(fq.package_path.to_string(), "org/pkg");
168 assert_eq!(fq.module_path.to_string(), "mod/sub");
169 assert_eq!(fq.local_name.to_kebab_case(), "func");
170 }
171
172 #[test]
173 fn test_fqname_roundtrip() {
174 let fq = FQName::parse("my/pkg:my/mod:myFunc").unwrap();
175 let s = fq.to_string();
176 assert_eq!(s, "my/pkg:my/mod#my-func");
177 }
178
179 #[test]
183 fn a_legacy_nested_array_reads_as_a_fully_qualified_name() {
184 let fq: FQName = serde_json::from_str(
185 r#"[[["acme"], ["b", "i"]], [["widget", "kit"]], ["make", "one"]]"#,
186 )
187 .expect("a legacy fully qualified name");
188 assert_eq!(fq.to_canonical_string(), "acme/BI:widget-kit#make-one");
189 }
190
191 #[test]
192 fn the_canonical_string_round_trips_through_serde() {
193 let text = "\"acme/BI:widget-kit#make-one\"";
194 let fq: FQName = serde_json::from_str(text).expect("a canonical fully qualified name");
195 assert_eq!(serde_json::to_string(&fq).unwrap(), text);
196 }
197
198 #[test]
202 fn a_string_that_is_not_a_fully_qualified_name_carries_a_diagnostic() {
203 let error = serde_json::from_str::<FQName>("\"acme/BI\"").unwrap_err();
204 let diagnostic = Diagnostic::from_serde_error(&error).expect("a carried diagnostic");
205 assert_eq!(diagnostic.code, DiagnosticCode::InvalidFqname);
206 assert_eq!(diagnostic.cursor, "/");
207 }
208
209 #[test]
210 fn an_array_of_the_wrong_length_carries_a_diagnostic() {
211 let error = serde_json::from_str::<FQName>(r#"[[["acme"]], [["widget"]]]"#).unwrap_err();
212 let diagnostic = Diagnostic::from_serde_error(&error).expect("a carried diagnostic");
213 assert_eq!(diagnostic.code, DiagnosticCode::InvalidFqname);
214 }
215}