Skip to main content

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}