use core::error::Error; use std::{ io, path::{Path, PathBuf}, }; use convert_case::Casing; use proc_macro2::Span; use quote::{ToTokens, quote}; use syn::{parse::Parse, parse_macro_input}; pub(crate) mod hashlist; mod targets; struct IncludePath { dir_path: PathBuf, modules: Vec, } fn display_err(span: Span, err: impl Error) -> syn::Error { syn::Error::new(span, err.to_string().as_str()) } fn read_modules(span: Span, path: &Path) -> syn::Result> { let read_dir = std::fs::read_dir(path).map_err(|err| display_err(span, err))?; Ok(read_dir .map(|file| file.map_err(|err| display_err(span, err))) .collect::, _>>()? .into_iter() .filter(|file| file.file_type().map(|ft| ft.is_file()).unwrap_or_default()) .filter_map(|file| { file.file_name() .to_string_lossy() .strip_suffix(".rs") .map(|f| f.to_string()) }) .map(|module_name| syn::Ident::new(&module_name, span)) .collect::>()) } impl Parse for IncludePath { fn parse(input: syn::parse::ParseStream) -> syn::Result { let directory = input.parse::()?; let dir_path = std::env::current_dir() .unwrap() .join(directory.value()) .to_path_buf(); let modules = read_modules(directory.span(), &dir_path)?; Ok(Self { modules, dir_path }) } } impl IncludePath { fn migrations_tokens(&self) -> proc_macro2::TokenStream { let dir = std::fs::read_dir(&self.dir_path).unwrap(); let mut files = dir .into_iter() .filter_map(|d| { d.ok().and_then(|d| { d.file_name() .to_string_lossy() .ends_with(".sql") .then_some(d.path()) }) }) .map(|path| { let timestamp = path .file_name() .unwrap() .to_string_lossy() .split_once('.') .and_then(|c| c.0.parse::().ok()); match timestamp { Some(ts) => Ok((ts, path)), None => Err(syn::Error::new( Span::call_site(), format!("migration at [{path:?}] doesn't have a valid timestamp").as_str(), )), } }) .collect::, syn::Error>>() .unwrap(); files.sort_by_key(|f| f.0); let migrations_len = files.len(); let includes = files.into_iter().map(|(_, path)| { let path = path.to_str().unwrap(); quote! { include_str!(#path), } }); quote! { const MIGRATIONS: [&'static str, #migrations_len] = [ #(#includes)* ]; } } fn posts_tokens(&self) -> proc_macro2::TokenStream { let mod_decl = self.modules.iter().map(|module| { let path = self.dir_path.join(format!("{module}.rs")); let path = path.to_str().unwrap(); quote! { mod #module { include!(#path); }} }); let posts = self.modules.iter().map(|module| { quote! { #module::POST.into_post() } }); quote! { #(#mod_decl)* pub const POSTS: &[Post] = &[#(#posts),*]; } } fn normal_tokens(&self) -> proc_macro2::TokenStream { let mod_decl = self.modules.iter().map(|module| { quote! {mod #module;} }); let pub_use = self.modules.iter().map(|module| { quote! { #module::* } }); quote! { #(#mod_decl)* pub use {#(#pub_use),*}; } } } enum IncludeOutput { Normal(IncludePath), Posts(IncludePath), Migrations(IncludePath), } impl ToTokens for IncludeOutput { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { tokens.extend(match self { IncludeOutput::Normal(include_path) => include_path.normal_tokens(), IncludeOutput::Posts(include_path) => include_path.posts_tokens(), IncludeOutput::Migrations(include_path) => include_path.migrations_tokens(), }); } } struct FileWithPath { // bytes: Vec, include: proc_macro2::TokenStream, rel_path: String, } impl FileWithPath { fn from_path(path: impl AsRef, origin_path: impl AsRef) -> Result { let abs_path = path .as_ref() .canonicalize() .ok() .and_then(|p| p.to_str().map(|t| t.to_string())) .ok_or(io::Error::new(io::ErrorKind::InvalidData, "invalid path"))?; let include = quote! { include_bytes!(#abs_path) }; let origin_abs = origin_path.as_ref().canonicalize().and_then(|p| { p.to_str() .ok_or(io::Error::new(io::ErrorKind::InvalidData, "invalid path")) .map(|t| t.to_string()) })?; let rel_path = abs_path.replace(&origin_abs, ""); Ok(Self { include, rel_path }) } } fn find_matching_files_recursive( p: P, include_in_rerun: F, directory_filter: D, out: &mut Vec, origin_path: &PathBuf, ) -> Result<(), io::Error> where P: AsRef, F: Fn(&str) -> bool + Copy, D: Fn(&str) -> bool + Copy, { for item in std::fs::read_dir(p.as_ref())? { let item = item?; if item.file_type()?.is_dir() { if directory_filter(item.file_name().to_str().ok_or(io::Error::new( io::ErrorKind::InvalidData, "file has no name", ))?) { find_matching_files_recursive( item.path(), include_in_rerun, directory_filter, out, origin_path, )?; } continue; } if let Some(file_name) = item.file_name().to_str() && include_in_rerun(file_name) { out.push(FileWithPath::from_path(item.path(), origin_path)?); } } Ok(()) } struct IncludeDist { name: syn::Ident, files: Vec, } impl IncludeDist { fn read_dist_path( name: syn::Ident, path_span: Span, path: &Path, origin_path: &PathBuf, ) -> syn::Result { let mut files = Vec::new(); find_matching_files_recursive(path, |_| true, |_| true, &mut files, origin_path) .map_err(|err| syn::Error::new(path_span, err.to_string()))?; Ok(Self { name, files }) } } impl Parse for IncludeDist { fn parse(input: syn::parse::ParseStream) -> syn::Result { let name = input.parse::()?; input.parse::()?; let directory = input.parse::()?; let dir_path = std::env::current_dir() .unwrap() .join(directory.value()) .to_path_buf(); Self::read_dist_path(name, directory.span(), &dir_path, &dir_path) } } impl ToTokens for IncludeDist { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { // std::collections::HashMap let array_contents = self.files.iter().map(|file| { let FileWithPath { include, rel_path } = file; quote! { (#rel_path, #include) } }); let name = &self.name; tokens.extend(quote! { pub const #name: &[(&'static str, &'static [u8])] = &[#(#array_contents),*]; }); } } struct StaticLinks { collect_into_const: Option, relative_to: PathBuf, files: Vec<(String, PathBuf)>, } impl Parse for StaticLinks { fn parse(input: syn::parse::ParseStream) -> syn::Result { let collect_into_const = if input.peek(syn::Ident) { let out = Some(input.parse()?); input.parse::]>()?; out } else { None }; const EXPECTED: &str = "expected 'relative to'"; let directory = input.parse::()?; { let rel = input .parse::() .map_err(|err| syn::Error::new(err.span(), EXPECTED))?; if rel != "relative" { return Err(syn::Error::new(rel.span(), EXPECTED)); } } { let to = input .parse::() .map_err(|err| syn::Error::new(err.span(), EXPECTED))?; if to != "to" { return Err(syn::Error::new(to.span(), EXPECTED)); } } let current_dir = std::env::current_dir().unwrap(); let relative_to = current_dir .join(input.parse::()?.value()) .canonicalize() .expect("cannonicalize relative to path"); let span = directory.span(); let path_dir = current_dir .join(directory.value()) .canonicalize() .expect("canonicalize base path") .to_path_buf(); let read_dir = std::fs::read_dir(&path_dir).map_err(|err| display_err(span, err))?; let files = read_dir .map(|file| file.map_err(|err| display_err(span, err))) .collect::, _>>()? .into_iter() .filter(|file| file.file_type().map(|ft| ft.is_file()).unwrap_or_default()) .map(|file| { ( file.file_name().to_str().expect("bad filename").to_string(), file.path(), ) }) .collect::>(); Ok(StaticLinks { collect_into_const, relative_to, files, }) } } impl ToTokens for StaticLinks { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let base_path = self.relative_to.to_str().expect("base path to_string"); let ident_with_decl = self.files.iter().map(|(file, path)| { let file_ident = syn::Ident::new( file.replace(['.', ' ', '-'], "_").to_uppercase().as_str(), Span::call_site(), ); let filepath = path .to_str() .expect("path to string") .replace(base_path, ""); let decl = quote! { pub const #file_ident: &'static str = #filepath; }; (file_ident, decl) }); let files = ident_with_decl.clone().map(|(_, q)| q); tokens.extend(quote! { #(#files)* }); if let Some(collect_into_const) = &self.collect_into_const { let idents = ident_with_decl.map(|(ident, _)| ident); tokens.extend(quote! { pub const #collect_into_const: &[&str] = &[#(#idents),*]; }); } } } impl StaticLinks { fn into_demand_metadata(self) -> StaticLinksDemandMetadata { let StaticLinks { collect_into_const, relative_to, files, } = self; StaticLinksDemandMetadata { collect_into_const, relative_to, files, } } } struct StaticLinksDemandMetadata { collect_into_const: Option, relative_to: PathBuf, files: Vec<(String, PathBuf)>, } impl ToTokens for StaticLinksDemandMetadata { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let base_path = self.relative_to.to_str().expect("base path to_string"); let ident_with_decl = self.files.iter().map(|(file, path)| { let file_ident = syn::Ident::new( file.replace(['.', ' ', '-'], "_").to_uppercase().as_str(), Span::call_site(), ); let filepath = path .to_str() .expect("path to string") .replace(base_path, ""); let decl = quote! { pub const #file_ident: &'static str = #filepath; }; (file_ident, filepath, decl) }); let files = ident_with_decl.clone().map(|(_, _, q)| q); tokens.extend(quote! { #(#files)* }); if let Some(collect_into_const) = &self.collect_into_const { let idents = ident_with_decl.clone().map(|(ident, _, _)| ident); tokens.extend(quote! { pub const #collect_into_const: &[&str] = &[#(#idents),*]; }); } let (paths_and_snake_idents, members): (Vec<_>, Vec<_>) = ident_with_decl .clone() .map(|(i, filepath, _)| { let snake_ident = syn::Ident::new( i.to_string().to_case(convert_case::Case::Snake).as_str(), i.span(), ); let member = quote! { pub #snake_ident: Metadata, }; ((filepath, snake_ident), member) }) .unzip(); let find_by_key = paths_and_snake_idents.iter().map(|(path, ident)| { quote! { #path => &self.#ident } }); tokens.extend(quote! { pub struct MetadataMap { #(#members)* } impl MetadataMap { pub fn find_by_key(&self, key: &str) -> Option<&Metadata> { Some(match key { #(#find_by_key,)* _ => return None, }) } } }); } } #[proc_macro] pub fn include_dist(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let incl_dist = parse_macro_input!(input as IncludeDist); quote! {#incl_dist}.into() } #[proc_macro] pub fn posts(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let pubuse = IncludeOutput::Posts(parse_macro_input!(input as IncludePath)); quote! {#pubuse}.into() } #[proc_macro] pub fn include_path(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let incl_path = IncludeOutput::Normal(parse_macro_input!(input as IncludePath)); quote! {#incl_path}.into() } #[proc_macro] pub fn static_links(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let static_links = parse_macro_input!(input as StaticLinks); quote! {#static_links}.into() } #[proc_macro] pub fn static_links_demand_metadata(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let demand_metadata = parse_macro_input!(input as StaticLinks).into_demand_metadata(); quote! {#demand_metadata}.into() } #[proc_macro] pub fn migrations(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let incl_path = IncludeOutput::Migrations(parse_macro_input!(input as IncludePath)); quote! {#incl_path}.into() }