spire_enum_macros 0.2.0

Procedural macros to facilitate enum usage, such as when delegating implementations, extracting variant types, or creating enum tables.
Documentation
use super::*;

pub fn run(input_stream: TokenStream1, enum_stream: TokenStream1) -> Result<TokenStream> {
	let table_attrs = parse_table_metas(input_stream)?;
	let input_enum = syn::parse::<Enum<SynMeta, SynMeta>>(enum_stream)?;
	let enum_def = input_enum.to_token_stream();

	let SaneEnum {
		ident: enum_ident,
		ty: enum_ty,
		variants,
	} = sanitize_enum(input_enum)?;

	let vis = Visibility::Public(Default::default());
	let lf = Lifetime::new("'_r", Span::call_site());
	let gen_t = Ident::new("Value", Span::call_site());

	let mod_ident = if let _Some(SettingModName { name, .. }) = table_attrs.mod_name {
		name
	} else {
		let mut enum_lower = enum_ident.to_string();
		enum_lower = enum_lower.to_case(Case::Snake);
		Ident::new(&format!("{enum_lower}_discriminant_table"), Span::call_site())
	};

	let table_ident = if let _Some(SettingTypeName { name, .. }) = table_attrs.ty_name {
		name
	} else {
		Ident::new(&format!("{enum_ident}DiscriminantTable"), Span::call_site())
	};

	let gen_params = quote! { <#gen_t> };
	let gen_args = &gen_params;
	let gen_lf_params = quote! { <#lf, #gen_t> };

	let table_ty = quote! { #table_ident::#gen_args };

	let fields = variants
		.iter()
		.map(|var| &var.table_field_ident)
		.collect::<Vec<_>>();

	const MACRO_LINK: &str = "spire_enum_macros::discriminant_generic_table";
	const DOCS_INTRO: &str = "This type was generated by an invocation of the macro [`discriminant_generic_table`](spire_enum_macros::discriminant_generic_table).";

	let table_def = {
		let attrs = table_attrs.syn_metas;

		let docs = docs_tokens(format!(
			"{DOCS_INTRO}\n\n\
             A fixed-size collection that contains exactly one value of the generic [`{gen_t}`] for each variant of [`{enum_ident}`].\n\
             Values can be accessed by calling [`get({enum_ident})`]({table_ident}::get) or [`get_mut`]({table_ident}::get_mut).\n\
             For a full list of all methods generated for this type, see the [macro]({MACRO_LINK}) documentation.\n\n\
             This type does not allocate heap memory(can be used in `no_std`), though no such guarantee is provided to the generic [`{gen_t}`]."
		));

		quote! {
			#docs
			#(#[#attrs])*
			#vis struct #table_ident #gen_params {
				#(pub #fields: #gen_t),*
			}
		}
	};

	let var_idents = variants.iter().map(|var| &var.ident).collect::<Vec<_>>();

	let length = variants.len();
	let table_impls = {
		let docs_new = docs_tokens(format!(
			"Constructs a new instance of the type.\n\n\
			 Since this table guarantees that it contains exactly one value for each variant of [`{enum_ident}`],\
			 all variants's values must be initialized during construction."
		));

		let from_fn_example = r###"
        ```rust
        use spire_enum_macros::discriminant_generic_table;
        
        #[discriminant_generic_table(ty_name = VolumeTable)]
        #[derive(Clone, Copy)]
        enum VolumeSetting {
            Main,
            Sfx,
            Music,
        }
        
        let default_values = VolumeTable::from_fn(
            |setting| { 
                match setting {
                    VolumeSetting::Main => 0.5,
                    VolumeSetting::Sfx => 1.0,
                    VolumeSetting::Music => 0.4,
                }
            }
        );
        
        assert_eq!(default_values[VolumeSetting::Main], 0.5);
        assert_eq!(default_values[VolumeSetting::Sfx], 1.0);
        assert_eq!(default_values[VolumeSetting::Music], 0.4);
        ```
        "###;

		let docs_from_fn = docs_tokens(format!(
			"Constructs a new instance of the type, invoking the closure `f` for every variant of [`{enum_ident}`].\n\n\
             # Example\n{from_fn_example}"
		));

		let docs_into_iter = docs_tokens(format!(
			"Convert this table into an iterator that yields all values of the generic [`{gen_t}`], that were mapped to each variant of [`{enum_ident}`].\n\
			 - This iterator yields tuples of `({enum_ident}, {gen_t})`, where the first element represents the variant that the second element was associated with.\n\
			 - It is guaranteed that values will be yielded in the exact order they were declared in [`{enum_ident}`] (Top to bottom).\n\
             - The iterator returned does not allocate heap memory, it is merely a fixed-size array with length known at compile time."
		));

		let docs_iter = docs_tokens(format!(
			"Iterates through references of all values of the generic [`{gen_t}`], that are mapped to each variant of [`{enum_ident}`].\n\
             - This iterator yields tuples of `({enum_ident}, &{gen_t})`, where the first element represents the variant that the second element is associated with.\n\
             - It is guaranteed that values will be yielded in the exact order they were declared in [`{enum_ident}`] (Top to bottom).\n\
             - The iterator returned does not allocate heap memory, it is merely a fixed-size array with length known at compile time."
		));

		let docs_iter_mut = docs_tokens(format!(
			"Iterates through mutable references of all values of the generic [`{gen_t}`], that are mapped to each variant of [`{enum_ident}`].\n\
             - This iterator yields tuples of `({enum_ident}, &mut {gen_t})`, where the first element represents the variant that the second element is associated with.\n\
             - It is guaranteed that values will be yielded in the exact order they were declared in [`{enum_ident}`] (Top to bottom).\n\
             - The iterator returned does not allocate heap memory, it is merely a fixed-size array with length known at compile time."
		));

		quote! {
			#[allow(clippy::too_many_arguments)]
			impl #gen_params #table_ty {
				#docs_new
				pub const fn new( #(#fields: #gen_t),* ) -> Self {
					Self { #(#fields),* }
				}

				#[doc = "Constructs a new instance of the type, populating every variant's value with the parameter `__val`"]
				pub fn filled_with(__val: #gen_t) -> Self where #gen_t: Clone {
					Self { #(#fields: __val.clone()),* }
				}

				#docs_from_fn
				pub fn from_fn(mut f: impl FnMut(#enum_ty) -> #gen_t) -> Self {
					Self { #(#fields: f(#enum_ty::#var_idents)),* }
				}

				#[doc = "Returns a reference to the value associated with the variant `var` that is always present in this table.\n\n\
				This method will never panic, it is guaranteed at compile time that the table contains the value associated with `var`."]
				pub const fn get(&self, var: #enum_ty) -> & #gen_t {
					match var {
						#(#enum_ty::#var_idents {..} => &self.#fields),*
					}
				}

				#[doc = "Returns a mutable reference to the value associated with the variant `var` that is always present in this table.\n\n\
				This method will never panic, it is guaranteed at compile time that the table contains the value associated with `var`."]
				pub const fn get_mut(&mut self, var: #enum_ty) -> &mut #gen_t {
					match var {
						#(#enum_ty::#var_idents {..} => &mut self.#fields),*
					}
				}

				#[allow(clippy::needless_lifetimes)]
				#docs_iter
				pub fn iter<#lf>(&#lf self) -> core::array::IntoIter<(#enum_ty, &#lf #gen_t), #length> {
					[
						#((#enum_ty::#var_idents, &self.#fields)),*
					].into_iter()
				}

				#[allow(clippy::needless_lifetimes)]
				#docs_iter_mut
				pub fn iter_mut<#lf>(&#lf mut self) -> core::array::IntoIter<(#enum_ty, &#lf mut #gen_t), #length> {
					[
						#((#enum_ty::#var_idents, &mut self.#fields)),*
					].into_iter()
				}
			}

			impl #gen_params IntoIterator for #table_ty {
				type Item = (#enum_ty, #gen_t);
				type IntoIter = core::array::IntoIter<(#enum_ty, #gen_t), #length>;

				#docs_into_iter
				fn into_iter(self) -> Self::IntoIter {
					[
						#((#enum_ty::#var_idents, self.#fields) ),*
					].into_iter()
				}
			}

			impl #gen_lf_params IntoIterator for &#lf #table_ty {
				type Item = (#enum_ty, &#lf #gen_t);
				type IntoIter = core::array::IntoIter<Self::Item, #length>;

				#[doc = "See [`iter`](Self::iter)"]
				fn into_iter(self) -> Self::IntoIter { self.iter() }
			}

			impl #gen_lf_params IntoIterator for &#lf mut #table_ty {
				type Item = (#enum_ty, &#lf mut #gen_t);
				type IntoIter = core::array::IntoIter<Self::Item, #length>;

				#[doc = "See [`iter_mut`](Self::iter_mut)"]
				fn into_iter(self) -> Self::IntoIter { self.iter_mut() }
			}

			impl #gen_params std::ops::Index<#enum_ty> for #table_ty {
				type Output = #gen_t;

				#[doc = "See [`get`](Self::get)"]
				fn index(&self, index: #enum_ty) -> &Self::Output {
					self.get(index)
				}
			}

			impl #gen_params std::ops::IndexMut<#enum_ty> for #table_ty {
				#[doc = "See [`get_mut`](Self::get_mut)"]
				fn index_mut(&mut self, index: #enum_ty) -> &mut Self::Output {
					self.get_mut(index)
				}
			}
		}
	};

	let from_const_fn_macro = {
		let macro_ident = {
			let mut str = table_ident.to_string();
			str = str.to_case(Case::Snake);
			Ident::new(&format!("{str}_from_const_fn"), Span::call_site())
		};

		quote! {
			macro_rules! #macro_ident {
				( | $var: ident | $( -> $ret: ty )? $closure: block ) => {{
					#table_ident {
						#(
							#fields: {
								let $var = #enum_ty::#var_idents;
								$closure
							}
						),*
					}
				}};

				( |_| $( -> $ret: ty )? $closure: block ) => {{
					#table_ident {
						#( #fields: $closure ),*
					}
				}};
			}
		}
	};

	Ok(quote! {
		#enum_def

		pub(crate) use #mod_ident::#table_ident;

		mod #mod_ident {
			use super::#enum_ident;

			#table_def
			#table_impls
			#from_const_fn_macro
		}
	})
}

