Create basic `Output` proc-macro

finchie
Jan 31, 2024, 4:26 AM
UKFEFT6LSI4K7X6UHQFZYD52DILKXMZMYSO2UYS2FCHNPXIF4BEQC

Dependencies

Change contents

  • file addition: src (d--r------)
    [2.1]
  • file addition: lib.rs (----------)
    [0.15]
    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"),
    }
    }
  • file addition: examples (d--r------)
    [2.1]
  • file addition: basic.rs (----------)
    [0.3340]
    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!!"))
    }
  • file addition: Cargo.toml (----------)
    [2.1]
    [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"] }
  • file addition: Cargo.lock (----------)
    [2.1]
    # 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"
  • file addition: .ignore (----------)
    [2.1]
    .git
    .DS_Store
    # Added by cargo
    /target