#[must_use]
pub fn uppercase_first_letter(s: &str) -> String {
let mut c = s.chars();
c.next().map_or_else(String::new, |f| {
f.to_uppercase().collect::<String>() + c.as_str()
})
}
pub fn split_pascal_case(target: &str) -> Result<Vec<&str>, Box<dyn std::error::Error>> {
let re = fancy_regex::Regex::new(r"[A-Z]+(?=[A-Z][a-z]|$)|[A-Z][a-z0-9]*")?;
let mut parts = Vec::new();
for mat in re.find_iter(target) {
parts.push(mat?.into());
}
if parts.is_empty() || parts.join("") != target {
return Err("Invalid PascalCase string".into());
}
Ok(parts)
}
#[must_use]
pub fn unwrap_option(ty: &syn::Type) -> Option<&syn::Type> {
if let syn::Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == "Option"
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
&& let Some(syn::GenericArgument::Type(inner)) = args.args.first()
{
return Some(inner);
}
None
}
#[must_use]
pub fn respanned(
tokens: &impl quote::ToTokens,
span: proc_macro2::Span,
) -> proc_macro2::TokenStream {
fn respan(
stream: proc_macro2::TokenStream,
span: proc_macro2::Span,
) -> proc_macro2::TokenStream {
stream
.into_iter()
.map(|mut tree| {
if let proc_macro2::TokenTree::Group(group) = &tree {
let mut respanned_group =
proc_macro2::Group::new(group.delimiter(), respan(group.stream(), span));
respanned_group.set_span(span);
tree = proc_macro2::TokenTree::Group(respanned_group);
} else {
tree.set_span(span);
}
tree
})
.collect()
}
respan(tokens.to_token_stream(), span)
}
#[cfg(test)]
mod tests {
use super::{respanned, split_pascal_case, unwrap_option, uppercase_first_letter};
use pretty_assertions::assert_eq;
use rstest::rstest;
fn ty(source: &str) -> syn::Type {
syn::parse_str::<syn::Type>(source).unwrap()
}
#[rstest]
#[case("aVeryLongString", "AVeryLongString")]
#[case("AVeryLongString", "AVeryLongString")]
#[case("x", "X")]
#[case("1abc", "1abc")]
#[case("", "")]
fn uppercase_first_letter_only_touches_the_first_character(
#[case] input: &str,
#[case] expected: &str,
) {
assert_eq!(uppercase_first_letter(input), expected);
}
#[rstest]
#[case("AVeryLongString", vec!["A", "Very", "Long", "String"])]
#[case("UserModel", vec!["User", "Model"])]
#[case("Model", vec!["Model"])]
#[case("HTTPServer", vec!["HTTP", "Server"])]
#[case("Order2Model", vec!["Order2", "Model"])]
fn split_pascal_case_splits_on_segment_boundaries(
#[case] input: &str,
#[case] expected: Vec<&str>,
) {
assert_eq!(split_pascal_case(input).unwrap(), expected);
}
#[rstest]
#[case("aVeryLongString")]
#[case("a_very_long_string")]
#[case("User_Model")]
#[case("")]
fn split_pascal_case_rejects_non_pascal_case(#[case] input: &str) {
assert!(split_pascal_case(input).is_err());
}
#[test]
fn unwrap_option_peels_exactly_one_layer() {
assert_eq!(unwrap_option(&ty("Option<u32>")), Some(&ty("u32")));
assert_eq!(
unwrap_option(&ty("Option<Option<u32>>")),
Some(&ty("Option<u32>"))
);
}
#[rstest]
#[case("u32")]
#[case("Vec<u32>")]
#[case("Option")]
#[case("&str")]
fn unwrap_option_returns_none_for_non_options(#[case] source: &str) {
assert_eq!(unwrap_option(&ty(source)), None);
}
#[test]
fn respanned_keeps_the_tokens() {
let tokens: syn::Type = syn::parse_quote!(::core::option::Option<(u8, [u16; 2])>);
assert_eq!(
respanned(&tokens, proc_macro2::Span::call_site()).to_string(),
quote::ToTokens::to_token_stream(&tokens).to_string()
);
}
#[test]
fn doc_example_uppercase_first_letter() {
use crate::utils::uppercase_first_letter;
use pretty_assertions::assert_eq;
let uppercase_1 = uppercase_first_letter("aVeryLongString");
let uppercase_2 = uppercase_first_letter("AVeryLongString");
assert_eq!(uppercase_2, uppercase_1);
}
#[test]
fn doc_example_split_pascal_case() {
use crate::utils::split_pascal_case;
use pretty_assertions::assert_eq;
let segments = split_pascal_case("AVeryLongString").unwrap();
assert_eq!(vec!["A", "Very", "Long", "String"], segments);
let segments_result = split_pascal_case("aVeryLongString");
assert!(segments_result.is_err());
let segments_result = split_pascal_case("a_very_long_string");
assert!(segments_result.is_err());
}
#[test]
fn doc_example_unwrap_option() {
use crate::utils::unwrap_option;
use pretty_assertions::assert_eq;
use syn::{Type, parse_str};
let ty = parse_str::<Type>("Option<u32>").unwrap();
let inner_ty_option = unwrap_option(&ty);
let expected_inner_ty = parse_str::<Type>("u32").unwrap();
let expected_inner_ty_option = Some(&expected_inner_ty);
assert_eq!(expected_inner_ty_option, inner_ty_option);
}
#[test]
fn doc_example_respanned() {
use crate::utils::respanned;
let path: syn::Path = syn::parse_quote!(::queryweave::RawValue);
let span = proc_macro2::Span::call_site();
assert_eq!(
respanned(&path, span).to_string(),
":: queryweave :: RawValue"
);
}
}