zrx_id/id/format/builder.rs
1// Copyright (c) 2025-2026 Zensical and contributors
2
3// SPDX-License-Identifier: MIT
4// All contributions are certified under the DCO
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to
8// deal in the Software without restriction, including without limitation the
9// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10// sell copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12
13// The above copyright notice and this permission notice shall be included in
14// all copies or substantial portions of the Software.
15
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22// IN THE SOFTWARE.
23
24// ----------------------------------------------------------------------------
25
26//! Formatted string builder.
27
28use std::array;
29use std::borrow::Cow;
30
31use super::encoding::encode;
32use super::error::{Error, Result};
33use super::path::validate;
34use super::Format;
35
36// ----------------------------------------------------------------------------
37// Structs
38// ----------------------------------------------------------------------------
39
40/// Formatted string builder.
41#[derive(Clone, Debug)]
42pub struct Builder<'a, const N: usize> {
43 /// Formatted string source, if any.
44 source: Option<&'a Format<N>>,
45 /// Component values.
46 values: [Option<Cow<'a, str>>; N],
47}
48
49// ----------------------------------------------------------------------------
50// Implementations
51// ----------------------------------------------------------------------------
52
53impl<const N: usize> Format<N> {
54 /// Creates a formatted string builder.
55 ///
56 /// # Examples
57 ///
58 /// ```
59 /// use zrx_id::format::Format;
60 ///
61 /// // Create formatted string builder
62 /// let mut builder = Format::<3>::builder();
63 /// ```
64 #[inline]
65 #[must_use]
66 pub fn builder<'a>() -> Builder<'a, N> {
67 Builder::default()
68 }
69
70 /// Creates a formatted string builder from the formatted string.
71 ///
72 /// This method creates a builder from the current formatted string, which
73 /// allows to modify components and build a new formatted string.
74 ///
75 /// # Examples
76 ///
77 /// ```
78 /// # use std::error::Error;
79 /// # fn main() -> Result<(), Box<dyn Error>> {
80 /// use zrx_id::format::Format;
81 ///
82 /// // Create formatted string from string
83 /// let format: Format::<3> = "a:b:c".parse()?;
84 ///
85 /// // Create formatted string builder
86 /// let mut builder = format.to_builder();
87 /// builder.set(2, "d");
88 ///
89 /// // Create formatted string from builder
90 /// let format = builder.build()?;
91 /// assert_eq!(format.as_str(), "a:b:d");
92 /// # Ok(())
93 /// # }
94 /// ```
95 #[inline]
96 #[must_use]
97 pub fn to_builder(&self) -> Builder<'_, N> {
98 Builder {
99 source: Some(self),
100 values: [const { None }; N],
101 }
102 }
103}
104
105// ----------------------------------------------------------------------------
106
107impl<'a, const N: usize> Builder<'a, N> {
108 /// Sets the value at the index.
109 ///
110 /// # Panics
111 ///
112 /// Panics if the index is out of bounds. Since [`Format`] is a low-level
113 /// construct, we don't expect this to happen, as indices should be known.
114 ///
115 /// # Examples
116 ///
117 /// ```
118 /// use zrx_id::format::Format;
119 ///
120 /// // Create formatted string builder and set value
121 /// let mut builder = Format::<3>::builder().with(0, "a");
122 /// ```
123 #[inline]
124 #[must_use]
125 pub fn with<S>(mut self, index: usize, value: S) -> Self
126 where
127 S: Into<Cow<'a, str>>,
128 {
129 self.set(index, value);
130 self
131 }
132
133 /// Sets the value at the index.
134 ///
135 /// This method accepts all types that can be converted into a reference to
136 /// a string slice, most prominently [`str`] and [`String`].
137 ///
138 /// # Panics
139 ///
140 /// Panics if the index is out of bounds. Since [`Format`] is a low-level
141 /// construct, we don't expect this to happen, as indices should be known.
142 ///
143 /// # Examples
144 ///
145 /// ```
146 /// use zrx_id::format::Format;
147 ///
148 /// // Create formatted string builder and set value
149 /// let mut builder = Format::<3>::builder();
150 /// builder.set(0, "a");
151 /// ```
152 #[inline]
153 pub fn set<S>(&mut self, index: usize, value: S) -> &mut Self
154 where
155 S: Into<Cow<'a, str>>,
156 {
157 self.values[index] = Some(value.into());
158 self
159 }
160
161 /// Builds the formatted string.
162 ///
163 /// This method consumes the builder and constructs a [`Format`] from the
164 /// provided values. If no value is set for a component, it's represented
165 /// as an empty component in the resulting formatted string. [`Format::get`]
166 /// returns [`None`] for such components.
167 ///
168 /// # Errors
169 ///
170 /// If a span overflows, [`Error::Overflow`] is returned.
171 ///
172 /// ```
173 /// # use std::error::Error;
174 /// # fn main() -> Result<(), Box<dyn Error>> {
175 /// use zrx_id::format::Format;
176 ///
177 /// // Create formatted string builder
178 /// let mut builder = Format::<3>::builder();
179 /// builder.set(0, "a");
180 /// builder.set(1, "b");
181 /// builder.set(2, "c");
182 ///
183 /// // Create formatted string from builder
184 /// let format = builder.build()?;
185 ///
186 /// // Obtain string representation
187 /// assert_eq!(format.as_str(), "a:b:c");
188 /// # Ok(())
189 /// # }
190 /// ```
191 pub fn build(self) -> Result<Format<N>> {
192 let mut spans = array::from_fn(|_| 0u16..0u16);
193 let mut flags = 0;
194
195 // Compute the minimum capacity by using the length from the formatted
196 // string source, if any, or a reasonable default. This turns out to be
197 // significantly faster than summing up individual component lengths,
198 // especially if only a few components are modified.
199 let capacity = self.source.map_or(64, |format| format.value.len());
200 let mut buffer = Vec::with_capacity(capacity);
201
202 // Write all components to the buffer, interspersed with `:` separators,
203 // and percent-encode each component if it contains `:` characters
204 for (index, opt) in self.values.into_iter().enumerate() {
205 if index > 0 {
206 buffer.push(b':');
207 }
208
209 // Compute the starting position of the current component, and make
210 // sure the index fits into 16 bits, or return an overflow error
211 let start =
212 u16::try_from(buffer.len()).map_err(|_| Error::Overflow)?;
213
214 // If no value is set for this component, but we have a formatted
215 // string source, we can just copy the component from there, since
216 // we can be sure that the encoding is already correct, if any, and
217 // can thus skip encoding and validation
218 if let (None, Some(format)) = (opt.as_ref(), self.source) {
219 let p = format.spans[index].start as usize;
220 let q = format.spans[index].end as usize;
221
222 // Write component from formatted string source and compute the
223 // ending position of the current component
224 buffer.extend_from_slice(&format.value[p..q]);
225 let end =
226 u16::try_from(buffer.len()).map_err(|_| Error::Overflow)?;
227
228 // Store span for current component, and copy encoding flags
229 // from formatted string source, since they are identical
230 spans[index] = start..end;
231 flags |= format.flags & (1 << index);
232 continue;
233 }
234
235 // If no value is set for this component, we append a colon and set
236 // the span to an empty range at the current position
237 let Some(value) = opt else {
238 spans[index] = start..start;
239 continue;
240 };
241
242 // Percent-encode the current component, and remember if encoding is
243 // necessary by inspecting if the encoded value is borrowed or owned
244 let value = encode(value.as_bytes());
245 match value {
246 Cow::Borrowed(_) => flags &= !(1 << index),
247 Cow::Owned(_) => flags |= 1 << index,
248 }
249
250 // Ensure that the given value is a valid path
251 validate(&value)?;
252
253 // Compute the ending position of the current component, and make
254 // sure the index fits into 16 bits, or return an overflow error
255 buffer.extend_from_slice(value.as_bytes());
256 let end =
257 u16::try_from(buffer.len()).map_err(|_| Error::Overflow)?;
258
259 // Store span for current component
260 spans[index] = start..end;
261 }
262
263 // Return formatted string
264 Ok(Format {
265 value: buffer.into(),
266 spans,
267 flags,
268 })
269 }
270}
271
272// ----------------------------------------------------------------------------
273// Trait implementations
274// ----------------------------------------------------------------------------
275
276impl<const N: usize> Default for Builder<'_, N> {
277 /// Creates a formatted string builder.
278 ///
279 /// # Examples
280 ///
281 /// ```
282 /// use zrx_id::format::Builder;
283 ///
284 /// // Create formatted string builder
285 /// let mut builder = Builder::<3>::default();
286 /// ```
287 #[inline]
288 fn default() -> Self {
289 Self {
290 source: None,
291 values: [const { None }; N],
292 }
293 }
294}