code_moniker_core/core/uri/
mod.rs1mod 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}