bok/bok-macro/src/repos.rs
Luca Fulchir ae44556dd7
better ergonomics: hide generics in packages
Signed-off-by: Luca Fulchir <luca.fulchir@runesauth.com>
2024-12-03 22:40:16 +01:00

635 lines
19 KiB
Rust

/*
* Copyright 2024 Luca Fulchir <luca.fulchir@runesauth.com>
*
* Licensed under the Apache License, Version 2.0 with LLVM exception (the
* "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License and of the exception at
*
* http://www.apache.org/licenses/LICENSE-2.0
* https://spdx.org/licenses/LLVM-exception.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use ::proc_macro::TokenStream;
use ::quote::quote;
use ::syn::{parse_macro_input, DeriveInput, Fields, ItemImpl, ItemStruct};
pub(crate) fn repository(
attrs: TokenStream,
input: TokenStream,
) -> TokenStream {
let local = parse_macro_input!(input as ItemStruct);
let Fields::Named(local_fields) = local.fields else {
use ::syn::spanned::Spanned;
return ::syn::Error::new(
local.fields.span(),
"unnamed fields are not supported",
)
.to_compile_error()
.into();
};
// do not duplicate token export or derive macro
let local_attrs = local.attrs.iter().filter(|&x| {
match &x.meta {
::syn::Meta::Path(p) => {
// looking for:
// #[::macro_magic::export_tokens]
if p.segments.len() == 2
&& p.segments[0].ident == "macro_magic"
&& p.segments[1].ident == "export_tokens"
{
false
} else {
true
}
}
::syn::Meta::List(ml) => {
// looking for:
// #[derive(::bok::Repository, Debug)]
if ml.path.segments.len() > 0 && ml.path.is_ident("derive") {
use ::syn::{punctuated::Punctuated, *};
if let Ok(v) = ml.parse_args_with(
Punctuated::<::syn::Path, Token![,]>::parse_terminated,
) {
if v.len() == 2
&& v[0].segments.len() == 2
&& v[0].segments[0].ident.to_string() == "bok"
&& v[0].segments[1].ident.to_string()
== "Repository"
&& v[1].segments.len() == 1
&& v[1].segments[0].ident.to_string() == "Debug"
{
return false;
}
}
}
true
}
_ => true,
}
});
let (_, generics, where_clause) = local.generics.split_for_impl();
let ident = local.ident;
let vis = local.vis;
let base = parse_macro_input!(attrs as ItemStruct);
let Fields::Named(base_fields) = &base.fields else {
use ::syn::spanned::Spanned;
return ::syn::Error::new(
base.fields.span(),
"`#[::bok::repository(..)]`: base has unsupported unnamed fields",
)
.to_compile_error()
.into();
};
// make sure base is a repo
if base_fields
.named
.iter()
.find(|&x| {
x.ident
.as_ref()
.is_some_and(|id| id.to_string() == "_bok_repo")
})
.is_none()
{
use ::syn::spanned::Spanned;
return ::syn::Error::new(
base.fields.span(),
"`#[::bok::repository(..)]` base is not a repo",
)
.to_compile_error()
.into();
}
let mut all_fields = Vec::<::syn::Field>::with_capacity(
local_fields.named.len() + base_fields.named.len(),
);
all_fields.extend(local_fields.named.iter().cloned());
// make sure there is always `_bok_repo` marker
if local_fields
.named
.iter()
.find(|&f| {
f.ident
.as_ref()
.is_some_and(|id| id.to_string() == "_bok_repo")
})
.is_none()
{
all_fields.push(::syn::parse_quote! {
_bok_repo: ::std::marker::PhantomData<::bok::RepositoryEmpty>
});
}
for b_f in base_fields.named.iter() {
let Some(b_f_id) = &b_f.ident else { continue };
let b_f_id_str = b_f_id.to_string();
if all_fields
.iter()
.find(|&f| {
f.ident
.as_ref()
.is_some_and(|id| id.to_string() == b_f_id_str)
})
.is_none()
{
all_fields.push(b_f.clone());
}
}
quote! {
#(#local_attrs)
*
#[::macro_magic::export_tokens]
#[derive(::bok::Repository, Debug)]
#vis struct #ident #generics #where_clause {
#(#all_fields),
*
}
}
.into()
}
pub(crate) fn derive_repository(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident.clone();
let ::syn::Data::Struct(items) = &input.data else {
use ::syn::spanned::Spanned;
return ::syn::Error::new(
input.span(),
"#[derive(::bok::Repository)]: not called on a struct",
)
.to_compile_error()
.into();
};
if items
.fields
.iter()
.find(|&f| {
f.ident
.as_ref()
.is_some_and(|id| id.to_string() == "_bok_repo")
})
.is_none()
{
use ::syn::spanned::Spanned;
return ::syn::Error::new(
input.span(),
"#[derive(::bok::Repository)]: struct is not a bok repo. use \
`#[::bok::repository(..)]` first",
)
.to_compile_error()
.into();
}
// holds the list of all package names, snake case
let mut all_pkgs = Vec::<::syn::Ident>::with_capacity(items.fields.len());
for it in items.fields.iter() {
let Some(id) = &it.ident else { continue };
let id_str = id.to_string();
if id_str.starts_with("_p_") {
let name = id_str.strip_prefix("_p_").unwrap().to_owned();
all_pkgs.push(::quote::format_ident!("{}", name));
}
}
all_pkgs.sort_by(|a, b| a.to_string().cmp(&b.to_string()));
let pkgs_num: usize = all_pkgs.len();
quote! {
impl ::bok::Repository for #name {
fn name(&self) -> ::bok::RepoName {
(module_path!().to_owned() + "::" + ::std::stringify!(#name)).as_str().into()
}
fn path(&self) -> ::bok::Path<::bok::RepoName> {
(module_path!().to_owned() + "::" + ::std::stringify!(#name)).as_str().into()
}
fn pkg_list(&self) -> &[&'static str] {
const PKGS : [&'static str; #pkgs_num] = [
#(stringify!(#all_pkgs)),
*
];
&PKGS
}
fn get(&self, pkg_name: &str) -> Option<::std::boxed::Box<dyn ::bok::Pkg>> {
match pkg_name {
#(#all_pkgs => Some(::std::boxed::Box::new(self.#all_pkgs())),)
*
_ => None,
}
}
}
}.into()
}
pub(crate) fn repo_packages(
attrs: TokenStream,
input: TokenStream,
) -> TokenStream {
let local = parse_macro_input!(input as ItemStruct);
let mut packages = parse_macro_input!(attrs as crate::PathList);
// make sure all the packages have generics, and if not add the repo as
// generic aka: rewrite
// "#[repo_packages(my::pkg)]" -->> "#[repo_packages(my::pkg<Self>)])"
let mut rewrite = false;
let repo_argument = {
let generic_id: ::syn::Path = ::syn::parse_quote!(id<Self>);
generic_id.segments.last().unwrap().arguments.clone()
};
for p in packages.0.iter_mut() {
let last = p.segments.last_mut().unwrap();
if last.arguments.is_empty() {
rewrite = true;
last.arguments = repo_argument.clone();
}
}
if rewrite {
let p_list = packages.0.into_iter();
return quote! {
#[::bok::repo_packages(#(#p_list,)*)]
#local
}
.into();
}
let packages = packages; // remove mut
let local_attrs = local.attrs.iter();
let (_, generics, where_clause) = local.generics.split_for_impl();
let ident = local.ident;
let vis = local.vis;
use ::syn::spanned::Spanned;
let Fields::Named(ref local_fields) = local.fields else {
use ::syn::spanned::Spanned;
return ::syn::Error::new(
local.fields.span(),
"#[repo_packages(..)]: unnamed fields are not supported",
)
.to_compile_error()
.into();
};
// find the marker. we need it to separate things added manually
// from things we get by extending the other repositories
let Some(marker) = local_fields.named.iter().find(|&f| {
f.ident
.as_ref()
.is_some_and(|id| id.to_string() == "_bok_repo")
}) else {
use ::syn::spanned::Spanned;
return ::syn::Error::new(
local_fields.span(),
"#[repo_packages(..)]: struct is not a repository. Forgot \
'#[::bok::repository(..)]` first?",
)
.to_compile_error()
.into();
};
let fields_up_to_marker = local_fields
.named
.iter()
.take_while(|&f| {
f.ident
.as_ref()
.is_some_and(|id| id.to_string() != "_bok_repo")
})
.collect::<Vec<&::syn::Field>>();
let mut fields_after_marker = local_fields
.named
.iter()
.skip_while(|&f| {
f.ident
.as_ref()
.is_some_and(|id| id.to_string() != "_bok_repo")
})
.skip_while(|&f| {
f.ident
.as_ref()
.is_some_and(|id| id.to_string() == "_bok_repo")
})
.collect::<Vec<&::syn::Field>>();
// the packages added manually must not be repeated manually.
// but they will override any other package added by
// extending the repository
let mut fields_added = Vec::<::syn::Field>::with_capacity(packages.0.len());
for p in packages.0 {
let path_ident = &p.segments.last().unwrap().ident;
use ::convert_case::{Case, Casing};
let pkg_id = "_p_".to_owned()
+ path_ident.to_string().to_case(Case::Snake).as_str();
if fields_up_to_marker
.iter()
.find(|&f| {
if let Some(id) = &f.ident {
id.to_string() == pkg_id
} else {
false
}
})
.is_some()
{
use ::syn::spanned::Spanned;
return ::syn::Error::new(
local_fields.span(),
"#[repo_packages(..)]: package already present: ".to_owned()
+ pkg_id.as_str(),
)
.to_compile_error()
.into();
}
if fields_added
.iter()
.find(|&f| {
if let Some(id) = &f.ident {
id.to_string() == pkg_id
} else {
false
}
})
.is_some()
{
use ::syn::spanned::Spanned;
return ::syn::Error::new(
local_fields.span(),
"#[repo_packages(..)]: package added twice: ".to_owned()
+ pkg_id.as_str(),
)
.to_compile_error()
.into();
}
let pkg_ident =
::quote::format_ident!("{}", pkg_id, span = local_fields.span());
let new_pkg: ::syn::Field = ::syn::parse_quote! {
#pkg_ident : ::std::marker::PhantomData<#p>
};
fields_added.push(new_pkg);
fields_after_marker.retain(|&f| {
f.ident.as_ref().is_some_and(|id| id.to_string() != pkg_id)
});
}
let mut all_fields =
Vec::with_capacity(local_fields.named.len() + fields_added.len());
all_fields.extend(fields_up_to_marker.iter());
all_fields.extend(fields_added.iter());
all_fields.push(marker);
all_fields.extend(fields_after_marker.iter());
quote! {
#(#local_attrs)
*
#vis struct #ident #generics #where_clause {
#(#all_fields),
*
}
}
.into()
}
pub(crate) fn repo_impl(
_attrs: TokenStream,
input: TokenStream,
) -> TokenStream {
let local = parse_macro_input!(input as ItemImpl);
let reponame = &local.self_ty;
quote! {
#[::bok_macro::repo_impl_methods(#reponame)]
#local
}
.into()
}
pub(crate) fn repo_impl_methods(
attrs: TokenStream,
input: TokenStream,
__source_name: TokenStream,
) -> TokenStream {
let base = parse_macro_input!(attrs as ItemStruct);
let local = parse_macro_input!(input as ItemImpl);
let source_name = parse_macro_input!(__source_name as ::syn::Path);
if let ::syn::Type::Path(self_type_path) = local.self_ty.as_ref() {
if self_type_path.path != source_name {
return ::syn::Error::new(
proc_macro2::Span::call_site(),
"#[::bok_macro::repo_impl_methods(..)]: argument and impl \
type differ",
)
.to_compile_error()
.into();
}
} else {
return ::syn::Error::new(
proc_macro2::Span::call_site(),
"#[::bok_macro::repo_impl_methods(..)]: argument and impl type \
differ",
)
.to_compile_error()
.into();
}
let local_attrs = local.attrs.iter();
let (_, generics, where_clause) = local.generics.split_for_impl();
let ::syn::Type::Path(local_tp) = local.self_ty.as_ref() else {
return ::syn::Error::new(
proc_macro2::Span::call_site(),
"#[::bok_macro::repo_impl_methods(..)]: no ident?",
)
.to_compile_error()
.into();
};
let local_ident = local_tp.path.get_ident().expect("NOT AN IDENT");
let items = local.items.iter();
// FIXME: make sure `items` does not have methods that are not packages in
// the impl
let ::syn::Fields::Named(base_fields) = base.fields else {
return ::syn::Error::new(
proc_macro2::Span::call_site(),
"#[::bok_macro::repo_impl_methods(..)]: type has unsupported \
unnamed fields",
)
.to_compile_error()
.into();
};
let mut all_pkgs_types = Vec::with_capacity(base_fields.named.len());
let mut fn_to_add = Vec::new();
for f in base_fields.named.iter() {
if !f
.ident
.as_ref()
.is_some_and(|id| id.to_string().starts_with("_p_"))
{
continue;
};
let ::syn::Type::Path(f_type) = &f.ty else {
continue;
};
let t_phantom = &f_type.path;
let t_id = {
let args = &t_phantom
.segments
.last()
.expect("t_phantom no last?")
.arguments;
let ::syn::PathArguments::AngleBracketed(bracketed) = &args else {
panic!("phantom without anglebracket?");
};
let ::syn::GenericArgument::Type(::syn::Type::Path(t)) =
bracketed.args.first().expect("phantom bracketed, no args")
else {
panic!("phantom bracketed, generic not a type path");
};
t.path.clone()
};
let pkg_name = {
let segment = t_id.segments.iter().last().unwrap();
use ::convert_case::{Case, Casing};
::quote::format_ident!(
"{}",
segment.ident.to_string().to_case(Case::Snake)
)
};
all_pkgs_types.push(t_id.clone());
if local
.items
.iter()
.find(|&func_it| {
let ::syn::ImplItem::Fn(func) = &func_it else {
return false;
};
func.sig.ident.to_string() == pkg_name.to_string()
})
.is_some()
{
// the user overrode the `::default()` package build
// don't try to add it again
continue;
}
let mut t_id_build = t_id.clone();
if let ::syn::PathArguments::AngleBracketed(args) =
&mut t_id_build.segments.last_mut().unwrap().arguments
{
args.colon2_token = Some(::syn::token::PathSep::default());
}
let pkg_fn: ::syn::ImplItemFn = ::syn::parse_quote! {
pub fn #pkg_name(&self) -> #t_id {
#t_id_build::default()
}
};
fn_to_add.push(pkg_fn);
}
// keep sorted for easier debugging when expanding macros
fn_to_add
.sort_by(|a, b| a.sig.ident.to_string().cmp(&b.sig.ident.to_string()));
all_pkgs_types.sort_by(|a, b| {
a.segments
.last()
.unwrap()
.ident
.to_string()
.cmp(&b.segments.last().unwrap().ident.to_string())
});
let new_fn = fn_to_add.iter();
let mut all_impl_deps = Vec::with_capacity(all_pkgs_types.len());
for p_type in all_pkgs_types.into_iter() {
let bok_dep_trait = {
let mut tmp = p_type.clone();
let last = tmp.segments.last_mut().unwrap();
last.ident = quote::format_ident!("BokDeps{}", last.ident);
last.arguments = ::syn::PathArguments::None;
tmp
};
let pkg_trait_impl = &bok_dep_trait;
let dep_view = quote! {
#[::bok_macro::repo_impl_pkg_deps(#pkg_trait_impl)]
impl #bok_dep_trait for #local_ident {}
};
all_impl_deps.push(dep_view);
}
quote! {
#(#local_attrs)
*
impl #local_ident #generics #where_clause {
#(#items)
*
#(#new_fn)
*
}
#(#all_impl_deps)
*
}
.into()
}
pub(crate) fn repo_impl_pkg_deps(
attrs: TokenStream,
input: TokenStream,
) -> TokenStream {
let trait_deps = parse_macro_input!(attrs as ::syn::ItemTrait);
let local = parse_macro_input!(input as ItemImpl);
let impl_trait = local
.trait_
.expect("#[::bok_macro::repo_impl_pkg_deps()]: no trait found")
.1;
let local_attrs = local.attrs.iter();
let (_, generics, _) = local.generics.split_for_impl();
let ident = &local.self_ty;
//let deps = Vec::<::syn::TraitItemFn>::new();
let deps = trait_deps
.items
.iter()
.filter_map(|x| {
if let ::syn::TraitItem::Fn(func) = &x {
let name = &func.sig.ident;
let dep_impl: ::syn::TraitItemFn = ::syn::parse_quote! {
fn #name(&self) -> ::std::boxed::Box<dyn ::bok::Pkg> {
::std::boxed::Box::new(self.#name())
}
};
Some(dep_impl)
} else {
None
}
})
.collect::<Vec<::syn::TraitItemFn>>();
quote! {
#(#local_attrs)
*
impl #impl_trait for #ident #generics {
#(#deps)
*
}
}
.into()
}