struct SaneEnum {
	ident: Ident,
	ty: Type,
	variants: Vec<SaneVariant>,
}

struct SaneVariant {
	ident: Ident,
	table_field_ident: Ident,
}

fn sanitize_enum(input: Enum<SynMeta, SynMeta>) -> Result<SaneEnum> {
	let Enum {
		attrs: _,
		vis: _,
		enum_token: _,
		ident,
		generics,
		where_clause,
		variants,
	} = input;

	let generics = sanitize_generics(generics, where_clause)?;
	let ty = new_ty_maybe_generic(&ident, &generics);

	let variants = variants
		.into_inner()
		.into_iter()
		.map(|Var { ident, fields, .. }| {
			const HELP: &str =
				"A discriminants table can only be generated if all variants are units (have 0 fields).";

			match fields {
				VarFields::Named(named) => {
					if named.is_empty() {
						Ok(SaneVariant {
							table_field_ident: var_to_field_ident(&ident),
							ident,
						})
					} else {
						bail!(named => HELP)
					}
				}
				VarFields::Unnamed(unnamed) => {
					if unnamed.is_empty() {
						Ok(SaneVariant {
							table_field_ident: var_to_field_ident(&ident),
							ident,
						})
					} else {
						bail!(unnamed => HELP)
					}
				}
				VarFields::Unit => Ok(SaneVariant {
                    table_field_ident: var_to_field_ident(&ident),
                    ident,
                }),
			}
		})
		.try_collect()?;

	Ok(SaneEnum {
		ident,
		variants,
		ty,
	})
}