Skip to content

Commit

Permalink
Break down logic into smaller sub-modules
Browse files Browse the repository at this point in the history
Implement features described by new documentation/tests
  • Loading branch information
speelbarrow committed Aug 14, 2023
1 parent 4a26a92 commit 1152033
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 107 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ proc-macro = true

[dependencies]
convert_case = "0.6.0"
proc-macro-error = "1.0.4"
quote = "1.0.32"
syn = { version = "2.0.27", features = ["full", "extra-traits"] }

Expand Down
134 changes: 134 additions & 0 deletions src/extract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use proc_macro::TokenStream;
use proc_macro_error::{abort, emit_error};
use syn::{
parse_quote, punctuated::Pair, spanned::Spanned, Expr, FnArg, ImplItem, ItemImpl, ReturnType,
Signature, Token,
};

use crate::generate::WithoutTypes;

pub fn pub_token(args: TokenStream) -> Result<Option<Token![pub]>, syn::Error> {
if args.is_empty() {
Ok(None)
} else {
syn::parse::<Token![pub]>(args).map(Some)
}
}

pub struct Functions<'a> {
pub signatures: Vec<&'a Signature>,
pub return_type: ReturnType,
pub calls: Vec<Expr>,
pub asyncness: Option<Token![async]>,
pub constness: Option<Token![const]>,
pub unsafety: Option<Token![unsafe]>,
}
impl Functions<'_> {
fn new() -> Self {
Functions {
signatures: Vec::new(),
return_type: ReturnType::Default,
calls: Vec::new(),
asyncness: None,
constness: None,
unsafety: None,
}
}
}
impl<'a> TryFrom<&'a ItemImpl> for Functions<'a> {
type Error = syn::Error;

fn try_from(input: &'a ItemImpl) -> Result<Self, Self::Error> {
let mut r = Functions::new();

// This will be set once the first function is found, and then used to ensure that all other functions have the
// same return type.
let mut return_type: Option<&ReturnType> = None;

// Iterate over all items in the `input` block.
for item in &input.items {
// Only process the item if it is a function.
if let ImplItem::Fn(function) = item {
// If the return type has been set, check that it matches.
if let Some(return_type) = return_type {
if return_type != &function.sig.output {
emit_error!(
return_type.span(),
"return type does not match `{:?}`",
function.sig.output
);
emit_error!(
function.sig.output,
"return type does not match `{:?}`",
return_type
);
}

// Otherwise, assign `return_type`.
} else {
return_type = Some(&function.sig.output);
}

// Check that we aren't mixing `async` and `const` functions (otherwise [`map`] would need to be `async
// const`, which is not possible).
let async_const = match (
&function.sig.asyncness,
&function.sig.constness,
&r.asyncness,
&r.constness,
) {
(Some(asyncness), None, None, Some(constness)) => Some((asyncness, constness)),
(None, Some(constness), Some(asyncness), None) => Some((asyncness, constness)),
_ => None,
};

if let Some((asyncness, constness)) = async_const {
emit_error!(
asyncness,
"cannot mix `async` and `const` functions, as this would require `map` to be `async const`"
);
abort!(
constness,
"cannot mix `async` and `const` functions, as this would require `map` to be `async const`"
);
}

// Once all checks have passed, add the function signature to the list and set the modifier flags on
// the return `struct` (if necessary).
r.signatures.push(&function.sig);
r.calls.push({
let name = &function.sig.ident;
let recv = if let Some(FnArg::Receiver(r)) = &function.sig.inputs.first() {
Some(Pair::new(r, Some(<Token![,]>::default())))
} else {
None
};
let args = FnArg::without_types(&function.sig.inputs);

let mut call = Expr::Call(parse_quote!(Self::#name(#recv #args)));
if function.sig.asyncness.is_some() {
call = Expr::Await(parse_quote!(#call .await));
}

call
});
macro_rules! set_flag {
( $( $flag:ident ),* ) => {
$(
if let Some($flag) = function.sig.$flag {
r.$flag = Some($flag.clone());
}
)*
};
}
set_flag!(asyncness, constness, unsafety);
}
}

if let Some(return_type) = return_type {
r.return_type = return_type.clone();
}

Ok(r)
}
}
73 changes: 73 additions & 0 deletions src/generate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use convert_case::{Case, Casing};
use proc_macro::Span;
use syn::{
parse_quote,
punctuated::{Pair, Punctuated},
Field, FieldsNamed, FnArg, Ident, Pat, Signature, Token, Variant,
};

use crate::extract::Functions;

pub struct Variants(pub Vec<Variant>);
impl Variants {
fn convert_single(signature: &Signature) -> Variant {
let variant_name = Ident::new(
&signature.ident.to_string().to_case(Case::Pascal),
Span::call_site().into(),
);
let fields: Option<FieldsNamed> = {
if !signature.inputs.is_empty() {
let mut inputs = signature.inputs.iter().peekable();
if let Some(FnArg::Receiver(_)) = inputs.peek() {
inputs.next();
}
Some(parse_quote!({ #(#inputs),* }))
} else {
None
}
};

parse_quote!(#variant_name #fields)
}
}
impl From<&Functions<'_>> for Variants {
fn from(input: &Functions<'_>) -> Self {
let mut r = Vec::new();
for signature in &input.signatures {
r.push(Variants::convert_single(signature));
}

Self(r)
}
}

pub trait WithoutTypes: Sized {
fn without_types(from: &Punctuated<Self, Token![,]>) -> Punctuated<Ident, Token![,]>;
}

impl WithoutTypes for FnArg {
fn without_types(from: &Punctuated<Self, Token![,]>) -> Punctuated<Ident, Token![,]> {
Punctuated::from_iter(from.pairs().filter_map(|pair| {
if let FnArg::Typed(pat_type) = pair.value() {
match pat_type.pat.as_ref() {
Pat::Ident(pat_ident) => Some(pat_ident.ident.clone()),
Pat::Wild(_) => None,
_ => unreachable!(),
}
} else {
None
}
}))
}
}

impl WithoutTypes for Field {
fn without_types(from: &Punctuated<Self, Token![,]>) -> Punctuated<Ident, Token![,]> {
Punctuated::from_iter(from.pairs().map(|pair| {
Pair::new(
pair.value().ident.as_ref().unwrap().clone(),
pair.punct().map(|p| **p),
)
}))
}
}
Loading

0 comments on commit 1152033

Please sign in to comment.