plan/plan-macros/src/lib.rs

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()
}