aocd_proc/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::parse::{Parse, ParseStream, Result};
4use syn::{parse_macro_input, Expr, LitInt, Token};
5
6struct ClientArgs {
7    year: u16,
8    day: u8,
9    test_input_file: Option<String>,
10}
11
12impl Parse for ClientArgs {
13    fn parse(input: ParseStream) -> Result<Self> {
14        let help_text = format!(
15            "Provide a year and a day, e.g. #[aocd({})]",
16            chrono::Utc::now().format("%Y, %d")
17        );
18
19        let year = input
20            .parse::<LitInt>()
21            .unwrap_or_else(|_| panic!("Expected a literal year. {help_text}"))
22            .base10_parse::<u16>()?;
23        input
24            .parse::<Token![,]>()
25            .unwrap_or_else(|_| panic!("Expected 2 arguments. {help_text}"));
26        let day = input
27            .parse::<LitInt>()
28            .unwrap_or_else(|_| panic!("Expected a literal day. {help_text}"))
29            .base10_parse::<u8>()?;
30
31        let mut test_input_file = None;
32        if input.parse::<Token![,]>().is_ok() {
33            if let Ok(file_name) = input.parse::<syn::LitStr>() {
34                assert!(
35                    std::fs::metadata(file_name.value()).is_ok(),
36                    "Test file {} does not exist",
37                    file_name.value()
38                );
39                test_input_file = Some(file_name.value());
40            }
41        }
42
43        Ok(ClientArgs {
44            year,
45            day,
46            test_input_file,
47        })
48    }
49}
50
51struct SubmitArgs {
52    part: Expr,
53    answer: Expr,
54}
55
56impl Parse for SubmitArgs {
57    fn parse(input: ParseStream) -> Result<Self> {
58        let part = input.parse::<Expr>()?;
59
60        // If the expr is a literal integer, give an error if it isn't 1 or 2.
61        if let Expr::Lit(part_lit) = &part {
62            if let syn::Lit::Int(part_int) = &part_lit.lit {
63                if let Ok(part) = part_int.base10_parse::<i64>() {
64                    assert!(part == 1 || part == 2, "Part should be 1 or 2, not {part}",);
65                }
66            }
67        }
68
69        input.parse::<Token![,]>()?;
70        let answer: Expr = input.parse()?;
71        Ok(SubmitArgs { part, answer })
72    }
73}
74
75/// Annotate your main function with `#[aocd(year, day)]`.
76///
77/// This sets up your main function so that you can use the `aocd::input!` and `aocd::submit!` macros.
78///
79/// You can optionally provide a third argument, a file name. If you do, this is treated as a
80/// test-input, containing a smaller input that you want to test your code on before submitting.
81/// In this case, the `aocd::input!` macro will read the input from that file instead of fetching
82/// it from the website, and the `aocd::submit!` macro will just be a println alias.
83///
84/// # Example
85/// ```ignore
86/// use aocd::*;
87///
88/// #[aocd(2015, 1)]
89/// fn main() {
90///    let part_1_answer = input!().lines().len();
91///    submit!(1, part_1_answer);
92/// }
93/// ```
94///
95/// ```ignore
96/// use aocd::prelude::*;  // Same as `use aocd::*;', but clippy allows it.
97///
98/// #[aocd(2015, 1, "test_input.txt")]
99/// fn main() {
100///    let part_1_answer = input!().lines().len();  // Reads from test_input.txt
101///    submit!(1, part_1_answer);  // Just prints the answer, doesn't submit it.
102/// }
103/// ```
104///
105/// # Panics
106/// Panics (i.e. surfaces a compile error) if the arguments are not two integers in the expected ranges,
107/// or if the optional third argument is not a string literal containing a valid file name.
108#[proc_macro_attribute]
109pub fn aocd(attr: TokenStream, input: TokenStream) -> TokenStream {
110    let args = parse_macro_input!(attr as ClientArgs);
111    let year = args.year;
112    let day = args.day;
113    let test_input_file = args.test_input_file;
114
115    // When https://github.com/rust-lang/rust/issues/54140 is closed, use that to get nicer error messages.
116    assert!(
117        year >= 2015,
118        "The first Advent of Code was in 2015, not {year}.",
119    );
120    assert!(
121        (1..=25).contains(&day),
122        "Chose a day from 1 to 25, not {day}.",
123    );
124
125    let mut fn_item: syn::ItemFn = syn::parse(input).unwrap();
126    if let Some(test_input_file) = test_input_file {
127        fn_item.block.stmts.insert(
128            0,
129            syn::parse(
130                quote!( let __aocd_client = aocd::Aocd::new(#year, #day, Some(#test_input_file));)
131                    .into(),
132            )
133            .unwrap(),
134        );
135    } else {
136        fn_item.block.stmts.insert(
137            0,
138            syn::parse(quote!( let __aocd_client = aocd::Aocd::new(#year, #day, None);).into())
139                .unwrap(),
140        );
141    }
142
143    TokenStream::from(quote!(#fn_item))
144}
145
146/// Returns the puzzle input as a String: `input!()`.
147///
148/// This must be used within a function annotated with `#[aocd(year, day)]`.
149///
150/// If you provide a file name in the function annotation, it will read the input from that file instead of fetching it from the website.
151/// This can be useful for testing with a smaller input, like the example input given in the puzzle description.
152#[proc_macro]
153pub fn input(_: TokenStream) -> TokenStream {
154    TokenStream::from(quote!(__aocd_client.get_input()))
155}
156
157/// Submit an answer for the given part: `submit!(part, answer)`.
158///
159/// This must be used within a function annotated with `#[aocd(year, day)]`.
160///
161/// If you provide a file name in the function annotation, this just prints the answer without
162/// submitting it to Advent of Code.
163#[proc_macro]
164pub fn submit(args: TokenStream) -> TokenStream {
165    let args = parse_macro_input!(args as SubmitArgs);
166    let part = args.part;
167    let answer = args.answer;
168    TokenStream::from(quote!(__aocd_client.submit(#part, #answer)))
169}