extern crate proc_macro;
extern crate proc_macro2;
mod templating;
mod parsing;
use std::io::Read;
use std::fs;
use std::str::FromStr;
use std::collections::HashSet;
use std::fs::File;
use std::convert::TryFrom;
use std::path::Path;
use litrs::StringLit;
use proc_macro2::{Ident, Span, TokenStream};
use quote::__private::ext::RepToTokensExt;
use quote::{quote, ToTokens};
use templating::{ArgsPrimitive, ArgsFullComponent, ArgsStructOnlyComponent, TemplateArgsDerivePax, StaticPropertyDefinition};
use sailfish::TemplateOnce;
use syn::{parse_macro_input, Data, DeriveInput, Type, Field, Fields, PathArguments, GenericArgument, Attribute, Meta, NestedMeta, parse2, MetaList, Lit};
use syn::parse::{Parse, ParseStream};
fn pax_primitive(input_parsed: DeriveInput, primitive_instance_import_path: String, include_imports: bool, is_custom_interpolatable: bool,) -> proc_macro2::TokenStream {
let original_tokens = quote! { #input_parsed }.to_string();
let pascal_identifier = input_parsed.ident.to_string();
let static_property_definitions = get_static_property_definitions_from_tokens(input_parsed.data);
let output = TemplateArgsDerivePax {
args_primitive: Some(ArgsPrimitive {
primitive_instance_import_path,
}),
args_struct_only_component: None,
args_full_component: None,
static_property_definitions,
pascal_identifier,
include_imports,
is_custom_interpolatable,
}.render_once().unwrap().to_string();
TokenStream::from_str(&output).unwrap().into()
}
fn extract_custom_attr(attr: &MetaList) -> Option<Vec<String>> {
let custom_attr = attr
.nested
.iter()
.find(|nested_meta| match nested_meta {
NestedMeta::Meta(Meta::NameValue(name_value)) => {
name_value.path.is_ident("custom")
}
_ => false,
})?;
let custom_values = match custom_attr {
NestedMeta::Meta(Meta::NameValue(name_value)) => match &name_value.lit {
syn::Lit::Str(lit_str) => {
lit_str.value().split(',').map(|s| s.trim().to_string()).collect()
}
_ => return None,
},
_ => return None,
};
Some(custom_values)
}
fn pax_struct_only_component(input_parsed: DeriveInput, include_imports: bool, is_custom_interpolatable: bool) -> proc_macro2::TokenStream {
let pascal_identifier = input_parsed.ident.to_string();
let static_property_definitions = get_static_property_definitions_from_tokens(input_parsed.data);
let output = templating::TemplateArgsDerivePax{
args_full_component: None,
args_primitive: None,
args_struct_only_component: Some(ArgsStructOnlyComponent {}),
pascal_identifier: pascal_identifier.clone(),
static_property_definitions,
include_imports,
is_custom_interpolatable,
}.render_once().unwrap().to_string();
TokenStream::from_str(&output).unwrap().into()
}
fn get_field_type(f: &Field) -> Option<(Type, bool)> {
let mut ret = None;
match &f.ty {
Type::Path(tp) => {
match tp.qself {
None => {
tp.path.segments.iter().for_each(|ps| {
if ps.ident.to_string().ends_with("Property") {
match &ps.arguments {
PathArguments::AngleBracketed(abga) => {
abga.args.iter().for_each(|abgaa| {
match abgaa {
GenericArgument::Type(gat) => {
ret = Some((gat.to_owned(), true));
},
_ => {}
};
})
},
_ => {}
}
}
});
if ret.is_none() {
ret = Some((f.ty.to_owned(), false));
}
},
_ => {},
};
},
_ => {},
};
ret
}
fn get_scoped_resolvable_types(t: &Type) -> (Vec<String>, String) {
let mut accum: Vec<String> = vec![];
recurse_get_scoped_resolvable_types(t, &mut accum);
let root_scoped_resolvable_type = accum.get(accum.len() - 1).unwrap().clone();
(accum, root_scoped_resolvable_type)
}
fn recurse_get_scoped_resolvable_types(t: &Type, accum: &mut Vec<String>) {
match t {
Type::Path(tp) => {
match tp.qself {
None => {
let mut accumulated_scoped_resolvable_type = "".to_string();
tp.path.segments.iter().for_each(|ps| {
match &ps.arguments {
PathArguments::AngleBracketed(abga) => {
if accumulated_scoped_resolvable_type.ne("") {
accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + "::"
}
let ident = ps.ident.to_token_stream().to_string();
let turbofish_contents = ps.to_token_stream()
.to_string()
.replacen(&ident, "", 1)
.replace(" ", "");
accumulated_scoped_resolvable_type =
accumulated_scoped_resolvable_type.clone() +
&ident +
"::" +
&turbofish_contents;
abga.args.iter().for_each(|abgaa| {
match abgaa {
GenericArgument::Type(gat) => {
recurse_get_scoped_resolvable_types(gat, accum);
},
_ => { }
};
})
},
PathArguments::Parenthesized(_) => {unimplemented!("Parenthesized path arguments (for example, Fn types) not yet supported inside Pax `Property<...>`")},
PathArguments::None => {
if accumulated_scoped_resolvable_type.ne("") {
accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + "::"
}
accumulated_scoped_resolvable_type = accumulated_scoped_resolvable_type.clone() + &ps.to_token_stream().to_string();
}
_ => {}
}
});
accum.push(accumulated_scoped_resolvable_type);
},
_ => { unimplemented!("Self-types not yet supported with Pax `Property<...>`")}
}
},
Type::Tuple(t) => {
t.elems.iter().for_each(|tuple_elem| {
recurse_get_scoped_resolvable_types(tuple_elem, accum);
});
},
_ => {
unimplemented!("Unsupported Type::Path {}", t.to_token_stream().to_string());
}
}
}
fn get_static_property_definitions_from_tokens(data: Data) -> Vec<StaticPropertyDefinition> {
let ret = match data {
Data::Struct(ref data) => {
match data.fields {
Fields::Named(ref fields) => {
let mut ret = vec![];
fields.named.iter().for_each(|f| {
let field_name = f.ident.as_ref().unwrap();
let field_type = match get_field_type(f) {
None => { },
Some(ty) => {
let type_name = quote!(#(ty.0)).to_string().replace(" ", "");
let (scoped_resolvable_types, root_scoped_resolvable_type) = get_scoped_resolvable_types(&ty.0);
let pascal_identifier = type_name.split("::").last().unwrap().to_string();
ret.push(
StaticPropertyDefinition {
original_type: type_name,
field_name: quote!(#field_name).to_string(),
scoped_resolvable_types,
root_scoped_resolvable_type,
pascal_identifier,
is_property_wrapped: ty.1
}
)
}
};
});
ret
},
_ => {
unimplemented!("Pax may only be attached to `struct`s with named fields");
}
}
},
Data::Enum(ref data) => {
let mut ret = vec![];
data.variants.iter().for_each(|variant| {
let variant_name = &variant.ident;
variant.fields.iter().for_each(|f| {
if let Some(ty) = get_field_type(f) {
let original_type = quote!(#(ty.0)).to_string().replace(" ", "");
let (scoped_resolvable_types, root_scoped_resolvable_type) = get_scoped_resolvable_types(&ty.0);
let pascal_identifier = original_type.split("::").last().unwrap().to_string();
ret.push(
StaticPropertyDefinition {
original_type,
field_name: quote!(#variant_name).to_string(),
scoped_resolvable_types,
root_scoped_resolvable_type,
pascal_identifier,
is_property_wrapped: ty.1,
}
)
}
})
});
ret
}
_ => {unreachable!("Pax may only be attached to `struct`s")}
};
ret
}
fn pax_full_component(raw_pax: String, input_parsed: DeriveInput, is_main_component: bool, include_fix : Option<TokenStream>, include_imports: bool, is_custom_interpolatable: bool) -> proc_macro2::TokenStream {
let pascal_identifier = input_parsed.ident.to_string();
let static_property_definitions = get_static_property_definitions_from_tokens(input_parsed.data);
let template_dependencies = parsing::parse_pascal_identifiers_from_component_definition_string(&raw_pax);
let pax_dir: Option<&'static str> = option_env!("PAX_DIR");
let reexports_snippet = if let Some(pax_dir) = pax_dir {
let reexports_path = std::path::Path::new(pax_dir).join("reexports.partial.rs");
fs::read_to_string(&reexports_path).unwrap()
} else {
"".to_string()
};
let output = TemplateArgsDerivePax {
args_primitive: None,
args_struct_only_component: None,
args_full_component: Some(ArgsFullComponent {
is_main_component,
raw_pax,
template_dependencies,
reexports_snippet,
}),
pascal_identifier,
include_imports,
static_property_definitions,
is_custom_interpolatable,
}.render_once().unwrap().to_string();
let ret = TokenStream::from_str(&output).unwrap().into();
if !include_fix.is_none(){
quote!{
#include_fix
#ret
}
}else {
ret
}.into()
}
#[proc_macro_derive(Pax, attributes(main, file, inlined, primitive, custom, default))]
pub fn pax_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let attrs = &input.attrs;
let mut is_main_component = false;
let mut file_path: Option<String> = None;
let mut inlined_contents: Option<String> = None;
let mut custom_values: Option<Vec<String>> = None;
let mut primitive_instance_import_path: Option<String> = None;
let mut is_primitive = false;
for attr in attrs {
if attr.path.is_ident("file") {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if let Some(nested_meta) = meta_list.nested.first() {
if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
file_path = Some(file_str.value());
}
}
}
} else if attr.path.is_ident("primitive") {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if let Some(nested_meta) = meta_list.nested.first() {
if let syn::NestedMeta::Lit(Lit::Str(file_str)) = nested_meta {
primitive_instance_import_path = Some(file_str.value());
is_primitive = true;
}
}
}
} else if attr.path.is_ident("inlined") {
let tokens = attr.tokens.clone();
let mut content = proc_macro2::TokenStream::new();
for token in tokens {
match token {
proc_macro2::TokenTree::Group(group) => {
if group.delimiter() == proc_macro2::Delimiter::Parenthesis {
content.extend(group.stream());
}
}
_ => {}
}
}
if !content.is_empty() {
inlined_contents = Some(content.to_string());
}
} else {
match attr.parse_meta() {
Ok(Meta::Path(path)) => {
if path.is_ident("main") {
is_main_component = true;
}
}
Ok(Meta::List(meta_list)) => {
if meta_list.path.is_ident("custom") {
let values: Vec<String> = meta_list
.nested
.into_iter()
.filter_map(|nested_meta| {
if let syn::NestedMeta::Meta(Meta::Path(path)) = nested_meta {
path.get_ident().map(|ident| ident.to_string())
} else {
None
}
})
.collect();
custom_values = Some(values);
}
}
_ => {}
}
}
}
if let (Some(_), Some(_)) = (file_path.as_ref(), inlined_contents.as_ref()) {
return syn::Error::new_spanned(input.ident, "`#[file(...)]` and `#[inlined(...)]` attributes cannot be used together")
.to_compile_error()
.into();
}
if let (None, None) = (file_path.as_ref(), inlined_contents.as_ref()) {
if is_main_component {
return syn::Error::new_spanned(input.ident, "Main (application-root) components must specify either a Pax file or inlined Pax content, e.g. #[file(\"some-file.pax\")] or #[inlined(<SomePax />)]")
.to_compile_error()
.into();
}
}
if is_primitive {
const ERR : &str = "Primitives cannot have attached templates. Instead, specify a fully qualified Rust import path pointing to the `impl RenderNode` struct for this primitive.";
if let Some(_) = file_path.as_ref() {
return syn::Error::new_spanned(input.ident, ERR)
.to_compile_error()
.into();
}
if let Some(_) = inlined_contents.as_ref() {
return syn::Error::new_spanned(input.ident, ERR)
.to_compile_error()
.into();
}
}
let mut clone_impl = match &input.data {
Data::Struct(data_struct) => {
match &data_struct.fields {
Fields::Named(fields) => {
let field_names = fields.named.iter().map(|f| &f.ident);
quote! {
impl #impl_generics ::core::clone::Clone for #name #ty_generics #where_clause {
fn clone(&self) -> Self {
#name {
#( #field_names : ::core::clone::Clone::clone(&self.#field_names), )*
}
}
}
}
}
Fields::Unnamed(fields) => {
let indices = (0..fields.unnamed.len()).map(syn::Index::from);
quote! {
impl #impl_generics ::core::clone::Clone for #name #ty_generics #where_clause {
fn clone(&self) -> Self {
#name (
#( ::core::clone::Clone::clone(&self.#indices), )*
)
}
}
}
}
Fields::Unit => {
quote! {
impl #impl_generics ::core::clone::Clone for #name #ty_generics #where_clause {
fn clone(&self) -> Self {
#name
}
}
}
}
}
}
Data::Enum(data_enum) => {
let variants = data_enum.variants.iter().map(|v| {
let variant_ident = &v.ident;
match &v.fields {
Fields::Named(fields) => {
let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect();
quote! {
#name::#variant_ident { #(ref #field_names, )* } => {
#name::#variant_ident {
#( #field_names : #field_names.clone(), )*
}
}
}
}
Fields::Unnamed(fields) => {
let indices: Vec<_> = (0..fields.unnamed.len()).map(|i| {
let name = format!("_{}", i);
Ident::new(&name, proc_macro2::Span::call_site())
}).collect();
quote! {
#name::#variant_ident ( #(ref #indices, )* ) => {
#name::#variant_ident (
#( #indices.clone(), )*
)
}
}
}
Fields::Unit => {
quote! {
#name::#variant_ident => #name::#variant_ident,
}
}
}
});
quote! {
impl #impl_generics ::core::clone::Clone for #name #ty_generics #where_clause {
fn clone(&self) -> Self {
match self {
#( #variants )*
}
}
}
}
}
Data::Union(_) => {
panic!("`Pax` derive macro does not support unions.");
}
};
let mut default_impl = match &input.data {
Data::Struct(data_struct) => {
match &data_struct.fields {
Fields::Named(fields_named) => {
let field_defaults = fields_named.named.iter().map(|f| {
let name = &f.ident;
quote! { #name: Default::default() }
});
quote! {
impl #impl_generics Default for #name #ty_generics #where_clause {
fn default() -> Self {
Self {
#(#field_defaults,)*
}
}
}
}
}
Fields::Unnamed(_) | Fields::Unit => {
quote! {
impl #impl_generics Default for #name #ty_generics #where_clause {
fn default() -> Self {
Self::default()
}
}
}
}
}
}
Data::Enum(data_enum) => {
let default_variant = data_enum.variants.iter().find(|variant| {
variant.attrs.iter().any(|attr| attr.path.is_ident("default"))
});
match default_variant {
Some(variant) => {
let variant_ident = &variant.ident;
quote! {
impl #impl_generics Default for #name #ty_generics #where_clause {
fn default() -> Self {
Self::#variant_ident
}
}
}
}
None => {
quote! {
compile_error!("#[default] attribute required on one of the enum variants");
}
}
}
}
Data::Union(_) => {
quote! {
compile_error!("Pax derive does not currently support Unions");
}
}
};
let mut include_imports = true;
let mut is_custom_interpolatable = false;
if let Some(custom) = custom_values {
if custom.contains(&"Default".to_string()) {
default_impl = quote! {};
}
if custom.contains(&"Clone".to_string()) {
clone_impl = quote! {};
}
if custom.contains(&"Imports".to_string()) {
include_imports = false;
}
if custom.contains(&"Interpolatable".to_string()) {
is_custom_interpolatable = true;
}
}
let is_pax_file = matches!(&file_path, Some(_f));
let is_pax_inlined = matches!(&inlined_contents, Some(_i));
let appended_tokens = if is_pax_file {
let filename = if let Some(p) = file_path.as_ref() {p} else {unreachable!()};
let current_dir = std::env::current_dir().expect("Unable to get current directory");
let path = current_dir.join(Path::new("src").join(Path::new(&filename)));
let name = Ident::new("PaxFile", Span::call_site());
let include_fix = generate_include(&name,path.clone().to_str().unwrap());
let mut file = File::open(path);
let mut content = String::new();
let _ = file.unwrap().read_to_string(&mut content);
let stream: proc_macro::TokenStream = content.parse().unwrap();
pax_full_component(stream.to_string(), input.clone(), is_main_component, Some(include_fix),include_imports, is_custom_interpolatable)
} else if is_pax_inlined {
let contents = if let Some(p) = inlined_contents {p} else {unreachable!()};
pax_full_component(contents, input.clone(), is_main_component, None,include_imports, is_custom_interpolatable)
} else if is_primitive {
pax_primitive(input.clone(), primitive_instance_import_path.unwrap(), include_imports, is_custom_interpolatable)
} else {
pax_struct_only_component(input.clone(), include_imports, is_custom_interpolatable)
};
let output = quote! {
#appended_tokens
#clone_impl
#default_impl
};
output.into()
}
fn generate_include(name: &Ident, path: &str) -> TokenStream {
let const_name = Ident::new(&format!("_PAX_FILE_{}", name), Span::call_site());
quote! {
#[allow(non_upper_case_globals)]
const #const_name: &'static str = include_str!(#path);
}
}