use super::FormatArgs;
use proc_macro2::TokenStream;
use quote::quote;
use regex::{Captures, Regex};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicUsize, Ordering};
use syn::{
parse::{Error, Result},
spanned::Spanned,
};
pub struct Formatter {
args: FormatArgs,
next_argument: AtomicUsize,
}
impl Formatter {
pub fn new(args: FormatArgs) -> Self {
Self {
args,
next_argument: AtomicUsize::new(0),
}
}
fn next_argument(&self) -> usize {
self.next_argument.fetch_add(1, Ordering::Relaxed)
}
pub fn quote_format_str(&self, input: &str) -> Result<TokenStream> {
lazy_static::lazy_static! {
static ref FORMAT_RE: Regex = Regex::new(r"\{(?P<arg>[^:]*?)(?P<spec>:.*?)?\}").unwrap();
}
let mut positions = HashSet::new();
let mut names = HashSet::new();
let input = FORMAT_RE.replace_all(input, |caps: &Captures| {
let arg = caps["arg"].trim();
if arg.is_empty() {
let pos = self.next_argument();
positions.insert(pos);
if let Some(spec) = caps.name("spec") {
format!("{{{}{}}}", pos, spec.as_str())
} else {
format!("{{{}}}", pos)
}
} else if let Ok(pos) = arg.parse::<usize>() {
positions.insert(pos);
caps.get(0).unwrap().as_str().to_string()
} else {
let name = arg.to_string();
names.insert(name);
caps.get(0).unwrap().as_str().to_string()
}
});
let mut pos_args = Vec::with_capacity(positions.len());
let index_and_arg_pos: Vec<(usize, usize)> = {
let mut tmp: Vec<usize> = positions.into_iter().collect();
tmp.sort_unstable();
tmp.into_iter().enumerate().collect()
};
for idx in index_and_arg_pos.iter().map(|(_, x)| x).copied() {
if let Some(expr) = self.args.positional_args.get(idx) {
pos_args.push(quote!(#expr));
} else {
return Err(Error::new(
self.args.format_string.span(),
format!(
"{{{}}} requested, but only {} positional arguments provided",
idx,
self.args.positional_args.len()
),
));
}
}
let mut name_args = Vec::with_capacity(names.len());
for name in names.iter() {
match self.args.named_args.iter().find(|(ident, _)| ident == name) {
Some((ident, expr)) => name_args.push(quote!(#ident = #expr)),
None => {
return Err(Error::new(
self.args.format_string.span(),
format!(
"{{{}}} requested, but no argumented named {} provided",
name, name
),
));
}
}
}
let arg_to_idx = index_and_arg_pos
.into_iter()
.map(|(a, b)| (b, a))
.collect::<HashMap<usize, usize>>();
let input = FORMAT_RE.replace_all(&input, |caps: &Captures| {
let arg = caps["arg"].trim();
if let Ok(pos) = arg.parse::<usize>() {
let idx = *arg_to_idx.get(&pos).unwrap();
if let Some(spec) = caps.name("spec") {
format!("{{{}{}}}", idx, spec.as_str())
} else {
format!("{{{}}}", idx)
}
} else {
caps.get(0).unwrap().as_str().to_string()
}
});
Ok(quote!(
::std::format!(#input #(,#pos_args)* #(,#name_args)*)
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_str;
#[test]
fn quote_format_str_should_support_implicit_positional_arguments() {
let args: FormatArgs = parse_str(r#"" ", "cool", 123"#).unwrap();
let formatter = Formatter::new(args);
let stream = formatter.quote_format_str("{} {}").unwrap();
assert_eq!(
stream.to_string(),
quote!(::std::format!("{0} {1}", "cool", 123)).to_string()
);
}
#[test]
fn quote_format_str_should_support_explicit_positional_arguments() {
let args: FormatArgs = parse_str(r#"" ", "cool", 123"#).unwrap();
let formatter = Formatter::new(args);
let stream = formatter.quote_format_str("{1} {0}").unwrap();
assert_eq!(
stream.to_string(),
quote!(::std::format!("{1} {0}", "cool", 123)).to_string()
);
}
#[test]
fn quote_format_str_should_support_selectively_including_positional_arguments(
) {
let args: FormatArgs = parse_str(r#"" ", "cool", 123, 456"#).unwrap();
let formatter = Formatter::new(args);
let stream = formatter.quote_format_str("{1}").unwrap();
assert_eq!(
stream.to_string(),
quote!(::std::format!("{0}", 123)).to_string()
);
let stream = formatter.quote_format_str("{}").unwrap();
assert_eq!(
stream.to_string(),
quote!(::std::format!("{0}", "cool")).to_string()
);
let stream = formatter.quote_format_str("no format text").unwrap();
assert_eq!(
stream.to_string(),
quote!(::std::format!("no format text")).to_string()
);
}
#[test]
fn quote_format_str_should_support_named_arguments() {
let args: FormatArgs =
parse_str(r#"" ", a = "cool", b = 123"#).unwrap();
let formatter = Formatter::new(args);
let stream = formatter.quote_format_str("{a} {b}").unwrap();
let expected_1 = quote!(::std::format!("{a} {b}", a = "cool", b = 123));
let expected_2 = quote!(::std::format!("{a} {b}", b = 123, a = "cool"));
assert!(
stream.to_string() == expected_1.to_string()
|| stream.to_string() == expected_2.to_string(),
"{} did not match either variant of {}",
stream.to_string(),
expected_1.to_string(),
);
}
#[test]
fn quote_format_str_should_support_selectively_including_named_arguments() {
let args: FormatArgs =
parse_str(r#"" ", a = "cool", b = 123"#).unwrap();
let formatter = Formatter::new(args);
let stream = formatter.quote_format_str("{a}").unwrap();
assert_eq!(
stream.to_string(),
quote!(::std::format!("{a}", a = "cool")).to_string()
);
let stream = formatter.quote_format_str("no format text").unwrap();
assert_eq!(
stream.to_string(),
quote!(::std::format!("no format text")).to_string()
);
}
#[test]
fn quote_format_str_should_support_mixture_of_arguments() {
let args: FormatArgs =
parse_str(r#"" ", "fish", 456, a = "cool", b = 123"#).unwrap();
let formatter = Formatter::new(args);
let stream =
formatter.quote_format_str("{1} {} {b} {0} {a} {}").unwrap();
let expected_1 = quote!(::std::format!(
"{1} {0} {b} {0} {a} {1}",
"fish",
456,
a = "cool",
b = 123
));
let expected_2 = quote!(::std::format!(
"{1} {0} {b} {0} {a} {1}",
"fish",
456,
b = 123,
a = "cool"
));
assert!(
stream.to_string() == expected_1.to_string()
|| stream.to_string() == expected_2.to_string(),
"{} did not match either variant of {}",
stream.to_string(),
expected_1.to_string(),
);
}
}