use std::{collections::HashSet, str::FromStr};
use quote::{quote, ToTokens};
use crate::Backend;
pub type Result<T = (), E = proc_macro2::TokenStream> = std::result::Result<T, E>;
fn parse_button_declaration(input: &str) -> Button {
let regex = regex::Regex::new(r#"^\s*\"(.*)\"\s*:\s*\"(.*)\"\s*$"#).unwrap();
let mut captures = regex.captures_iter(input);
let captured: (_, [&str; 2]) = captures.next().expect("Parse the button.").extract();
Button {
title: captured
.1
.first()
.expect("Parse the button title.")
.to_string(),
method_name: captured
.1
.get(1)
.expect("Parse the button method name.")
.to_string(),
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Button {
pub title: String,
pub method_name: String,
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Attribute {
Skip,
ReadOnly,
Rename(String),
Format(String),
Tooltip(String),
Documentation(String),
Button(Button),
Backend(Backend),
}
impl FromStr for Attribute {
type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> {
if input.contains('=') {
let (attribute, value) = {
let mut split = input.split('=');
let attribute = split.next().unwrap().trim().to_lowercase();
let value = split
.next()
.unwrap_or_else(|| panic!("A value for {attribute} attribute is required."))
.trim()
.replace('"', "")
.to_owned();
(attribute, value)
};
Ok(match attribute.as_ref() {
"rename" => Self::Rename(value),
"format" => Self::Format(value),
"tooltip" => Self::Tooltip(value),
"backend" => Self::Backend(Backend::from_str(&value)?),
a => return Err(a.to_owned()),
})
} else if input.contains('(') {
let (attribute, value) = {
let mut split = input.split('(');
let attribute = split.next().unwrap().trim().to_lowercase();
let value = split
.next()
.unwrap_or_else(|| panic!("A value for {attribute} attribute is required."))
.trim()
.replace([')'], "")
.to_owned();
(attribute, value)
};
Ok(match attribute.as_ref() {
"button" => {
let button = parse_button_declaration(&value);
Self::Button(button)
}
a => return Err(a.to_owned()),
})
} else {
Ok(match input.trim().to_lowercase().as_ref() {
"skip" => Self::Skip,
"readonly" => Self::ReadOnly,
a => return Err(a.to_owned()),
})
}
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Attributes {
attributes: HashSet<Attribute>,
}
impl From<HashSet<Attribute>> for Attributes {
fn from(attributes: HashSet<Attribute>) -> Self {
Self { attributes }
}
}
impl std::ops::Deref for Attributes {
type Target = HashSet<Attribute>;
fn deref(&self) -> &Self::Target {
&self.attributes
}
}
impl std::ops::DerefMut for Attributes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.attributes
}
}
pub trait AttributeHasDocumentation {
fn has_documentation(&self) -> bool;
fn get_documentation_name_value(&self) -> Option<&syn::MetaNameValue>;
fn get_documentation(&self) -> Option<&syn::Expr>;
fn get_documentation_string(&self) -> Option<String>;
}
pub trait AttributeHasMetaList {
fn get_meta_list(&self) -> Option<&syn::MetaList>;
}
impl AttributeHasDocumentation for syn::Attribute {
fn has_documentation(&self) -> bool {
self.get_documentation_name_value().is_some()
}
fn get_documentation_name_value(&self) -> Option<&syn::MetaNameValue> {
match &self.meta {
syn::Meta::NameValue(name_value) => {
if name_value.path.to_token_stream().to_string() == "doc" {
Some(name_value)
} else {
None
}
}
_ => None,
}
}
fn get_documentation(&self) -> Option<&syn::Expr> {
self.get_documentation_name_value()
.map(|name_value| &name_value.value)
}
fn get_documentation_string(&self) -> Option<String> {
self.get_documentation().and_then(|expr| match expr {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(str) => Some(str.value()),
_ => None,
},
_ => None,
})
}
}
impl AttributeHasMetaList for syn::Attribute {
fn get_meta_list(&self) -> Option<&syn::MetaList> {
match &self.meta {
syn::Meta::List(list) => Some(list),
_ => None,
}
}
}
impl Attributes {
pub fn new(attributes: &[String]) -> Result<Self> {
let mut unknown_attributes = Vec::new();
let attributes = attributes
.iter()
.filter_map(|a| {
if let Ok(a) = Attribute::from_str(a) {
Some(a)
} else {
unknown_attributes.push(a);
None
}
})
.collect();
if unknown_attributes.is_empty() {
Ok(Self { attributes })
} else {
let attrs = unknown_attributes
.into_iter()
.map(|s| format!("#[imgui_presentation]: unknown attributes: {s}"))
.collect::<Vec<String>>()
.join("\n");
Err(quote! {
compile_error!(#attrs);
})
}
}
pub fn parse(attribute: &syn::Attribute) -> Result<Self> {
let docs = attribute.get_documentation_string().map(|value| {
let mut attributes = HashSet::new();
attributes.insert(Attribute::Documentation(value.to_owned()));
Self { attributes }
});
if let Some(docs) = docs {
return Ok(docs);
}
let list = attribute.get_meta_list().unwrap();
let our_attribute = list.path.to_token_stream().to_string();
if our_attribute != "imgui_presentation" {
return Ok(Self::default());
}
let strings: Vec<String> = list
.tokens
.to_token_stream()
.to_string()
.split(',')
.map(|s| s.to_owned())
.collect();
Self::new(&strings)
}
pub fn parse_many(attributes: &[syn::Attribute]) -> Result<Self> {
let attributes: HashSet<Attribute> = attributes
.iter()
.map(Self::parse)
.collect::<Result<Vec<Attributes>>>()?
.into_iter()
.flat_map(|a| a.attributes)
.collect();
Ok(attributes.into())
}
pub fn parse_from_field(field: &syn::Field) -> Result<Self> {
Self::parse_many(&field.attrs)
}
pub fn has_skip(&self) -> bool {
self.attributes.contains(&Attribute::Skip)
}
pub fn has_readonly(&self) -> bool {
self.attributes.contains(&Attribute::ReadOnly)
}
pub fn get_rename(&self) -> Option<&str> {
self.attributes.iter().find_map(|a| {
if let Attribute::Rename(s) = a {
Some(s.as_ref())
} else {
None
}
})
}
#[allow(dead_code)]
pub fn get_format(&self) -> Option<&str> {
self.attributes.iter().find_map(|a| {
if let Attribute::Format(s) = a {
Some(s.as_ref())
} else {
None
}
})
}
pub fn get_documentation(&self) -> Option<String> {
let strings = self
.attributes
.iter()
.filter_map(|a| {
if let Attribute::Documentation(s) = a {
Some(s.trim())
} else {
None
}
})
.collect::<Vec<&str>>();
if strings.is_empty() {
None
} else {
Some(strings.join(""))
}
}
pub fn get_tooltip(&self) -> Option<&str> {
self.attributes.iter().find_map(|a| {
if let Attribute::Tooltip(s) = a {
Some(s.as_ref())
} else {
None
}
})
}
pub fn get_buttons(&self) -> Vec<&Button> {
self.attributes
.iter()
.filter_map(|a| {
if let Attribute::Button(s) = a {
Some(s)
} else {
None
}
})
.collect()
}
pub fn get_backends(&self) -> Vec<&Backend> {
self.attributes
.iter()
.filter_map(|a| {
if let Attribute::Backend(b) = a {
Some(b)
} else {
None
}
})
.collect()
}
pub fn get_tooltip_or_documentation(&self) -> Option<String> {
self.get_tooltip()
.map(|s| s.to_owned())
.or(self.get_documentation())
}
}