Experimenting with more structured ways to handle command-line input/output in Rust
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Output)]
pub fn output(input: TokenStream) -> TokenStream {
    let DeriveInput { ident, data, .. } = parse_macro_input!(input);

    match data {
        syn::Data::Struct(struct_data) => {
            let builder_ident = format_ident!("{ident}Builder");

            // Get all field identifiers in the struct
            // Each field is assumed to have a unique identifier (Rust should enforce this?)
            let field_idents = struct_data
                .fields
                .iter()
                .map(|field| field.ident.clone().unwrap()); // TODO: handle tuple structs

            let types = struct_data.fields.iter().map(|field| field.ty.clone());
            let ident_type_mappings = field_idents.clone().zip(types.clone());

            // In the builder struct definition, map each field from `field: T` to `field: Option<T>`
            let optional_mappings = ident_type_mappings
                .clone()
                .map(|(field_ident, ty)| quote! { #field_ident: Option<#ty> });

            // Functions to handle incoming data
            // In the future these functions will include logic for immediate output to stdout (human output),
            // just lazily setting (machine output), or something else (maybe test handler?)
            // TODO: investigate setting collections, e.g. have the setter extend rather than set the collection so that
            // it can be immmediately output rather than to buffer all items in collection
            let setter_functions = ident_type_mappings.clone().map(|(field_ident, ty)| {
                quote! { fn #field_ident(&mut self, value: #ty) -> &mut Self {
                    assert!(self.#field_ident.is_none());
                    self.#field_ident = Some(value);

                    self
                }}
            });

            // Simple getter functions to inspect the value set in the builder
            let getter_functions = field_idents
                .clone()
                .map(|field_ident| format_ident!("get_{field_ident}"))
                .zip(field_idents.clone())
                .zip(types.clone())
                .map(|((function_ident, field_ident), ty)| {
                    quote! { fn #function_ident(&self) -> &Option<#ty> {
                        &self.#field_ident
                    }}
                });

            // Construct the builder implementation and return the token stream
            quote! {
                struct #builder_ident {
                    #(#optional_mappings),*
                }

                impl #builder_ident {
                    const fn new() -> Self {
                        Self {
                            #(#field_idents: None),*
                        }
                    }

                    #(#setter_functions)*
                    #(#getter_functions)*
                }

                impl #ident {
                    pub const fn new() -> #builder_ident {
                        #builder_ident::new()
                    }
                }

            }
            .into()
        }
        _ => todo!("Handle deriving non-structs"),
    }
}