From 313c6f4e3bbca0a2785713845ee4779933c43160 Mon Sep 17 00:00:00 2001 From: Sebastian Tobie Date: Thu, 19 Dec 2024 09:17:37 +0100 Subject: [PATCH] added a new macro include_aoc! this is to replace the include_data! and include_example macros. It features partitioned inputs and unified interface --- macros/src/lib.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index d3c2c8c..30eb54e 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,8 +1,8 @@ -use std::fs; +use std::fs::{self, read_to_string}; use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse, Result}; +use syn::{parse, LitBool, Result}; use syn::{ parse::{Parse, ParseStream}, spanned::Spanned, @@ -152,3 +152,101 @@ pub fn include_example(data: TokenStream) -> TokenStream { } .into() } + +#[derive(Debug, Clone, Default)] +struct NewIncludeData { + var: String, + year: String, + day: String, + example: bool, + parts: usize, +} + +impl Parse for NewIncludeData { + fn parse(input: ParseStream) -> Result { + let mut v = Self::default(); + v.var = if input.peek(Ident) && !input.peek(LitInt) { + get_text(input.parse::().unwrap())? + } else { + VAR_NAME.to_string() + }; + v.year = get_text(input.parse::()?)?; + v.day = get_text(input.parse::()?)?; + if input.peek(LitBool) { + v.example = input.parse::()?.value; + } + if input.peek(LitInt) { + v.parts = input.parse::()?.base10_parse()?; + } + if v.parts == 0 { + v.parts = 1; + } + Ok(v) + } +} + +fn fallback(input: NewIncludeData, message: String) -> TokenStream { + if input.parts != 1 { + let postfixes = (1..=input.parts).map(|i| format_ident!("{}_{}", input.var, i)); + quote! { + #( + /// + const #postfixes: &str = ""; + )* + } + .into() + } else { + let name = format_ident!("{}", input.var); + quote! { + /// #name + const #name: &str = ""; + } + .into() + } +} + +/// Macro to include data and examples. +/// The First Argument(Optional) is the name/prefix for the constant, +/// The Second is the Year +/// The Third is the day/filename without the .txt at the end +/// the fourth is if its an example or not +/// and the last is the amount of parts in the file, if it is greater than 1 the first argument is treated as an Prefix. +#[proc_macro] +pub fn include_aoc(data: TokenStream) -> TokenStream { + let input = match parse::(data) { + Ok(data) => data, + Err(error) => return error.into_compile_error().into(), + }; + let mut content = Default::default(); + if !input.example { + let path = format!("{}/{}/{}.txt", DATA_DIR, input.year, input.day); + match fs::exists(path.clone()) { + Ok(true) => content = read_to_string(path).unwrap(), + Ok(false) => {}, + Err(error) => return fallback(input, format!("Failed to read file: {}", error)), + }; + } + if content.is_empty() { + let path = format!("{}/{}/{}.txt", EXAMPLE_DIR, input.year, input.day); + content = match fs::exists(path.clone()) { + Ok(true) => read_to_string(path).unwrap(), + Ok(false) => Default::default(), + Err(error) => return fallback(input, format!("Failed to read file: {}", error)), + }; + } + if input.parts != 1 { + let parts = content.splitn(input.parts, "\n\n"); + let suffix = (1..=input.parts).map(|i| format_ident!("{}_{}", input.var, i)); + quote! { + #( + const #suffix: &str = #parts; + )* + } + } else { + let name = format_ident!("{}", input.var); + quote! { + const #name: &str = #content; + } + } + .into() +}