use heck::{ToKebabCase, ToShoutySnakeCase};
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote};
use std::{borrow::Borrow, fmt::Display};
use syn::{
Error, Expr, ExprLit, GenericArgument, GenericParam, Generics, Lifetime, LifetimeParam, Lit,
LitChar, LitStr, Meta, Path, PathArguments, Token, Type, bracketed, meta::ParseNestedMeta,
parenthesized, parse::Parse, punctuated::Punctuated, spanned::Spanned,
};
pub fn type_is_bool(ty: &Type) -> bool {
match ty {
Type::Path(typepath) => typepath.qself.is_none() && typepath.path.is_ident("bool"),
_ => false,
}
}
fn type_is_given_generic(generic: &str, ty: &Type) -> Result<Option<Type>, syn::Error> {
fn path_is_given_generic(generic: &str, path: &Path) -> Result<Option<Type>, syn::Error> {
if path.leading_colon.is_none() && path.segments.len() == 1 {
let first = path.segments.first().unwrap();
if first.ident != generic {
return Ok(None);
}
return match &first.arguments {
PathArguments::AngleBracketed(generic_args) => {
if generic_args.args.len() != 1 {
return Err(Error::new(
generic_args.span(),
format!("Expected {generic}<T> for some type T"),
));
}
let arg = generic_args.args.first().unwrap();
match arg {
GenericArgument::Type(t) => Ok(Some(t.clone())),
_ => Err(Error::new(
generic_args.span(),
format!("Expected {generic}<T> for some type T"),
)),
}
}
_ => Err(Error::new(
first.arguments.span(),
format!("Expected {generic}<T> for some type T"),
)),
};
}
Ok(None)
}
match ty {
Type::Path(typepath) if typepath.qself.is_none() => {
path_is_given_generic(generic, &typepath.path)
}
_ => Ok(None),
}
}
pub fn type_is_option(ty: &Type) -> Result<Option<Type>, syn::Error> {
type_is_given_generic("Option", ty)
}
pub fn type_is_vec(ty: &Type) -> Result<Option<Type>, syn::Error> {
type_is_given_generic("Vec", ty)
}
pub fn type_is_signed_number(ty: &Type) -> bool {
match ty {
Type::Path(typepath) if typepath.qself.is_none() => {
let path = &typepath.path;
path.is_ident("i8")
|| path.is_ident("i16")
|| path.is_ident("i32")
|| path.is_ident("i64")
|| path.is_ident("i128")
|| path.is_ident("f32")
|| path.is_ident("f64")
}
_ => false,
}
}
pub fn type_is_pathbuf(ty: &Type) -> bool {
match ty {
Type::Path(typepath) if typepath.qself.is_none() => {
let path = &typepath.path;
path.is_ident("PathBuf")
}
_ => false,
}
}
pub fn type_is_osstring(ty: &Type) -> bool {
match ty {
Type::Path(typepath) if typepath.qself.is_none() => {
let path = &typepath.path;
path.is_ident("OsString")
}
_ => false,
}
}
pub fn parse_required_value<T: Parse>(meta: ParseNestedMeta<'_>) -> Result<T, Error> {
let t: T = meta.value()?.parse()?;
Ok(t)
}
pub fn parse_path_from_str(meta: ParseNestedMeta<'_>) -> Result<syn::Path, Error> {
let lit_str: syn::LitStr = parse_required_value(meta)?;
lit_str.parse()
}
pub fn parse_type_from_str(meta: ParseNestedMeta<'_>) -> Result<syn::Type, Error> {
let lit_str: syn::LitStr = parse_required_value(meta)?;
lit_str.parse()
}
pub fn parse_optional_value<T: Parse>(meta: ParseNestedMeta<'_>) -> Result<Option<T>, Error> {
if meta.input.is_empty() || meta.input.peek(Token![,]) {
Ok(None)
} else {
Ok(Some(parse_required_value::<T>(meta)?))
}
}
pub fn make_short(ident: &impl Display, span: Span) -> Option<LitChar> {
let string = ident.to_string();
if string.is_empty() {
return None;
}
let first = string.to_lowercase().chars().next().unwrap();
Some(LitChar::new(first, span))
}
pub fn make_long(ident: &impl Display, span: Span) -> Option<LitStr> {
let string = ident.to_string();
if string.is_empty() {
return None;
}
let kebab = string.to_kebab_case();
Some(LitStr::new(&kebab, span))
}
pub fn make_env(ident: &impl Display, span: Span) -> Option<LitStr> {
let string = ident.to_string();
if string.is_empty() {
return None;
}
let snake = string.to_shouty_snake_case();
Some(LitStr::new(&snake, span))
}
pub trait GetSpan {
fn get_span(&self) -> Span;
}
impl<T: Spanned> GetSpan for T {
fn get_span(&self) -> Span {
self.span()
}
}
pub fn set_once<T: GetSpan>(
context: &Path,
param: &mut Option<T>,
val: Option<T>,
) -> Result<(), Error> {
if let Some(param) = param.as_ref() {
let mut error = Error::new(
context.span(),
format!("{} cannot be specified twice", context.get_ident().unwrap()),
);
error.combine(Error::new(param.get_span(), "Earlier specified here"));
return Err(error);
}
*param = val;
Ok(())
}
pub fn mutually_exclusive_error<T1: GetSpan, T2: GetSpan>(
first_name: &str,
first: &T1,
second_name: &str,
second: &T2,
) -> Error {
let mut error = Error::new(
first.get_span(),
format!("{} and {} are mutually exclusive", first_name, second_name),
);
error.combine(Error::new(second.get_span(), "Conflicts with this"));
error
}
pub fn maybe_append_doc_string(
description: &mut Option<String>,
attr_meta: &Meta,
) -> Result<(), Error> {
let doc_expr = match attr_meta {
Meta::NameValue(name_value) if name_value.path.is_ident("doc") => &name_value.value,
_ => return Ok(()),
};
let lit = match doc_expr {
Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) => s.value(),
other => {
return Err(Error::new(
other.span(),
"Doc comment is not a string literal",
));
}
};
let mut trimmed = lit
.split('\n')
.map(|line| line.trim())
.fold(String::new(), |s, line| s + line + "\n");
trimmed.pop();
if let Some(desc) = description.as_mut() {
desc.push('\n');
desc.push_str(&trimmed);
} else {
*description = Some(trimmed);
}
Ok(())
}
pub fn quote_opt<T: ToTokens>(src: &Option<T>) -> TokenStream {
if let Some(string) = src.as_ref() {
quote! { Some(#string) }
} else {
quote! { None }
}
}
pub fn quote_opt_cow<T: ToTokens>(src: &Option<T>) -> TokenStream {
if let Some(string) = src.as_ref() {
quote! { Some(::std::borrow::Cow::Borrowed(#string)) }
} else {
quote! { None }
}
}
pub struct Array<T: Parse + ToTokens> {
pub elements: Punctuated<T, Token![,]>,
}
impl<T: Parse + ToTokens> Parse for Array<T> {
fn parse(input: syn::parse::ParseStream) -> Result<Self, syn::Error> {
let content;
bracketed!(content in input);
Ok(Self {
elements: Punctuated::parse_separated_nonempty(&content)?,
})
}
}
impl<T: Parse + ToTokens> GetSpan for Array<T> {
fn get_span(&self) -> Span {
self.elements.span()
}
}
impl<T: Parse + ToTokens> Array<T> {
pub fn quote_elements_cow(&self) -> TokenStream {
let elements = self.elements.iter();
quote! { #(::std::borrow::Cow::Borrowed(#elements)),* }
}
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
}
pub type LitStrArray = Array<LitStr>;
pub type LitCharArray = Array<LitChar>;
pub struct List<T: Parse + ToTokens> {
pub elements: Punctuated<T, Token![,]>,
}
impl<T: Parse + ToTokens> Parse for List<T> {
fn parse(input: syn::parse::ParseStream) -> Result<Self, syn::Error> {
let content;
parenthesized!(content in input);
Ok(Self {
elements: Punctuated::parse_separated_nonempty(&content)?,
})
}
}
impl<T: Parse + ToTokens> GetSpan for List<T> {
fn get_span(&self) -> Span {
self.elements.span()
}
}
pub fn make_lifetime(lt: impl AsRef<str>) -> Lifetime {
Lifetime::new(lt.as_ref(), Span::call_site())
}
pub fn prepend_generic_lifetimes<R: Borrow<Lifetime>>(
generics: &Generics,
lifetimes: impl AsRef<[R]>,
) -> Generics {
let mut generics = generics.clone();
for lt in lifetimes.as_ref().iter().rev() {
generics.params.insert(
0,
GenericParam::Lifetime(LifetimeParam::new(lt.borrow().clone())),
);
}
generics
}