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