| // Part of the Crubit project, under the Apache License v2.0 with LLVM | |
| // Exceptions. See /LICENSE for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| //! Dynamically parsed format strings, similar to `absl::Format<'s', ...>`, but using | |
| //! Rust's format-string syntax. | |
| use anyhow::{bail, ensure, Result}; | |
| // Note: needs to be Send to be used with clap. | |
| use std::sync::Arc; | |
| /// A (dynamically parsed) format string with NUM_VARIABLES substitutions. | |
| #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] | |
| pub struct Format<const NUM_VARIABLES: usize> { | |
| fragments: Arc<[Fragment]>, | |
| } | |
| #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] | |
| enum Fragment { | |
| /// A string fragment. | |
| String(String), | |
| /// A variable 0 <= n < NUM_VARIABLES | |
| Variable(usize), | |
| } | |
| impl<const N: usize> Format<N> { | |
| /// Parse a format string, using textual names for each variable. | |
| /// | |
| /// The order of the variable names in `variable_names` corresponds with the order of | |
| /// substitutions in the `format()` call later. | |
| pub fn parse_with_metavars( | |
| format_string: &str, | |
| variable_names: &[&'static str; N], | |
| ) -> Result<Self> { | |
| let mut fragments = Vec::new(); | |
| let mut processed_string = format_string; | |
| fn push_str(fragments: &mut Vec<Fragment>, s: &str) { | |
| if let Some(Fragment::String(last_string)) = fragments.last_mut() { | |
| last_string.push_str(s) | |
| } else { | |
| fragments.push(Fragment::String(s.to_string())); | |
| } | |
| } | |
| while let Some(i) = processed_string.find(&['{', '}']) { | |
| if processed_string[i..].starts_with("{{") || processed_string[i..].starts_with("}}") { | |
| push_str(&mut fragments, &processed_string[..=i]); | |
| processed_string = &processed_string[(i + 2)..]; | |
| continue; | |
| } | |
| ensure!( | |
| !processed_string[i..].starts_with('}'), | |
| "invalid format string: unmatched `}}`: {format_string:?}" | |
| ); | |
| push_str(&mut fragments, &processed_string[..i]); | |
| processed_string = &processed_string[(i + 1)..]; | |
| let Some(end) = processed_string.find('}') else { | |
| bail!("invalid format string: unmatched `{{`: {format_string:?}"); | |
| }; | |
| let variable_name = processed_string[..end].trim(); | |
| processed_string = &processed_string[(end + 1)..]; | |
| let Some(variable_index) = variable_names.iter().position(|x| *x == variable_name) | |
| else { | |
| bail!( | |
| "invalid format string: unknown variable `{variable_name}`: {format_string:?}" | |
| ); | |
| }; | |
| fragments.push(Fragment::Variable(variable_index)); | |
| } | |
| if !processed_string.is_empty() { | |
| push_str(&mut fragments, processed_string); | |
| } | |
| Ok(Self { fragments: fragments.into() }) | |
| } | |
| /// Formats a string using the provided substitutions. | |
| /// | |
| /// The args correspond to the variable names passed to `parse_with_metavars()`, in the order | |
| /// in which they were provided. | |
| /// | |
| /// For example: | |
| /// | |
| /// ``` | |
| /// let format_string = Format::parse_with_metavars( | |
| /// "/{arg2}/{arg1}", &["arg1", "arg2"]).unwrap(); | |
| /// assert_eq!(format_string.format(["a", "b"]).as_str(), "b/a"); | |
| /// ``` | |
| pub fn format(&self, args: &[&str; N]) -> String { | |
| let it = self.fragments.iter().map(|f| match f { | |
| Fragment::String(s) => s.as_str(), | |
| Fragment::Variable(i) => args[*i], | |
| }); | |
| let length = it.clone().map(|s| s.len()).sum(); | |
| let mut result = String::with_capacity(length); | |
| for fragment in it { | |
| result.push_str(fragment); | |
| } | |
| result | |
| } | |
| } |