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