lan-party-backend/macros/src/lib.rs

738 lines
19 KiB
Rust

use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use quote::{__private::TokenStream as TokenStream2, format_ident, quote};
use syn::{
parse::Parse, parse_str, token::Do, Attribute, DataEnum, DataStruct, Fields, Generics, Ident,
LifetimeDef, Lit, MetaNameValue, Path, PredicateType, Type, TypeParam,
};
enum ParsedAttribute {
Documentation(Documentation),
View(ViewAttribute),
Serde(SerdeAttribute),
None,
}
#[derive(Debug)]
enum Documentation {
Title(String),
Description(String),
None,
}
enum ViewAttribute {
Title(Ident),
None,
}
enum SerdeAttribute {
Untagged,
None,
}
impl Documentation {
fn parse(attr: &Attribute) -> Documentation {
if !attr.path.is_ident("doc") {
return Documentation::None;
}
let text = attr.tokens.to_string();
let text = text.trim_matches(|c: char| c == '\"' || c == '=' || c.is_whitespace());
match text.get(0..1) {
Some("#") => Documentation::Title(text.trim_matches('#').trim().into()),
Some(&_) => Documentation::Description(text.trim().into()),
None => Documentation::None,
}
}
}
impl ViewAttribute {
fn parse(attr: &Attribute) -> ViewAttribute {
if !attr.path.is_ident("web_view_attr") {
return Self::None;
}
let parsed: Result<MetaNameValue, _> = parse_str(
attr.tokens
.to_string()
.trim_matches(|c: char| c == '(' || c == ')'),
);
match parsed {
Ok(p) => match (p.path.get_ident().unwrap().to_string().as_str(), p.lit) {
("title", Lit::Str(ls)) => Self::Title(format_ident!("{}", ls.value())),
_ => Self::None,
},
Err(_) => Self::None,
}
}
}
impl SerdeAttribute {
fn parse(attr: &Attribute) -> SerdeAttribute {
if !attr.path.is_ident("serde") {
return Self::None;
}
let parsed: Result<Ident, _> = parse_str(
attr.tokens
.to_string()
.trim_matches(|c: char| c == '(' || c == ')'),
);
match parsed {
Ok(p) => match p.to_string().as_str() {
"untagged" => Self::Untagged,
_ => Self::None,
},
Err(_) => Self::None,
}
}
}
impl ParsedAttribute {
fn parse(attr: &Attribute) -> ParsedAttribute {
match attr.path.get_ident() {
Some(i) if i.to_string() == "doc" => Self::Documentation(Documentation::parse(attr)),
Some(i) if i.to_string() == "web_view_attr" => Self::View(ViewAttribute::parse(attr)),
Some(i) if i.to_string() == "serde" => Self::Serde(SerdeAttribute::parse(attr)),
_ => Self::None,
}
}
}
struct Attributes {
title: Option<String>,
title_field: Option<Ident>,
description: Option<String>,
untagged: bool,
}
impl Attributes {
pub fn parse(attrs: &[Attribute]) -> Self {
let parsed: Vec<_> = attrs.iter().map(ParsedAttribute::parse).collect();
let mut title = None;
let mut title_field = None;
let mut description: Option<String> = None;
let mut untagged = false;
for attr in parsed {
match attr {
ParsedAttribute::Documentation(doc) => match doc {
Documentation::Title(t) => title = Some(t),
Documentation::Description(d) => {
if description.is_some() {
description.as_mut().unwrap().push(' ');
} else {
let _ = description.insert(String::new());
}
description.as_mut().unwrap().push_str(&d);
}
_ => {}
},
ParsedAttribute::View(v) => match v {
ViewAttribute::Title(t) => title_field = Some(t),
_ => {}
},
ParsedAttribute::Serde(s) => match s {
SerdeAttribute::Untagged => untagged = true,
_ => {}
},
_ => {}
}
}
Self {
title,
description,
title_field,
untagged,
}
}
}
#[proc_macro_attribute]
pub fn web_view_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
struct ItemProps {
name: Ident,
attributes: Attributes,
generics: Generics,
}
struct StructField {
name: Ident,
name_str: String,
title: Option<String>,
description: Option<String>,
}
struct EnumVariant {
variant: Ident,
variant_lower: Ident,
inner: Type,
title: Option<String>,
description: Option<String>,
}
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
let ItemProps {
name,
attributes: Attributes {
title, description, ..
},
generics,
} = props;
let title = title.clone().unwrap_or(name.to_string());
let fields = s.fields.iter().map(|f| {
let name = f
.ident
.as_ref()
.expect("each struct field must be named")
.clone();
let name_str = name.to_string();
let Attributes {
title,
title_field: _,
description,
..
} = Attributes::parse(&f.attrs);
StructField {
name_str,
name,
title,
description,
}
});
let fields_view = fields.clone().map(|f| {
let title = f.title.unwrap_or(f.name_str.to_case(Case::Title));
let description = if let Some(d) = f.description {
quote! {
span(class="description") {
" " #d
}
}
} else {
quote! {}
};
let name = f.name;
quote! {
p {
label { (#title) br() #description }
(#name.edit(cx))
}
}
});
let signals = fields.clone().map(|f| {
let name = f.name;
quote! {
let #name = create_signal(cx, state.get_untracked().#name.clone());
}
});
let effect_fields = fields.clone().map(|f| {
let name = f.name;
quote! {
#name: #name.get().as_ref().clone()
}
});
quote! {
let state = props.state;
#(#signals)*
create_effect(cx, || {
state.set(#name {
#(#effect_fields,)*
..Default::default()
});
});
view! { cx,
Block(title=#title.to_string()) {
p(class="description") {
#description
}
#(#fields_view)*
}
}
}
}
fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
let ItemProps {
name,
attributes: Attributes {
title, description, ..
},
generics,
} = props;
let title = title.clone().unwrap_or(name.to_string());
let variants = e.variants.iter().map(|v| {
let variant = v.ident.clone();
let inner = match &v.fields {
Fields::Unnamed(u) => u.unnamed.first().expect("the should be a field").ty.clone(),
_ => unimplemented!(),
};
let Attributes {
title,
title_field: _,
description,
..
} = Attributes::parse(&v.attrs);
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
EnumVariant {
variant_lower,
variant,
inner,
title,
description,
}
});
let first = variants.clone().next().unwrap().variant_lower;
let first_str = first.to_string();
let options = variants.clone().map(|v| {
let lower = v.variant_lower;
let title = v
.title
.unwrap_or(v.variant.to_string().to_case(Case::Title));
let selected = first == lower;
quote! { option(value={stringify!(#lower)}, selected=#selected) { (#title) } }
});
let view_match = variants.clone().map(|v| {
let lower = v.variant_lower;
let lower_str = format!("{}", lower);
quote! {
#lower_str => #lower.edit(cx)
}
});
let view_description = variants.clone().map(|v| {
let lower = v.variant_lower;
let lower_str = format!("{}", lower);
let description = if let Some(d) = v.description {
quote! {
span(class="description") {
" " #d
}
}
} else {
quote! {}
};
quote! {
#lower_str => view! { cx, #description }
}
});
let signals = variants.clone().map(
|EnumVariant {
variant,
variant_lower,
inner,
..
}| {
quote! {
let #variant_lower = if let #name::#variant(v) = state.get_untracked().as_ref().clone() {
create_signal(cx, v.clone())
} else {
create_signal(cx, <#inner>::default())
};
}
},
);
let effect_match = variants.clone().map(|v| {
let lower = v.variant_lower;
let variant = v.variant;
let lower_str = format!("{}", lower);
quote! {
#lower_str => state.set(#name::#variant(#lower.get().as_ref().clone()))
}
});
quote! {
let state = props.state;
let selected = create_signal(cx, String::from(#first_str));
#(#signals)*
create_effect(cx, || {
match selected.get().as_str() {
#(#effect_match,)*
_ => {}
}
});
view! { cx,
Block(title=#title.to_string()) {
p(class="description") {
#description
}
select(bind:value=selected) {
#(#options)*
}
(match selected.get().as_str() {
#(#view_description,)*
_ => view! { cx, }
})
br()
(match selected.get().as_str() {
#(#view_match,)*
_ => view! { cx, }
})
}
}
}
}
fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
let ItemProps {
name,
attributes:
Attributes {
title,
description,
title_field,
..
},
generics,
} = props;
let title = title.clone().unwrap_or(name.to_string());
let fields = s.fields.iter().map(|f| {
let name = f
.ident
.as_ref()
.expect("each struct field must be named")
.clone();
let name_str = name.to_string();
let Attributes {
title,
title_field: _,
description,
..
} = Attributes::parse(&f.attrs);
StructField {
name_str,
name,
title,
description,
}
});
let refs = fields.clone().map(|f| {
let name = f.name;
quote! {
let #name = create_ref(cx, state.#name.clone());
}
});
let fields_view = fields.clone().map(|f| {
let title = f.title.unwrap_or(f.name_str.to_case(Case::Title));
let description = if let Some(d) = f.description {
quote! {
span(class="description") {
" " #d
}
}
} else {
quote! {}
};
let name = f.name;
quote! {
p {
label { (#title) br() #description }
(#name.view(cx))
}
}
});
let title = if let Some(title_field) = title_field {
quote! {
state.#title_field
}
} else {
quote! {
#title
}
};
quote! {
let state = props.state;
#(#refs)*
view! { cx,
Block(title=#title.to_string()) {
p(class="description") {
#description
}
#(#fields_view)*
}
}
}
}
fn enum_fields<'a>(e: &'a DataEnum) -> impl Iterator<Item = EnumVariant> + 'a {
e.variants.iter().map(|v| {
let variant = v.ident.clone();
let inner = match &v.fields {
Fields::Unnamed(u) => u.unnamed.first().expect("the should be a field").ty.clone(),
_ => unimplemented!(),
};
let Attributes {
title,
title_field: _,
description,
..
} = Attributes::parse(&v.attrs);
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
EnumVariant {
variant_lower,
variant,
inner,
title,
description,
}
})
}
fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
let ItemProps {
name,
attributes: Attributes {
title, description, ..
},
generics,
} = props;
let title = title.clone().unwrap_or(name.to_string());
let variants = e.variants.iter().map(|v| {
let variant = v.ident.clone();
let inner = match &v.fields {
Fields::Unnamed(u) => u.unnamed.first().expect("the should be a field").ty.clone(),
_ => unimplemented!(),
};
let Attributes {
title,
title_field: _,
description,
..
} = Attributes::parse(&v.attrs);
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
EnumVariant {
variant_lower,
variant,
inner,
title,
description,
}
});
let view_match = variants.clone().map(|v| {
let variant = v.variant;
quote! {
#name::#variant(x) => x.view(cx)
}
});
let view_description = variants.clone().map(|v| {
let variant = v.variant;
let description = if let Some(d) = v.description {
quote! {
span(class="description") {
" " #d
}
}
} else {
quote! {}
};
quote! {
#name::#variant(_) => view! { cx, #description }
}
});
quote! {
let state = props.state;
view! { cx,
Block(title=#title.to_string()) {
p(class="description") {
#description
}
(match state {
#(#view_description,)*
_ => view! { cx, }
})
(match state {
#(#view_match,)*
_ => view! { cx, }
})
}
}
}
}
#[proc_macro_derive(WebEdit)]
pub fn web_edit(tokens: TokenStream) -> TokenStream {
let input: syn::DeriveInput = syn::parse(tokens).unwrap();
let name = input.ident;
let edit_ident = format_ident!("{}Edit", name);
let attrs = Attributes::parse(&input.attrs);
let props = ItemProps {
name: name.clone(),
attributes: attrs,
generics: input.generics.clone(),
};
let inner = match input.data {
syn::Data::Struct(s) => struct_edit(&props, s),
syn::Data::Enum(e) => enum_edit(&props, e),
_ => unimplemented!(),
};
let mut generics = input.generics.clone();
let input_generics = input.generics;
//if generics.type_params().count() == 0 {
generics
.params
.push(syn::GenericParam::Lifetime(syn::parse_str("'a").unwrap()));
generics
.params
.push(syn::GenericParam::Type(syn::parse_str("G: Html").unwrap()));
//}
let (input_impl_generics, input_ty_generics, input_where_clause) =
input_generics.split_for_impl();
for ty_param in input_generics.type_params() {
generics.make_where_clause().predicates.push(
syn::parse_str(&format!(
"{}: for<'b> Editable<'b, G>",
ty_param.ident.to_string()
))
.unwrap(),
);
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let res = quote! {
pub struct #edit_ident;
impl #impl_generics Editor<'a, G, #name #input_ty_generics> for #edit_ident #where_clause {
fn edit(cx: Scope<'a>, props: EditProps<'a, #name #input_ty_generics>) -> View<G> {
#inner
}
}
impl #impl_generics Editable<'a, G> for #name #input_ty_generics #where_clause {
type Editor = #edit_ident;
}
};
println!("{}", &res.to_string());
TokenStream::from(res)
}
#[proc_macro_derive(WebView)]
pub fn web_view(tokens: TokenStream) -> TokenStream {
let input: syn::DeriveInput = syn::parse(tokens).unwrap();
let name = input.ident;
let view_ident = format_ident!("{}View", name);
let attrs = Attributes::parse(&input.attrs);
let props = ItemProps {
name: name.clone(),
attributes: attrs,
generics: input.generics.clone(),
};
let inner = match input.data {
syn::Data::Struct(s) => struct_view(&props, s),
syn::Data::Enum(e) => enum_view(&props, e),
_ => unimplemented!(),
};
let mut generics = input.generics.clone();
let input_generics = input.generics;
//if generics.type_params().count() == 0 {
generics
.params
.push(syn::GenericParam::Lifetime(syn::parse_str("'a").unwrap()));
generics
.params
.push(syn::GenericParam::Type(syn::parse_str("G: Html").unwrap()));
//}
let (input_impl_generics, input_ty_generics, input_where_clause) =
input_generics.split_for_impl();
for ty_param in input_generics.type_params() {
generics.make_where_clause().predicates.push(
syn::parse_str(&format!(
"{}: for<'b> Viewable<'b, G>",
ty_param.ident.to_string()
))
.unwrap(),
);
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
//println!("{}", &inner.to_string());
let res = quote! {
pub struct #view_ident;
impl #impl_generics Viewer<'a, G, #name #input_ty_generics> for #view_ident #where_clause {
fn view(cx: Scope<'a>, props: ViewProps<'a, #name #input_ty_generics>) -> View<G> {
#inner
}
}
impl #impl_generics Viewable<'a, G> for #name #input_ty_generics #where_clause {
type Viewer = #view_ident;
}
};
println!("{}", &res.to_string());
TokenStream::from(res)
}