Skip to main content

code_moniker_core/core/uri/
serialize.rs

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