code_moniker_core/core/uri/
serialize.rs1use super::UriConfig;
2use crate::core::moniker::Moniker;
3
4pub fn to_uri(moniker: &Moniker, config: &UriConfig<'_>) -> String {
5 let view = moniker.as_view();
6 let mut out = String::with_capacity(config.scheme.len() + view.as_encoded().len() + 16);
7 out.push_str(config.scheme);
8 write_name(&mut out, view.project());
9
10 for seg in view.segments() {
11 out.push('/');
12 let kind = match std::str::from_utf8(seg.kind) {
13 Ok(kind) => kind,
14 Err(_) => panic!("moniker segment kind must be UTF-8"),
15 };
16 out.push_str(kind);
17 out.push(':');
18 write_name(&mut out, seg.name);
19 }
20
21 out
22}
23
24fn name_needs_escaping(bytes: &[u8]) -> bool {
25 bytes.is_empty()
26 || bytes
27 .iter()
28 .any(|b| *b == b'/' || *b == b'`' || b.is_ascii_whitespace())
29}
30
31fn write_name(out: &mut String, bytes: &[u8]) {
32 let s = match std::str::from_utf8(bytes) {
33 Ok(s) => s,
34 Err(_) => panic!("moniker project and segment names must be UTF-8"),
35 };
36 if !name_needs_escaping(bytes) {
37 out.push_str(s);
38 return;
39 }
40 out.push('`');
41 for c in s.chars() {
42 if c == '`' {
43 out.push_str("``");
44 } else {
45 out.push(c);
46 }
47 }
48 out.push('`');
49}
50
51#[cfg(test)]
52mod tests {
53 use super::super::test_helpers::*;
54 use super::*;
55 use crate::core::moniker::MonikerBuilder;
56
57 #[test]
58 fn to_uri_project_only() {
59 let m = MonikerBuilder::new().project(b"my-app").build();
60 assert_eq!(to_uri(&m, &default_config()), "esac+moniker://my-app");
61 }
62
63 #[test]
64 fn to_uri_path_chain() {
65 let m = MonikerBuilder::new()
66 .project(b"my-app")
67 .segment(b"path", b"main")
68 .segment(b"path", b"com")
69 .segment(b"path", b"acme")
70 .segment(b"class", b"Foo")
71 .build();
72 assert_eq!(
73 to_uri(&m, &default_config()),
74 "esac+moniker://my-app/path:main/path:com/path:acme/class:Foo"
75 );
76 }
77
78 #[test]
79 fn to_uri_method_no_arity_in_name() {
80 let m = MonikerBuilder::new()
81 .project(b"my-app")
82 .segment(b"path", b"main")
83 .segment(b"class", b"Foo")
84 .segment(b"method", b"bar()")
85 .build();
86 assert_eq!(
87 to_uri(&m, &default_config()),
88 "esac+moniker://my-app/path:main/class:Foo/method:bar()"
89 );
90 }
91
92 #[test]
93 fn to_uri_method_with_arity_in_name() {
94 let m = MonikerBuilder::new()
95 .project(b"app")
96 .segment(b"class", b"Foo")
97 .segment(b"method", b"bar(2)")
98 .build();
99 assert_eq!(
100 to_uri(&m, &default_config()),
101 "esac+moniker://app/class:Foo/method:bar(2)"
102 );
103 }
104
105 #[test]
106 fn to_uri_escapes_slash_in_name() {
107 let m = MonikerBuilder::new()
108 .project(b"app")
109 .segment(b"path", b"util/test.ts")
110 .build();
111 assert_eq!(
112 to_uri(&m, &default_config()),
113 "esac+moniker://app/path:`util/test.ts`"
114 );
115 }
116
117 #[test]
118 fn to_uri_escapes_backtick() {
119 let m = MonikerBuilder::new()
120 .project(b"app")
121 .segment(b"class", b"weird`name")
122 .build();
123 assert_eq!(
124 to_uri(&m, &default_config()),
125 "esac+moniker://app/class:`weird``name`"
126 );
127 }
128
129 use proptest::prelude::*;
130
131 fn arb_moniker() -> impl Strategy<Value = crate::core::moniker::Moniker> {
132 use crate::core::moniker::MonikerBuilder;
133 (
134 "[a-zA-Z][a-zA-Z0-9_-]{0,15}",
135 proptest::collection::vec(("[a-zA-Z][a-zA-Z0-9_]{0,7}", "\\PC{0,32}"), 0..6),
136 )
137 .prop_map(|(project, segs)| {
138 let mut b = MonikerBuilder::new();
139 b.project(project.as_bytes());
140 for (kind, name) in &segs {
141 b.segment(kind.as_bytes(), name.as_bytes());
142 }
143 b.build()
144 })
145 }
146
147 proptest! {
148 #![proptest_config(ProptestConfig {
149 cases: 256,
150 ..ProptestConfig::default()
151 })]
152
153 #[test]
154 fn to_uri_from_uri_roundtrip(m in arb_moniker()) {
155 let s = to_uri(&m, &default_config());
156 let m2 = super::super::parse::from_uri(&s, &default_config())
157 .expect("from_uri must accept what to_uri produced");
158 prop_assert_eq!(m, m2);
159 }
160 }
161}