Mostly just learning how to write proc-macros, this code by itself isn't really useful as it just implements half of the builder pattern (no finish() method yet), but hopefully will be extended to make some pretty cool things :)
UKFEFT6LSI4K7X6UHQFZYD52DILKXMZMYSO2UYS2FCHNPXIF4BEQC
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"),
}
}
use cli_framework::Output;
#[derive(Output)]
struct SimpleOutput {
first: usize,
second: String,
}
fn main() {
// Create builder (auto-generated by #[derive(Output)])
let mut builder = SimpleOutput::new();
dbg!(builder.get_first()); // Empty (None)
builder.first(2); // Set to Some(2)
dbg!(builder.get_first()); // Full (Some(2))
dbg!(builder.get_second()); // Empty (None)
builder.second(String::from("It works!!")); // Set to Some("It works!!")
dbg!(builder.get_second()); // Full (Some("It works!!"))
}
[package]
name = "cli_framework"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[lints.clippy]
all = "deny"
pedantic = "warn"
nursery = "warn"
cargo = "warn"
[dependencies]
proc-macro2 = "1.0.78"
quote = "1.0.35"
syn = { version = "2.0.48", features = ["full"] }
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "cli_framework"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
.git
.DS_Store
# Added by cargo
/target