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}
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}