use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Data, DeriveInput, Fields, Ident, LitBool, LitInt, LitStr, Token};
use crate::shared::named_field_ident;
#[derive(Default)]
pub(crate) struct ColumnsAttrs {
column_count: Option<LitInt>,
equal: Option<LitBool>,
expand: Option<LitBool>,
padding: Option<LitInt>,
title: Option<LitStr>,
}
pub(crate) struct ColumnsAttr {
pub(crate) key: Ident,
pub(crate) value: ColumnsAttrValue,
}
pub(crate) enum ColumnsAttrValue {
Str(LitStr),
Bool(LitBool),
Int(LitInt),
Flag,
}
impl Parse for ColumnsAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key: Ident = input.parse()?;
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
if input.peek(LitStr) {
let lit: LitStr = input.parse()?;
Ok(ColumnsAttr {
key,
value: ColumnsAttrValue::Str(lit),
})
} else if input.peek(LitBool) {
let lit: LitBool = input.parse()?;
Ok(ColumnsAttr {
key,
value: ColumnsAttrValue::Bool(lit),
})
} else if input.peek(LitInt) {
let lit: LitInt = input.parse()?;
Ok(ColumnsAttr {
key,
value: ColumnsAttrValue::Int(lit),
})
} else {
Err(input.error("expected string literal, bool, or integer"))
}
} else {
Ok(ColumnsAttr {
key,
value: ColumnsAttrValue::Flag,
})
}
}
}
pub(crate) fn parse_columns_attrs(input: &DeriveInput) -> syn::Result<ColumnsAttrs> {
let mut attrs = ColumnsAttrs::default();
for attr in &input.attrs {
if !attr.path().is_ident("columns") {
continue;
}
let items: Punctuated<ColumnsAttr, Token![,]> =
attr.parse_args_with(Punctuated::parse_terminated)?;
for item in items {
let key_str = item.key.to_string();
match key_str.as_str() {
"column_count" => {
attrs.column_count = Some(columns_expect_int(&item, "column_count")?);
}
"equal" => {
attrs.equal = Some(columns_expect_bool(&item, "equal")?);
}
"expand" => {
attrs.expand = Some(columns_expect_bool(&item, "expand")?);
}
"padding" => {
attrs.padding = Some(columns_expect_int(&item, "padding")?);
}
"title" => {
attrs.title = Some(columns_expect_str(&item, "title")?);
}
_ => {
return Err(syn::Error::new_spanned(
&item.key,
format!("unknown columns attribute `{}`", key_str),
));
}
}
}
}
Ok(attrs)
}
pub(crate) fn columns_expect_str(attr: &ColumnsAttr, name: &str) -> syn::Result<LitStr> {
match &attr.value {
ColumnsAttrValue::Str(s) => Ok(s.clone()),
_ => Err(syn::Error::new_spanned(
&attr.key,
format!("`{}` expects a string literal", name),
)),
}
}
pub(crate) fn columns_expect_bool(attr: &ColumnsAttr, _name: &str) -> syn::Result<LitBool> {
match &attr.value {
ColumnsAttrValue::Bool(b) => Ok(b.clone()),
ColumnsAttrValue::Flag => Ok(LitBool::new(true, attr.key.span())),
_ => Err(syn::Error::new_spanned(
&attr.key,
format!("`{}` expects a bool", _name),
)),
}
}
pub(crate) fn columns_expect_int(attr: &ColumnsAttr, name: &str) -> syn::Result<LitInt> {
match &attr.value {
ColumnsAttrValue::Int(i) => Ok(i.clone()),
_ => Err(syn::Error::new_spanned(
&attr.key,
format!("`{}` expects an integer literal", name),
)),
}
}
pub(crate) fn derive_columns_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let struct_name = &input.ident;
let struct_name_str = struct_name.to_string();
let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(named) => &named.named,
Fields::Unnamed(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"Columns derive only supports structs with named fields",
));
}
Fields::Unit => {
return Err(syn::Error::new_spanned(
struct_name,
"Columns derive does not support unit structs",
));
}
},
Data::Enum(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"Columns derive does not support enums",
));
}
Data::Union(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"Columns derive does not support unions",
));
}
};
let columns_attrs = parse_columns_attrs(input)?;
struct ColFieldInfo {
ident: Ident,
label: String,
style: Option<String>,
}
let mut field_infos: Vec<ColFieldInfo> = Vec::new();
for field in fields.iter() {
let ident = named_field_ident(field)?.clone();
let fa = crate::panel::parse_field_attrs(field)?;
let skip = fa.skip.as_ref().map(|b| b.value).unwrap_or(false);
if skip {
continue;
}
let label = match &fa.label {
Some(lit) => lit.value(),
None => crate::shared::snake_to_title_case(&ident.to_string()),
};
let style = fa.style.as_ref().map(|lit| lit.value());
field_infos.push(ColFieldInfo {
ident,
label,
style,
});
}
let line_pushes: Vec<proc_macro2::TokenStream> = field_infos
.iter()
.map(|fi| {
let ident = &fi.ident;
let label = &fi.label;
match &fi.style {
Some(sty) => {
let open_tag = format!("[{}]", sty);
let close_tag = format!("[/{}]", sty);
quote! {
lines.push(format!("{}{}:{} {}", #open_tag, #label, #close_tag, self.#ident));
}
}
None => {
quote! {
lines.push(format!("{}: {}", #label, self.#ident));
}
}
}
})
.collect();
let mut cols_config = Vec::new();
if let Some(ref lit) = columns_attrs.column_count {
let val: usize = lit.base10_parse()?;
cols_config.push(quote! {
cols.width = Some(max_width / #val);
});
}
if let Some(ref lit) = columns_attrs.equal {
let val = lit.value;
cols_config.push(quote! {
cols.equal = #val;
});
}
if let Some(ref lit) = columns_attrs.expand {
let val = lit.value;
cols_config.push(quote! {
cols.expand = #val;
});
}
if let Some(ref lit) = columns_attrs.padding {
let val: usize = lit.base10_parse()?;
cols_config.push(quote! {
cols.padding = (0, #val, 0, #val);
});
}
if let Some(ref lit) = columns_attrs.title {
let val = lit.value();
cols_config.push(quote! {
cols.title = Some(#val.to_string());
});
}
let card_title = struct_name_str;
let expanded = quote! {
impl #struct_name {
pub fn to_card(&self) -> gilt::panel::Panel {
let mut lines: Vec<String> = Vec::new();
#(#line_pushes)*
let content = gilt::text::Text::from_markup(&lines.join("\n"))
.unwrap_or_else(|_| gilt::text::Text::from(lines.join("\n").as_str()));
let mut panel = gilt::panel::Panel::new(content);
panel.title = Some(gilt::text::Text::from(#card_title));
panel
}
pub fn to_columns(items: &[Self]) -> gilt::columns::Columns {
let mut cols = gilt::columns::Columns::new();
#[allow(unused_variables)]
let max_width: usize = 80;
#(#cols_config)*
for item in items {
let card = item.to_card();
cols.add_renderable(&format!("{}", card));
}
cols
}
}
};
Ok(expanded)
}