mz_ore/
str.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! String utilities.
17
18use std::fmt::{self, Write};
19use std::ops::Deref;
20
21/// Extension methods for [`str`].
22pub trait StrExt {
23    /// Wraps the string slice in a type whose display implementation renders
24    /// the string surrounded by double quotes with any inner double quote
25    /// characters escaped.
26    ///
27    /// # Examples
28    ///
29    /// In the standard case, when the wrapped string does not contain any
30    /// double quote characters:
31    ///
32    /// ```
33    /// use mz_ore::str::StrExt;
34    ///
35    /// let name = "bob";
36    /// let message = format!("unknown user {}", name.quoted());
37    /// assert_eq!(message, r#"unknown user "bob""#);
38    /// ```
39    ///
40    /// In a pathological case:
41    ///
42    /// ```
43    /// use mz_ore::str::StrExt;
44    ///
45    /// let name = r#"b@d"inp!t""#;
46    /// let message = format!("unknown user {}", name.quoted());
47    /// assert_eq!(message, r#"unknown user "b@d\"inp!t\"""#);
48    /// ```
49    fn quoted(&self) -> QuotedStr;
50    /// Same as [`StrExt::quoted`], but also escapes new lines and tabs.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use mz_ore::str::StrExt;
56    ///
57    /// let name = "b@d\"\tinp!t\"\r\n";
58    /// let message = format!("unknown user {}", name.escaped());
59    /// assert_eq!(message, r#"unknown user "b@d\"\tinp!t\"\r\n""#);
60    /// ```
61    fn escaped(&self) -> EscapedStr;
62}
63
64impl StrExt for str {
65    fn quoted(&self) -> QuotedStr {
66        QuotedStr(self)
67    }
68    fn escaped(&self) -> EscapedStr {
69        EscapedStr(self)
70    }
71}
72
73/// Displays a string slice surrounded by double quotes with any inner double
74/// quote characters escaped.
75///
76/// Constructed by [`StrExt::quoted`].
77#[derive(Debug)]
78pub struct QuotedStr<'a>(&'a str);
79
80impl<'a> fmt::Display for QuotedStr<'a> {
81    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
82        f.write_char('"')?;
83        for c in self.chars() {
84            match c {
85                '"' => f.write_str("\\\"")?,
86                _ => f.write_char(c)?,
87            }
88        }
89        f.write_char('"')
90    }
91}
92
93impl<'a> Deref for QuotedStr<'a> {
94    type Target = str;
95
96    fn deref(&self) -> &str {
97        self.0
98    }
99}
100
101/// Same as [`QuotedStr`], but also escapes new lines and tabs.
102///
103/// Constructed by [`StrExt::escaped`].
104#[derive(Debug)]
105pub struct EscapedStr<'a>(&'a str);
106
107impl<'a> fmt::Display for EscapedStr<'a> {
108    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
109        f.write_char('"')?;
110        for c in self.chars() {
111            // We don't use `char::escape_default()` here to prevent escaping Unicode chars.
112            match c {
113                '"' => f.write_str("\\\"")?,
114                '\n' => f.write_str("\\n")?,
115                '\r' => f.write_str("\\r")?,
116                '\t' => f.write_str("\\t")?,
117                _ => f.write_char(c)?,
118            }
119        }
120        f.write_char('"')
121    }
122}
123
124impl<'a> Deref for EscapedStr<'a> {
125    type Target = str;
126
127    fn deref(&self) -> &str {
128        self.0
129    }
130}
131
132/// Creates a type whose [`fmt::Display`] implementation outputs item preceded
133/// by `open` and followed by `close`.
134pub fn bracketed<'a, D>(open: &'a str, close: &'a str, contents: D) -> impl fmt::Display + 'a
135where
136    D: fmt::Display + 'a,
137{
138    struct Bracketed<'a, D> {
139        open: &'a str,
140        close: &'a str,
141        contents: D,
142    }
143
144    impl<'a, D> fmt::Display for Bracketed<'a, D>
145    where
146        D: fmt::Display,
147    {
148        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
149            write!(f, "{}{}{}", self.open, self.contents, self.close)
150        }
151    }
152
153    Bracketed {
154        open,
155        close,
156        contents,
157    }
158}
159
160/// Given a closure, it creates a Display that simply calls the given closure when fmt'd.
161pub fn closure_to_display<F>(fun: F) -> impl fmt::Display
162where
163    F: Fn(&mut fmt::Formatter) -> fmt::Result,
164{
165    struct Mapped<F> {
166        fun: F,
167    }
168
169    impl<F> fmt::Display for Mapped<F>
170    where
171        F: Fn(&mut fmt::Formatter) -> fmt::Result,
172    {
173        fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
174            (self.fun)(formatter)
175        }
176    }
177
178    Mapped { fun }
179}
180
181/// Creates a type whose [`fmt::Display`] implementation outputs each item in
182/// `iter` separated by `separator`.
183pub fn separated<'a, I>(separator: &'a str, iter: I) -> impl fmt::Display + 'a
184where
185    I: IntoIterator,
186    I::IntoIter: Clone + 'a,
187    I::Item: fmt::Display + 'a,
188{
189    struct Separated<'a, I> {
190        separator: &'a str,
191        iter: I,
192    }
193
194    impl<'a, I> fmt::Display for Separated<'a, I>
195    where
196        I: Iterator + Clone,
197        I::Item: fmt::Display,
198    {
199        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
200            for (i, item) in self.iter.clone().enumerate() {
201                if i != 0 {
202                    write!(f, "{}", self.separator)?;
203                }
204                write!(f, "{}", item)?;
205            }
206            Ok(())
207        }
208    }
209
210    Separated {
211        separator,
212        iter: iter.into_iter(),
213    }
214}
215
216/// A helper struct to keep track of indentation levels.
217///
218/// This will be most often used as part of the rendering context
219/// type for various `Display$Format` implementation.
220#[derive(Debug, Clone)]
221pub struct Indent {
222    unit: String,
223    buff: String,
224    mark: Vec<usize>,
225}
226
227impl AsMut<Indent> for Indent {
228    fn as_mut(&mut self) -> &mut Indent {
229        self
230    }
231}
232
233impl Indent {
234    /// Construct a new `Indent` where one level is represented
235    /// by the given `unit` repeated `step` times.
236    pub fn new(unit: char, step: usize) -> Indent {
237        Indent {
238            unit: std::iter::repeat(unit).take(step).collect::<String>(),
239            buff: String::with_capacity(unit.len_utf8()),
240            mark: vec![],
241        }
242    }
243
244    fn inc(&mut self, rhs: usize) {
245        for _ in 0..rhs {
246            self.buff += &self.unit;
247        }
248    }
249
250    fn dec(&mut self, rhs: usize) {
251        let tail = rhs.saturating_mul(self.unit.len());
252        let head = self.buff.len().saturating_sub(tail);
253        self.buff.truncate(head);
254    }
255
256    /// Remember the current state.
257    pub fn set(&mut self) {
258        self.mark.push(self.buff.len());
259    }
260
261    /// Reset `buff` to the last marked state.
262    pub fn reset(&mut self) {
263        if let Some(len) = self.mark.pop() {
264            while self.buff.len() < len {
265                self.buff += &self.unit;
266            }
267            self.buff.truncate(len);
268        }
269    }
270}
271
272/// Convenience methods for pretty-printing based on indentation
273/// that are automatically available for context objects that can
274/// be mutably referenced as an [`Indent`] instance.
275pub trait IndentLike {
276    /// Print a block of code defined in `f` one step deeper
277    /// from the current [`Indent`].
278    fn indented<F>(&mut self, f: F) -> fmt::Result
279    where
280        F: FnMut(&mut Self) -> fmt::Result;
281
282    /// Same as [`IndentLike::indented`], but the `f` only going to be printed
283    /// in an indented context if `guard` is `true`.
284    fn indented_if<F>(&mut self, guard: bool, f: F) -> fmt::Result
285    where
286        F: FnMut(&mut Self) -> fmt::Result;
287}
288
289impl<T: AsMut<Indent>> IndentLike for T {
290    fn indented<F>(&mut self, mut f: F) -> fmt::Result
291    where
292        F: FnMut(&mut Self) -> fmt::Result,
293    {
294        *self.as_mut() += 1;
295        let result = f(self);
296        *self.as_mut() -= 1;
297        result
298    }
299
300    fn indented_if<F>(&mut self, guard: bool, mut f: F) -> fmt::Result
301    where
302        F: FnMut(&mut Self) -> fmt::Result,
303    {
304        if guard {
305            *self.as_mut() += 1;
306        }
307        let result = f(self);
308
309        if guard {
310            *self.as_mut() -= 1;
311        }
312        result
313    }
314}
315
316impl Default for Indent {
317    fn default() -> Self {
318        Indent::new(' ', 2)
319    }
320}
321
322impl std::ops::AddAssign<usize> for Indent {
323    fn add_assign(&mut self, rhs: usize) {
324        self.inc(rhs)
325    }
326}
327
328impl std::ops::SubAssign<usize> for Indent {
329    fn sub_assign(&mut self, rhs: usize) {
330        self.dec(rhs)
331    }
332}
333
334impl fmt::Display for Indent {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        f.write_str(&self.buff)
337    }
338}
339
340/// Newtype wrapper around [`String`] whose _byte_ length is guaranteed to be less than or equal to
341/// the provided `MAX`.
342#[derive(Debug, Clone, PartialEq)]
343pub struct MaxLenString<const MAX: usize>(String);
344
345impl<const MAX: usize> MaxLenString<MAX> {
346    /// Creates a new [`MaxLenString`] returning an error if `s` is more than `MAX` bytes long.
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use mz_ore::str::MaxLenString;
352    ///
353    /// type ShortString = MaxLenString<30>;
354    ///
355    /// let good = ShortString::new("hello".to_string()).unwrap();
356    /// assert_eq!(good.as_str(), "hello");
357    ///
358    /// // Note: this is only 8 characters, but each character requires 4 bytes.
359    /// let too_long = "😊😊😊😊😊😊😊😊";
360    /// let smol = ShortString::new(too_long.to_string());
361    /// assert!(smol.is_err());
362    /// ```
363    ///
364    pub fn new(s: String) -> Result<Self, String> {
365        if s.len() > MAX {
366            return Err(s);
367        }
368
369        Ok(MaxLenString(s))
370    }
371
372    /// Consume self, returning the inner [`String`].
373    pub fn into_inner(self) -> String {
374        self.0
375    }
376
377    /// Returns a reference to the underlying string.
378    pub fn as_str(&self) -> &str {
379        self
380    }
381}
382
383impl<const MAX: usize> Deref for MaxLenString<MAX> {
384    type Target = str;
385
386    fn deref(&self) -> &Self::Target {
387        &self.0
388    }
389}
390
391impl<const MAX: usize> fmt::Display for MaxLenString<MAX> {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        f.write_str(&self.0)
394    }
395}
396
397impl<const MAX: usize> TryFrom<String> for MaxLenString<MAX> {
398    type Error = String;
399
400    fn try_from(s: String) -> Result<Self, Self::Error> {
401        Self::new(s)
402    }
403}
404
405impl<'a, const MAX: usize> TryFrom<&'a String> for MaxLenString<MAX> {
406    type Error = String;
407
408    fn try_from(s: &'a String) -> Result<Self, Self::Error> {
409        Self::try_from(s.clone())
410    }
411}
412
413impl<'a, const MAX: usize> TryFrom<&'a str> for MaxLenString<MAX> {
414    type Error = String;
415
416    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
417        Self::try_from(String::from(s))
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[crate::test]
426    fn test_indent() {
427        let mut indent = Indent::new('~', 3);
428        indent += 1;
429        assert_eq!(indent.to_string(), "~~~".to_string());
430        indent += 3;
431        assert_eq!(indent.to_string(), "~~~~~~~~~~~~".to_string());
432        indent -= 2;
433        assert_eq!(indent.to_string(), "~~~~~~".to_string());
434        indent -= 4;
435        assert_eq!(indent.to_string(), "".to_string());
436        indent += 1;
437        assert_eq!(indent.to_string(), "~~~".to_string());
438    }
439}