bok/bok-macro/src/lib.rs
Luca Fulchir 1d9ea5a699
Propagate _code to Pkg trait
Signed-off-by: Luca Fulchir <luca.fulchir@runesauth.com>
2024-03-25 09:08:18 +01:00

426 lines
13 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::{ParseBuffer, ParseStream, Parser},
parse_macro_input, DeriveInput,
};
/// Use as #[repository(MyBaseRepo)]
/// Will setup a `base` field that is the given base repo
///
/// e.g.: `#[repository(::bok::RepositoryEmpty)]`
#[proc_macro_attribute]
pub fn repository(attrs: TokenStream, input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as DeriveInput);
match &mut ast.data {
syn::Data::Struct(ref mut struct_data) => {
match &mut struct_data.fields {
syn::Fields::Named(fields) => {
let base = proc_macro2::TokenStream::from(attrs);
fields.named.push(
syn::Field::parse_named
.parse2(quote! { base: #base })
.unwrap(),
);
}
_ => (),
}
quote! {
#ast
}
.into()
}
_ => panic!("`repository` has to be used with a struct"),
}
}
/// Use as #[derive(Repository)]
/// adds extension capabilities to a repo
#[proc_macro_derive(Repository)]
pub fn derive_repository(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident.clone();
let elements = match input.data {
::syn::Data::Struct(s) => match s.fields {
syn::Fields::Named(n) => n.named,
_ => panic!("only named supported"),
},
_ => panic!("`Repository` has to be used on a struct"),
};
let base_type = elements
.iter()
.find_map(|field| {
if let Some(name) = &field.ident {
if name != "base" {
None
} else {
let t = &field.ty;
Some(quote! {#t})
}
} else {
None
}
})
.expect("can't find base value. Add #[repository] to your struct");
let expanded = quote! {
impl ::bok::Repository for #name {}
impl ::std::ops::Deref for #name {
type Target = #base_type;
fn deref(&self) -> &#base_type {
&self.base
}
}
};
TokenStream::from(expanded)
}
/// Use on a function as #[to_code]
/// will create a new function, same name with `_code` that
/// returns the ::proc_macro2::TokenStream of the function
///
/// e.g.:
/// ```
/// impl MyPackage {
/// #[package(::bok_macro::to_code)]
/// fn build() {
/// ...
/// }
/// }
/// let p = MyPackage::default();
/// let tokens : ::proc_macro2::TokenStream = p.build_code()
/// ```
#[proc_macro_attribute]
pub fn to_code(_attrs: TokenStream, input: TokenStream) -> TokenStream {
let ast_orig: ::syn::ItemFn = parse_macro_input!(input as ::syn::ItemFn);
let mut ast_new = ast_orig.clone();
let new_fn_name =
quote::format_ident!("{}", ast_orig.sig.ident.to_string() + "_code");
ast_new.sig.ident = quote::format_ident!("{}", new_fn_name);
use ::quote::ToTokens;
let fn_block = ast_new.block.to_token_stream();
//let quote_macro = ::quote::format_ident!("quote");
let asdf = quote! {
#ast_orig
fn #new_fn_name () -> ::proc_macro2::TokenStream {
//::quote::#quote_macro! {
::quote::quote! #fn_block
//}
}
};
asdf.into()
/*
let fn_block_pb = ::syn::punctuated::Punctuated::<
::syn::Expr,
::syn::Token![,],
>::parse_terminated
.parse2(fn_block)
.unwrap();
*/
/*
let fn_block_pb = ::syn::parse::Parser::parse2(
//::syn::punctuated::Punctuated::<::syn::Expr, ::syn::Token![,]>::parse_terminated,
|input: ParseStream<'_>| ::syn::Result::Ok({ input.clone() }),
fn_block,
)
.unwrap();
*/
/*
let fn_block_pb = ::syn::punctuated::Punctuated::<
//::syn::parse::ParseBuffer<'_>,
proc_macro2::TokenStream,
::syn::Token![,],
>::parse_terminated
.parse2(fn_block)
.unwrap();
*/
/*
let wtf: ::syn::parse::ParseBuffer<'_> =
parse_macro_input!(fn_block_pb as ::syn::parse::ParseBuffer).into();
*/
/*
match ::syn::Block::parse_within(&fn_block) {
//match ::syn::Block::parse_within(&wtf) {
Ok(_) => {
//
}
Err(_) => panic!("can't parse code block"),
}
*/
/*
use ::quote::ToTokens;
ast_new.into_token_stream().into()
*/
}
/// Use as #[package(MyBasePackage)]
/// Will setup a `base` field that is the given base package
///
/// e.g.: `#[package(::bok::PkgEmpty)]`
#[proc_macro_attribute]
pub fn package(attrs: TokenStream, input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as DeriveInput);
match &mut ast.data {
syn::Data::Struct(ref mut struct_data) => {
match &mut struct_data.fields {
syn::Fields::Named(fields) => {
let base = proc_macro2::TokenStream::from(attrs);
fields.named.push(
syn::Field::parse_named
.parse2(quote! { base: #base })
.unwrap(),
);
}
_ => (),
}
quote! {
#ast
}
.into()
}
_ => panic!("`package` has to be used with a struct"),
}
}
/// #[derive(Package)]
/// adds:
/// * Builder pattern to a package
/// * deref for builder towards `.base`
/// * getters/setters for all package fields
/// * deref for package
#[proc_macro_derive(Package)]
pub fn derive_package(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident.clone();
let name_builder = quote::format_ident!("{name}Builder");
let name_builder2 = name_builder.clone();
let elements = match input.data {
::syn::Data::Struct(s) => match s.fields {
syn::Fields::Named(n) => n.named,
_ => panic!("only named supported"),
},
_ => panic!("only struct allowed"),
};
let all_fields = elements.iter().filter_map(|field| {
if let Some(id) = field.ident.clone() {
if id.to_string() != "base" {
return Some(&field.ident);
}
}
None
});
let all_fields2 = all_fields.clone();
let all_fields3 = all_fields.clone();
let all_fields_mut = elements.iter().filter_map(|field| {
if let Some(id) = field.ident.clone() {
if id.to_string() != "base" {
return Some(quote::format_ident!("{}_mut", id.to_string()));
}
}
None
});
let base_type = elements
.iter()
.find_map(|field| {
if let Some(id) = field.ident.clone() {
if id.to_string() == "base" {
return Some(&field.ty);
}
}
None
})
.expect("expected a base type");
let all_types = elements.iter().filter_map(|field| {
if let Some(id) = field.ident.clone() {
if id.to_string() != "base" {
return Some(&field.ty);
}
}
None
});
let all_types2 = all_types.clone();
let non_opt_fields = elements.iter().filter_map(|field| {
if let Some(id) = field.ident.clone() {
if id.to_string() == "base" {
return None;
}
}
match &field.ty {
syn::Type::Path(pth) => {
let first_path = pth.path.segments.first().unwrap();
if first_path.ident == "Option" {
None
} else {
let id = &field.ident;
Some(quote! {#id})
}
}
t => Some(quote! {#t}),
}
});
let opt_fields = elements.iter().filter_map(|field| {
if let Some(id) = field.ident.clone() {
if id.to_string() == "base" {
return None;
}
}
match &field.ty {
syn::Type::Path(pth) => {
let first_path = pth.path.segments.first().unwrap();
if first_path.ident == "Option" {
let id = &field.ident;
Some(quote! {#id})
} else {
None
}
}
_ => None,
}
});
let non_opt_types = elements.iter().filter_map(|field| {
if let Some(id) = field.ident.clone() {
if id.to_string() == "base" {
return None;
}
}
match &field.ty {
syn::Type::Path(pth) => {
let first_path = pth.path.segments.first().unwrap();
if first_path.ident == "Option" {
None
} else {
let t = &field.ty;
Some(quote! {#t})
}
}
_ => None,
}
});
let opt_types = elements.iter().filter_map(|field| {
if let Some(id) = field.ident.clone() {
if id.to_string() == "base" {
return None;
}
}
if let ::syn::Type::Path(pth) = &field.ty {
let first_path = pth.path.segments.first().unwrap();
if first_path.ident == "Option" {
if let syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments { args, .. },
) = &first_path.arguments
{
if let Some(syn::GenericArgument::Type(syn::Type::Path(
p,
))) = args.first()
{
let id = &p.path.segments.first().unwrap().ident;
return Some(quote! {#id});
}
}
}
}
None
});
let non_opt_fields2 = non_opt_fields.clone();
let non_opt_fields3 = non_opt_fields.clone();
let non_opt_fields4 = non_opt_fields.clone();
let opt_fields2 = opt_fields.clone();
let opt_fields3 = opt_fields.clone();
let opt_types2 = opt_types.clone();
let non_opt_types2 = non_opt_types.clone();
let expanded = quote! {
impl ::bok::Pkg for #name {
fn base(&self) -> Option<&impl ::bok::Pkg> {
Some(&self.base)
}
}
// FIXME: proper formatting of all functions
impl ::core::fmt::Display for #name {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>)
->::core::fmt::Result
{
use ::bok::Pkg;
let pkg = self.package_code();
pkg.fmt(f)
}
}
impl #name {
pub fn builder() -> #name_builder {
#name_builder {
#(#all_fields : ::std::option::Option::None,)*
}
}
#(pub fn #all_fields2(&self) -> &#all_types {
&self.#all_fields2
})*
#(pub fn #all_fields_mut(&mut self) -> &#all_types2 {
&mut self.#all_fields3
})*
}
pub struct #name_builder {
#(#non_opt_fields: ::std::option::Option<#non_opt_types>,)*
#(#opt_fields: ::std::option::Option<#opt_types>,)*
}
/*
impl ::std::ops::Deref for #name_builder {
type Target = #base_type;
fn deref(&self) -> &#base_type {
&self.base
}
}
*/
impl #name_builder2 {
pub fn build(&mut self) -> Result<#name, ::std::boxed::Box<dyn ::std::error::Error>> {
#(if self.#non_opt_fields2.is_none() {
return ::std::result::Result::Err("unset field".into());
})*
Ok(
#name {
// FIXME: user must be able to override. Trait?
base: #base_type::default(),
#(#non_opt_fields3 : self.#non_opt_fields3.clone().unwrap(),)*
#(#opt_fields2 : self.#opt_fields2.clone(),)*
}
)
}
#(pub fn #non_opt_fields4 (&mut self, val : #non_opt_types2) -> &mut Self {
self.#non_opt_fields4 = Some(val);
self
})*
#(pub fn #opt_fields3 (&mut self, val : #opt_types2) -> &mut Self {
self.#opt_fields3 = Some(val);
self
})*
}
};
TokenStream::from(expanded)
}