492 lines
15 KiB
Rust
492 lines
15 KiB
Rust
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<syn::Ident>,
|
|
}
|
|
|
|
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<Vec<syn::Ident>> {
|
|
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::<Result<Vec<_>, _>>()?
|
|
.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::<Vec<_>>())
|
|
}
|
|
|
|
impl Parse for IncludePath {
|
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
let directory = input.parse::<syn::LitStr>()?;
|
|
|
|
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::<u64>().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::<Result<Box<[_]>, 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<u8>,
|
|
include: proc_macro2::TokenStream,
|
|
rel_path: String,
|
|
}
|
|
impl FileWithPath {
|
|
fn from_path(path: impl AsRef<Path>, origin_path: impl AsRef<Path>) -> Result<Self, io::Error> {
|
|
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, F, D>(
|
|
p: P,
|
|
include_in_rerun: F,
|
|
directory_filter: D,
|
|
out: &mut Vec<FileWithPath>,
|
|
origin_path: &PathBuf,
|
|
) -> Result<(), io::Error>
|
|
where
|
|
P: AsRef<Path>,
|
|
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<FileWithPath>,
|
|
}
|
|
|
|
impl IncludeDist {
|
|
fn read_dist_path(
|
|
name: syn::Ident,
|
|
path_span: Span,
|
|
path: &Path,
|
|
origin_path: &PathBuf,
|
|
) -> syn::Result<Self> {
|
|
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<Self> {
|
|
let name = input.parse::<syn::Ident>()?;
|
|
input.parse::<syn::Token![,]>()?;
|
|
let directory = input.parse::<syn::LitStr>()?;
|
|
|
|
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<syn::Ident>,
|
|
relative_to: PathBuf,
|
|
files: Vec<(String, PathBuf)>,
|
|
}
|
|
|
|
impl Parse for StaticLinks {
|
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
|
let collect_into_const = if input.peek(syn::Ident) {
|
|
let out = Some(input.parse()?);
|
|
input.parse::<syn::Token![=>]>()?;
|
|
out
|
|
} else {
|
|
None
|
|
};
|
|
const EXPECTED: &str = "expected 'relative to'";
|
|
let directory = input.parse::<syn::LitStr>()?;
|
|
{
|
|
let rel = input
|
|
.parse::<syn::Ident>()
|
|
.map_err(|err| syn::Error::new(err.span(), EXPECTED))?;
|
|
if rel != "relative" {
|
|
return Err(syn::Error::new(rel.span(), EXPECTED));
|
|
}
|
|
}
|
|
{
|
|
let to = input
|
|
.parse::<syn::Ident>()
|
|
.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::<syn::LitStr>()?.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::<Result<Vec<_>, _>>()?
|
|
.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::<Vec<_>>();
|
|
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<syn::Ident>,
|
|
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()
|
|
}
|