evm-coder-procedural 0.4.3

Implementation of proc-macro for evm-coder
Documentation
use quote::quote;

use super::extract_docs;

pub fn impl_solidity_option<'a>(
	docs: &[String],
	name: &proc_macro2::Ident,
	enum_options: impl Iterator<Item = &'a syn::Variant> + Clone,
) -> proc_macro2::TokenStream {
	let variant_names = enum_options.clone().map(|opt| {
		let opt = &opt.ident;
		let s = name.to_string() + "." + opt.to_string().as_str();
		let as_string = proc_macro2::Literal::string(s.as_str());
		quote!(#name::#opt => #as_string,)
	});
	let solidity_name = name.to_string();

	let solidity_fields = enum_options.map(|v| {
		let docs = extract_docs(&v.attrs).expect("TODO: handle bad docs");
		let name = v.ident.to_string();
		quote! {
			SolidityEnumVariant {
				docs: &[#(#docs),*],
				name: #name,
			}
		}
	});

	quote!(
		#[cfg(feature = "stubgen")]
		impl ::evm_coder::solidity::SolidityEnumTy for #name {
			fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector) -> String {
				use evm_coder::solidity::*;
				use core::fmt::Write;
				let interface = SolidityEnum {
					docs: &[#(#docs),*],
					name: #solidity_name,
					fields: &[#(
						#solidity_fields,
					)*],
				};
				let mut out = String::new();
				let _ = interface.format(&mut out, tc);
				tc.collect(out);
				#solidity_name.to_string()
			}
			fn solidity_option(&self) -> &str {
				match self {
					#(#variant_names)*
				}
			}
		}
	)
}

pub fn impl_enum_from_u8<'a>(
	name: &proc_macro2::Ident,
	enum_options: impl Iterator<Item = &'a syn::Variant>,
) -> proc_macro2::TokenStream {
	let error_str = format!("value not convertible into enum \"{name}\"");
	let error_str = proc_macro2::Literal::string(&error_str);
	let enum_options = enum_options.enumerate().map(|(i, opt)| {
		let opt = &opt.ident;
		let n = proc_macro2::Literal::u8_suffixed(u8::try_from(i).expect("checked can cast"));
		quote! {#n => Ok(#name::#opt),}
	});

	quote!(
		impl TryFrom<u8> for #name {
			type Error = &'static str;

			fn try_from(value: u8) -> ::core::result::Result<Self, Self::Error> {
				const err: &'static str = #error_str;
				match value {
					#(#enum_options)*
					_ => Err(err)
				}
			}
		}
	)
}

pub fn impl_enum_abi_type(name: &syn::Ident) -> proc_macro2::TokenStream {
	quote! {
		impl ::evm_coder::abi::AbiType for #name {
			const SIGNATURE: ::evm_coder::custom_signature::SignatureUnit = <u8 as ::evm_coder::abi::AbiType>::SIGNATURE;
			const IS_DYNAMIC: bool = <u8 as ::evm_coder::abi::AbiType>::IS_DYNAMIC;
			const HEAD_WORDS: u32 = <u8 as ::evm_coder::abi::AbiType>::HEAD_WORDS;
		}
	}
}

pub fn impl_enum_abi_read(name: &syn::Ident) -> proc_macro2::TokenStream {
	quote!(
		impl ::evm_coder::abi::AbiDecode for #name {
			fn dec(reader: &mut ::evm_coder::abi::AbiDecoder) -> ::evm_coder::abi::Result<Self> {
				Ok(
					<u8 as ::evm_coder::abi::AbiDecode>::dec(reader)?
						.try_into()?
				)
			}
		}
	)
}

pub fn impl_enum_abi_write(name: &syn::Ident) -> proc_macro2::TokenStream {
	quote!(
		impl ::evm_coder::abi::AbiEncode for #name {
			fn enc(&self, writer: &mut ::evm_coder::abi::AbiEncoder) {
				::evm_coder::abi::AbiEncode::enc(&(*self as u8), writer);
			}
		}
	)
}

pub fn impl_enum_solidity_type_name(name: &syn::Ident) -> proc_macro2::TokenStream {
	quote!(
		#[cfg(feature = "stubgen")]
		impl ::evm_coder::solidity::SolidityTypeName for #name {
			fn solidity_name(
				writer: &mut impl ::core::fmt::Write,
				tc: &::evm_coder::solidity::TypeCollector,
			) -> ::core::fmt::Result {
				write!(writer, "{}", tc.collect_enum::<Self>())
			}

			fn is_simple() -> bool {
				true
			}

			fn solidity_default(
				writer: &mut impl ::core::fmt::Write,
				tc: &::evm_coder::solidity::TypeCollector,
			) -> ::core::fmt::Result {
				write!(writer, "{}", <#name as ::evm_coder::solidity::SolidityEnumTy>::solidity_option(&<#name>::default()))
			}
		}
	)
}

pub fn check_enum_fields(de: &syn::DataEnum) -> syn::Result<()> {
	for v in &de.variants {
		if !v.fields.is_empty() {
			return Err(syn::Error::new(
				v.ident.span(),
				"Enumeration parameters should not have fields",
			));
		} else if v.discriminant.is_some() {
			return Err(syn::Error::new(
				v.ident.span(),
				"Enumeration options should not have an explicit specified value",
			));
		}
	}

	Ok(())
}

pub fn check_repr_u8(name: &syn::Ident, attrs: &[syn::Attribute]) -> syn::Result<()> {
	let mut has_repr = false;
	for attr in attrs {
		if attr.path.is_ident("repr") {
			has_repr = true;
			let meta = attr.parse_meta()?;
			check_meta_u8(&meta)?;
		}
	}

	if !has_repr {
		return Err(syn::Error::new(name.span(), "Enum is not \"repr(u8)\""));
	}

	Ok(())
}

fn check_meta_u8(meta: &syn::Meta) -> Result<(), syn::Error> {
	if let syn::Meta::List(p) = meta {
		for nm in &p.nested {
			if let syn::NestedMeta::Meta(syn::Meta::Path(p)) = nm {
				if !p.is_ident("u8") {
					return Err(syn::Error::new(
						p.segments
							.first()
							.expect("repr segments are empty")
							.ident
							.span(),
						"Enum is not \"repr(u8)\"",
					));
				}
			}
		}
	}
	Ok(())
}

pub fn expand_enum(
	de: &syn::DataEnum,
	ast: &syn::DeriveInput,
) -> syn::Result<proc_macro2::TokenStream> {
	let name = &ast.ident;
	check_repr_u8(name, &ast.attrs)?;
	check_enum_fields(de)?;
	let docs = extract_docs(&ast.attrs)?;
	let enum_options = de.variants.iter();

	let from = impl_enum_from_u8(name, enum_options.clone());
	let solidity_option = impl_solidity_option(&docs, name, enum_options.clone());
	let abi_type = impl_enum_abi_type(name);
	let abi_read = impl_enum_abi_read(name);
	let abi_write = impl_enum_abi_write(name);
	let solidity_type_name = impl_enum_solidity_type_name(name);

	Ok(quote! {
		#from
		#solidity_option
		#abi_type
		#abi_read
		#abi_write
		#solidity_type_name
	})
}