Skip to main content

code_moniker_core/core/moniker/
builder.rs

1// code-moniker: ignore-file[smell-feature-envy-local, smell-balanced-method-fanout, smell-harmonious-method-size]
2// TODO(smell): keep MonikerBuilder as the encoded-moniker construction boundary; revisit this suppression if parsing or query behavior moves here.
3use super::encoding::{EncodingError, HEADER_FIXED_LEN, VERSION, is_uri_kind, write_u16};
4use super::{Moniker, MonikerView};
5
6const MAX_COMPONENT_LEN: usize = u16::MAX as usize;
7
8/// Builder for the internal encoded `Moniker` value.
9///
10/// Use `try_build` for untrusted bytes. `build` is the trusted path for code
11/// that has already enforced the URI-safe moniker invariants.
12#[derive(Default, Debug)]
13pub struct MonikerBuilder {
14	project: Vec<u8>,
15	segments: Vec<(Vec<u8>, Vec<u8>)>,
16}
17
18impl MonikerBuilder {
19	pub fn new() -> Self {
20		Self::default()
21	}
22
23	pub fn from_view(view: MonikerView<'_>) -> Self {
24		let mut b = Self::new();
25		b.project(view.project());
26		for seg in view.segments() {
27			b.segment(seg.kind, seg.name);
28		}
29		b
30	}
31
32	pub fn truncate(&mut self, len: usize) {
33		self.segments.truncate(len);
34	}
35
36	pub fn project(&mut self, project: &[u8]) -> &mut Self {
37		self.project.clear();
38		self.project.extend_from_slice(project);
39		self
40	}
41
42	pub fn segment(&mut self, kind: &[u8], name: &[u8]) -> &mut Self {
43		self.segments.push((kind.to_vec(), name.to_vec()));
44		self
45	}
46
47	pub fn build(&self) -> Moniker {
48		self.try_build()
49			.unwrap_or_else(|error| panic!("invalid moniker builder input: {error}"))
50	}
51
52	pub fn try_build(&self) -> Result<Moniker, EncodingError> {
53		if self.project.is_empty() {
54			return Err(EncodingError::EmptyProject);
55		}
56		std::str::from_utf8(&self.project).map_err(|_| EncodingError::NonUtf8Project)?;
57		if self.project.len() > MAX_COMPONENT_LEN {
58			return Err(EncodingError::ProjectTooLong(self.project.len()));
59		}
60		let mut buf = Vec::with_capacity(self.estimated_size());
61		buf.push(VERSION);
62		write_u16(&mut buf, self.project.len() as u16);
63		buf.extend_from_slice(&self.project);
64		for (kind, name) in &self.segments {
65			std::str::from_utf8(kind).map_err(|_| EncodingError::NonUtf8SegmentKind)?;
66			if !is_uri_kind(kind) {
67				return Err(EncodingError::InvalidSegmentKind);
68			}
69			std::str::from_utf8(name).map_err(|_| EncodingError::NonUtf8SegmentName)?;
70			if kind.len() > MAX_COMPONENT_LEN {
71				return Err(EncodingError::SegmentKindTooLong(kind.len()));
72			}
73			if name.len() > MAX_COMPONENT_LEN {
74				return Err(EncodingError::SegmentNameTooLong(name.len()));
75			}
76			write_u16(&mut buf, kind.len() as u16);
77			buf.extend_from_slice(kind);
78			write_u16(&mut buf, name.len() as u16);
79			buf.extend_from_slice(name);
80		}
81		Ok(Moniker::from_encoded_unchecked(buf))
82	}
83
84	fn estimated_size(&self) -> usize {
85		HEADER_FIXED_LEN
86			+ self.project.len()
87			+ self
88				.segments
89				.iter()
90				.map(|(k, n)| 2 + k.len() + 2 + n.len())
91				.sum::<usize>()
92	}
93}
94
95#[cfg(test)]
96mod tests {
97	use super::super::Segment;
98	use super::*;
99
100	#[test]
101	#[should_panic(expected = "project must not be empty")]
102	fn builder_panics_on_empty_project() {
103		MonikerBuilder::new().build();
104	}
105
106	#[test]
107	fn builder_with_project_no_segments() {
108		let m = MonikerBuilder::new().project(b"my-app").build();
109		let v = m.as_view();
110		assert_eq!(v.project(), b"my-app");
111		assert_eq!(v.segment_count(), 0);
112	}
113
114	#[test]
115	fn builder_with_segments() {
116		let m = MonikerBuilder::new()
117			.project(b"my-app")
118			.segment(b"module", b"main")
119			.segment(b"package", b"com")
120			.segment(b"package", b"acme")
121			.segment(b"class", b"Foo")
122			.build();
123		let v = m.as_view();
124		assert_eq!(v.segment_count(), 4);
125		let segs: Vec<_> = v.segments().collect();
126		assert_eq!(
127			segs[0],
128			Segment {
129				kind: b"module",
130				name: b"main"
131			}
132		);
133		assert_eq!(
134			segs[3],
135			Segment {
136				kind: b"class",
137				name: b"Foo"
138			}
139		);
140	}
141
142	#[test]
143	fn builder_method_with_arity_in_name() {
144		let m = MonikerBuilder::new()
145			.project(b"app")
146			.segment(b"class", b"Foo")
147			.segment(b"method", b"bar()")
148			.segment(b"method", b"bar(2)")
149			.build();
150		let segs: Vec<_> = m.as_view().segments().collect();
151		assert_eq!(segs[1].name, b"bar()");
152		assert_eq!(segs[2].name, b"bar(2)");
153	}
154
155	#[test]
156	fn builder_accepts_max_length_component() {
157		let big = vec![b'a'; MAX_COMPONENT_LEN];
158		let m = MonikerBuilder::new()
159			.project(&big)
160			.segment(b"path", &big)
161			.build();
162		let v = m.as_view();
163		assert_eq!(v.project().len(), MAX_COMPONENT_LEN);
164		let seg = v.segments().next().unwrap();
165		assert_eq!(seg.name.len(), MAX_COMPONENT_LEN);
166	}
167
168	#[test]
169	#[should_panic(expected = "project longer than u16::MAX bytes")]
170	fn builder_panics_on_oversized_project() {
171		let oversize = vec![b'a'; MAX_COMPONENT_LEN + 1];
172		MonikerBuilder::new().project(&oversize).build();
173	}
174
175	#[test]
176	#[should_panic(expected = "segment kind longer than u16::MAX bytes")]
177	fn builder_panics_on_oversized_segment_kind() {
178		let oversize = vec![b'a'; MAX_COMPONENT_LEN + 1];
179		MonikerBuilder::new()
180			.project(b"app")
181			.segment(&oversize, b"x")
182			.build();
183	}
184
185	#[test]
186	#[should_panic(expected = "segment name longer than u16::MAX bytes")]
187	fn builder_panics_on_oversized_segment_name() {
188		let oversize = vec![b'a'; MAX_COMPONENT_LEN + 1];
189		MonikerBuilder::new()
190			.project(b"app")
191			.segment(b"path", &oversize)
192			.build();
193	}
194}