code_moniker_core/core/uri/
serialize.rs1use 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}