Skip to main content

code_moniker_core/core/uri/
mod.rs

1//! ```text
2//! <scheme><project>(/<kind>:<name>)*
3//! ```
4//! `kind` ∈ `[A-Za-z][A-Za-z0-9_]*`. A `name` containing `/`, backtick,
5//! or ASCII whitespace is backtick-wrapped; a literal backtick inside is
6//! doubled.
7
8mod parse;
9mod serialize;
10
11pub use parse::from_uri;
12pub use serialize::to_uri;
13
14#[derive(Clone, Eq, PartialEq, Debug)]
15pub enum UriError {
16	MissingScheme(String),
17	MissingProject,
18	EmptySegment(usize),
19	MissingKindSeparator(usize),
20	InvalidKind(String),
21	UnterminatedBacktick(usize),
22	TrailingAfterBacktick(usize),
23	NonUtf8Project,
24	NonUtf8Segment,
25}
26
27impl std::fmt::Display for UriError {
28	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29		match self {
30			Self::MissingScheme(expected) => {
31				write!(
32					f,
33					"URI does not start with the expected scheme `{expected}`"
34				)
35			}
36			Self::MissingProject => write!(f, "URI has no project authority"),
37			Self::EmptySegment(pos) => write!(f, "empty segment at byte {pos}"),
38			Self::MissingKindSeparator(pos) => {
39				write!(f, "segment at byte {pos} has no `:` between kind and name")
40			}
41			Self::InvalidKind(s) => write!(
42				f,
43				"kind `{s}` is not a plain identifier ([A-Za-z][A-Za-z0-9_]*)"
44			),
45			Self::UnterminatedBacktick(pos) => {
46				write!(f, "unterminated backtick-quoted name at byte {pos}")
47			}
48			Self::TrailingAfterBacktick(pos) => {
49				write!(
50					f,
51					"backtick-quoted name at byte {pos} is followed by data \
52					 other than a `/` separator"
53				)
54			}
55			Self::NonUtf8Project => write!(f, "project authority must be valid UTF-8"),
56			Self::NonUtf8Segment => write!(f, "segment must be valid UTF-8"),
57		}
58	}
59}
60
61impl std::error::Error for UriError {}
62
63#[derive(Copy, Clone, Debug)]
64pub struct UriConfig<'a> {
65	pub scheme: &'a str,
66}
67
68impl Default for UriConfig<'_> {
69	fn default() -> Self {
70		Self {
71			scheme: "code+moniker://",
72		}
73	}
74}
75
76#[cfg(test)]
77mod test_helpers {
78	use super::UriConfig;
79
80	pub fn default_config() -> UriConfig<'static> {
81		UriConfig {
82			scheme: "esac+moniker://",
83		}
84	}
85}
86
87#[cfg(test)]
88mod tests {
89	use super::test_helpers::*;
90	use super::*;
91
92	#[test]
93	fn roundtrip_simple() {
94		let original = "esac+moniker://my-app/path:main/path:com/path:acme/class:Foo/method:bar(2)";
95		let m = from_uri(original, &default_config()).unwrap();
96		assert_eq!(to_uri(&m, &default_config()).unwrap(), original);
97	}
98
99	#[test]
100	fn roundtrip_with_escapes() {
101		let original = "esac+moniker://app/path:`util/test.ts`/class:`weird``name`";
102		let m = from_uri(original, &default_config()).unwrap();
103		assert_eq!(to_uri(&m, &default_config()).unwrap(), original);
104	}
105
106	#[test]
107	fn roundtrip_project_only() {
108		let original = "esac+moniker://my-app";
109		let m = from_uri(original, &default_config()).unwrap();
110		assert_eq!(to_uri(&m, &default_config()).unwrap(), original);
111	}
112
113	#[test]
114	fn roundtrip_typed_callable_names_with_quoting_chars() {
115		use crate::core::moniker::MonikerBuilder;
116		let names: &[&[u8]] = &[
117			b"foo(int,String)",
118			b"f((x: number) => string)",
119			b"f(string | null)",
120			b"render(Map<String, List<Item>>)",
121			b"foo with spaces",
122		];
123		for name in names {
124			let m = MonikerBuilder::new()
125				.project(b"app")
126				.segment(b"path", b"x")
127				.segment(b"function", name)
128				.build();
129			let s = to_uri(&m, &default_config()).expect("serialize");
130			let parsed = from_uri(&s, &default_config())
131				.unwrap_or_else(|e| panic!("roundtrip failed on {s:?}: {e}"));
132			assert_eq!(parsed, m, "roundtrip mismatch for {s:?}");
133		}
134	}
135}