635 lines
19 KiB
Rust
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()
|
|
}
|