| // Copyright 2023 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| use std::{collections::HashMap, str::FromStr}; |
| |
| use nom::{ |
| branch::alt, |
| bytes::complete::{is_a, is_not, tag}, |
| character::complete::{alpha1, alphanumeric1}, |
| combinator::{map, recognize}, |
| multi::{many0_count, many1}, |
| sequence::{delimited, pair}, |
| IResult, |
| }; |
| use once_cell::sync::Lazy; |
| use regex::Regex; |
| |
| use crate::{Error, Result}; |
| |
| #[derive(Debug)] |
| enum StringFragment { |
| Literal(String), |
| Variable(String), |
| OpenBrace, |
| CloseBrace, |
| } |
| |
| #[derive(Debug)] |
| pub struct StringSub { |
| fragments: Vec<StringFragment>, |
| } |
| |
| static VARIABLE_NAME_REGEX: Lazy<Regex> = |
| Lazy::new(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_-]*$").expect("regex is valid")); |
| |
| /// Parser for a `StringSub` variable name. Starts with either a letter or |
| /// underscore, and can contain alphanumeric characters, underscores, or hyphens. |
| fn identifier(input: &str) -> IResult<&str, &str> { |
| recognize(pair( |
| alt((alpha1, tag("_"))), |
| many0_count(alt((alphanumeric1, is_a("_-")))), |
| ))(input) |
| } |
| |
| /// Parser for a `StringSub` variable placeholder, which is an identifier |
| /// enclosed in braces. |
| fn variable_sub(input: &str) -> IResult<&str, StringFragment> { |
| map(delimited(tag("{"), identifier, tag("}")), |s: &str| { |
| StringFragment::Variable(s.to_owned()) |
| })(input) |
| } |
| |
| /// Parser for a `StringSub` literal string, containing any characters other |
| /// than braces. |
| fn string_literal(input: &str) -> IResult<&str, StringFragment> { |
| map(is_not("{}"), |s: &str| { |
| StringFragment::Literal(s.to_owned()) |
| })(input) |
| } |
| |
| fn escaped_open_brace(input: &str) -> IResult<&str, StringFragment> { |
| map(tag("{{"), |_| StringFragment::OpenBrace)(input) |
| } |
| |
| fn escaped_close_brace(input: &str) -> IResult<&str, StringFragment> { |
| map(tag("}}"), |_| StringFragment::CloseBrace)(input) |
| } |
| |
| impl StringSub { |
| pub fn valid_variable_name(var: &str) -> bool { |
| VARIABLE_NAME_REGEX.is_match(var) |
| } |
| |
| pub fn new(string: &str) -> Result<Self> { |
| if string.is_empty() { |
| return Ok(Self { |
| fragments: Vec::new(), |
| }); |
| } |
| |
| let mut parser = many1(alt(( |
| escaped_open_brace, |
| escaped_close_brace, |
| variable_sub, |
| string_literal, |
| ))); |
| |
| // TODO(frolv): Map the nom error to a useful user-facing error. |
| let (remainder, fragments) = parser(string).map_err(|_| Error::GenericErrorPlaceholder)?; |
| if !remainder.is_empty() { |
| // TODO(frolv): Some of the string wasn't parsed. |
| return Err(Error::GenericErrorPlaceholder); |
| } |
| |
| Ok(Self { fragments }) |
| } |
| |
| /// Returns an iterator over the variable names in the string. |
| pub fn vars(&self) -> impl Iterator<Item = &str> { |
| self.fragments.iter().filter_map(|f| match f { |
| StringFragment::Variable(s) => Some(s.as_str()), |
| _ => None, |
| }) |
| } |
| |
| pub fn substitute<'a>(&self, vars: &HashMap<&'a str, &'a str>) -> Result<String> { |
| let mut s = String::new(); |
| |
| for frag in &self.fragments { |
| match frag { |
| StringFragment::Literal(lit) => s.push_str(lit), |
| StringFragment::Variable(var) => { |
| let Some(&value) = vars.get(var.as_str()) else { |
| // TODO(frolv): no value provided for `var`. |
| return Err(Error::GenericErrorPlaceholder); |
| }; |
| s.push_str(value); |
| } |
| StringFragment::OpenBrace => s.push('{'), |
| StringFragment::CloseBrace => s.push('}'), |
| } |
| } |
| |
| Ok(s) |
| } |
| } |
| |
| impl FromStr for StringSub { |
| type Err = Error; |
| |
| fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { |
| StringSub::new(s) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn substitute_one_var() { |
| let string = StringSub::new("hello, {foo}!").unwrap(); |
| assert_eq!(string.vars().count(), 1); |
| let vars = HashMap::from([("foo", "world")]); |
| assert_eq!(string.substitute(&vars).unwrap(), "hello, world!"); |
| } |
| |
| #[test] |
| fn substitute_multiple_vars() { |
| let string = StringSub::new("{greeting}, {subject}!").unwrap(); |
| assert_eq!(string.vars().count(), 2); |
| let vars = HashMap::from([("greeting", "hello"), ("subject", "world")]); |
| assert_eq!(string.substitute(&vars).unwrap(), "hello, world!"); |
| } |
| |
| #[test] |
| fn substitute_repeated_var() { |
| let string = StringSub::new("b{v}{v}a").unwrap(); |
| assert_eq!(string.vars().count(), 2); |
| let vars = HashMap::from([("v", "an")]); |
| assert_eq!(string.substitute(&vars).unwrap(), "banana"); |
| } |
| |
| #[test] |
| fn substitute_full_string() { |
| let string = StringSub::new("{foo}").unwrap(); |
| assert_eq!(string.vars().count(), 1); |
| let vars = HashMap::from([("foo", "hello, world!")]); |
| assert_eq!(string.substitute(&vars).unwrap(), "hello, world!"); |
| } |
| |
| #[test] |
| fn substitute_no_vars() { |
| let string = StringSub::new("hello").unwrap(); |
| assert_eq!(string.vars().count(), 0); |
| let mut vars = HashMap::new(); |
| assert_eq!(string.substitute(&vars).unwrap(), "hello"); |
| vars.insert("foo", "world"); |
| assert_eq!(string.substitute(&vars).unwrap(), "hello"); |
| } |
| |
| #[test] |
| fn substitute_empty_string() { |
| let string = StringSub::new("").unwrap(); |
| assert_eq!(string.vars().count(), 0); |
| let mut vars = HashMap::new(); |
| assert_eq!(string.substitute(&vars).unwrap(), ""); |
| vars.insert("foo", "world"); |
| assert_eq!(string.substitute(&vars).unwrap(), ""); |
| } |
| |
| #[test] |
| fn substitute_escaped_braces() { |
| let string = StringSub::new("hello, {{foo}}").unwrap(); |
| assert_eq!(string.vars().count(), 0); |
| let mut vars = HashMap::new(); |
| assert_eq!(string.substitute(&vars).unwrap(), "hello, {foo}"); |
| vars.insert("foo", "world"); |
| assert_eq!(string.substitute(&vars).unwrap(), "hello, {foo}"); |
| |
| assert_eq!( |
| StringSub::new("{{").unwrap().substitute(&vars).unwrap(), |
| "{", |
| ); |
| assert_eq!( |
| StringSub::new("}}").unwrap().substitute(&vars).unwrap(), |
| "}", |
| ); |
| assert_eq!( |
| StringSub::new("{{}}").unwrap().substitute(&vars).unwrap(), |
| "{}", |
| ); |
| } |
| |
| #[test] |
| fn substitute_variable_names() { |
| assert!(StringSub::new("{f}").is_ok()); |
| assert!(StringSub::new("{foo}").is_ok()); |
| assert!(StringSub::new("{__foo__}").is_ok()); |
| assert!(StringSub::new("{foo123}").is_ok()); |
| assert!(StringSub::new("{FooBar}").is_ok()); |
| assert!(StringSub::new("{f-o-o-}").is_ok()); |
| assert!(StringSub::new("{123foo}").is_err()); |
| assert!(StringSub::new("{-foo}").is_err()); |
| assert!(StringSub::new("{foo#bar}").is_err()); |
| assert!(StringSub::new("{!%(@)}").is_err()); |
| } |
| |
| #[test] |
| fn substitute_invalid_format_string() { |
| assert!(StringSub::new("{").is_err()); |
| assert!(StringSub::new("}").is_err()); |
| assert!(StringSub::new("hello, {foo").is_err()); |
| assert!(StringSub::new("hello, {}").is_err()); |
| assert!(StringSub::new("this is a closing brace: }").is_err()); |
| assert!(StringSub::new("hello, {foo{").is_err()); |
| assert!(StringSub::new("foo{{bar}baz").is_err()); |
| assert!(StringSub::new("{greeting}, {subject").is_err()); |
| } |
| |
| #[test] |
| fn substitute_missing_variables() { |
| let string = StringSub::new("hello, {foo}!").unwrap(); |
| assert_eq!(string.vars().count(), 1); |
| let vars = HashMap::new(); |
| assert!(string.substitute(&vars).is_err()); |
| } |
| } |