Skip to main content

code_moniker_core/core/uri/
serialize.rs

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