aoc_auto/
lib.rs

1//! This crate is used to generate the auto_import.rs and mod.rs files for each year/day/part.
2
3use quote::quote;
4use std::{
5    fs::File,
6    io::prelude::*,
7    path::{Path, PathBuf},
8};
9
10pub mod template;
11
12/// Run this function to generate the auto_import.rs and mod.rs files for each year folder and day file.
13/// 
14/// # Example
15/// 
16/// ```rust
17/// use aoc_auto::aoc_auto;
18/// fn main() {
19///    aoc_auto();
20/// }
21/// ```
22pub fn aoc_auto() {
23    // get years to make mod files for from src/, each folder is a year formatted as y20XX/
24    let years: Vec<String> = Path::new("src/")
25        .read_dir()
26        .unwrap()
27        .map(|e| e.unwrap())
28        .filter(|e| {
29            e.path().is_dir() &&
30            e.file_name().to_str().unwrap().starts_with("y") &&
31            // every character after y is a digit
32            e.file_name().to_str().unwrap().chars().skip(1).all(|c| c.is_digit(10))
33        })
34        .map(|e| e.file_name().to_str().unwrap().to_owned())
35        .collect();
36    // for each year, get days to include into the mod files for, each day is formatted as dX.rs
37    for year in &years {
38        let days: Vec<String> = Path::new("src/")
39            .join(year.clone())
40            .read_dir()
41            .unwrap()
42            .map(|e| e.unwrap())
43            .filter(|e| {
44                let filename = e.file_name().into_string().unwrap();
45                e.path().is_file() && filename.starts_with("d") && filename.ends_with(".rs") && 
46                // every character after d is a digit except for the file extension
47                filename.replace(".rs", "").chars().skip(1).all(|c| c.is_digit(10))
48            })
49            .map(|e| e.file_name().to_str().unwrap().to_owned())
50            .collect();
51
52        let days_expr: Vec<syn::Expr> = days
53            .iter()
54            .map(|e| {
55                let d = e.replace(".rs", "");
56                syn::parse_str::<syn::Expr>(&d).unwrap()
57            })
58            .collect();
59        let days_num_expr: Vec<syn::Expr> = days
60            .iter()
61            .map(|e| e.replace("d", "").replace(".rs", ""))
62            .map(|e| syn::parse_str::<syn::Expr>(&e).unwrap())
63            .collect();
64        let mod_code = quote! {
65            //! Auto-generated file by build script, do not edit!
66            #(pub mod #days_expr;)*
67
68            /// Selects the function for the given day and part
69            pub fn select_function(day: u32, part: u32) -> Result<fn(String) -> String, String> {
70                match day {
71                    #(#days_num_expr =>
72                        match part {
73                            1 => Ok(#days_expr::part1),
74                            2 => Ok(#days_expr::part2),
75                            _ => Err("Invalid part!".into()),
76                        }
77                    ),*
78                    _ => Err("Invalid day!".into()),
79                }
80            }
81        };
82
83        let mut mod_file_path = Path::new("src/").join(year).join("mod.rs");
84        write_and_format(mod_code.to_string(), &mut mod_file_path);
85    }
86
87    let years_expr: Vec<syn::Expr> = years
88        .iter()
89        .map(|e| syn::parse_str::<syn::Expr>(&e).unwrap())
90        .collect();
91    let auto_import_file = Path::new("src/auto_import.rs").to_owned();
92    let years_mod: Vec<String> = years.iter().map(|e| format!("{}/mod.rs", e)).collect();
93    let years_num_expr: Vec<syn::Expr> = years
94        .iter()
95        .map(|e| e.replace("y", ""))
96        .map(|e| syn::parse_str::<syn::Expr>(&e).unwrap())
97        .collect();
98
99    let auto_import_code = quote! {
100        //! Auto-generated file by build script, do not edit!
101        #(
102            #[path = #years_mod]
103            pub mod #years_expr;
104        )*
105        /// Selects the function for the given year, day, and part
106        pub fn select_function(year: u32, day: u32, part: u32) -> Result<fn(String) -> String, String> {
107            match year {
108                #(#years_num_expr => Ok(#years_expr::select_function(day, part)?),)*
109                _ => Err("Invalid year!".into()),
110            }
111        }
112    };
113
114    write_and_format(auto_import_code.to_string(), &auto_import_file)
115}
116
117fn write_and_format(file: String, path: &PathBuf) {
118    let syntax_tree = syn::parse_file(&file).unwrap();
119    let text = prettyplease::unparse(&syntax_tree);
120    let mut file: File = File::create(&path).unwrap();
121    file.write_all(text.as_bytes()).unwrap();
122}
123
124/// Automatically fill empty day files with the template.
125/// # Usage
126/// Simply call this function in your build script to automatically fill empty day files with the template.
127pub fn auto_template() {
128    // Get years to check for empty day files
129    let years: Vec<String> = Path::new("src/")
130        .read_dir()
131        .unwrap()
132        .map(|e| e.unwrap())
133        .filter(|e| {
134            e.path().is_dir() &&
135            e.file_name().to_str().unwrap().starts_with("y") &&
136            // every character after y is a digit
137            e.file_name().to_str().unwrap().chars().skip(1).all(|c| c.is_digit(10))
138        })
139        .map(|e| e.file_name().to_str().unwrap().to_owned())
140        .collect();
141
142    // For each year, check day files
143    for year in &years {
144        let days: Vec<PathBuf> = Path::new("src/")
145            .join(year.clone())
146            .read_dir()
147            .unwrap()
148            .map(|e| e.unwrap())
149            .filter(|e| {
150                let filename = e.file_name().into_string().unwrap();
151                e.path().is_file() && filename.starts_with("d") && filename.ends_with(".rs") && 
152                // every character after d is a digit except for the file extension
153                filename.replace(".rs", "").chars().skip(1).all(|c| c.is_digit(10))
154            })
155            .map(|e| e.path())
156            .collect();
157
158        // Check each day file
159        for day_path in days {
160            // Check if file is empty
161            let metadata = std::fs::metadata(&day_path).unwrap();
162            if metadata.len() == 0 {
163                // File is empty, fill with template
164                let day_name = day_path.file_name().unwrap().to_str().unwrap();
165                let day_number = day_name.replace("d", "").replace(".rs", "");
166                
167                // Parse year and day as numbers
168                let year_number = year.replace("y", "");
169                let year_num: u32 = year_number.parse().unwrap_or(0);
170                let day_num: u32 = day_number.parse().unwrap_or(0);
171                
172                // Try to get title from website, fall back to default if unavailable
173                let title = get_title(year_num, day_num)
174                    .unwrap_or_else(|| format!("--- Day {} ---", day_number));
175                
176                // Use template data with real title if available
177                let template_data = template::TemplateData {
178                    title,
179                    ..Default::default()
180                };
181                
182                // Generate template content
183                let template_content = template::create_template(template_data);
184                
185                // Write template to file
186                let mut file = File::create(&day_path).unwrap();
187                file.write_all(template_content.as_bytes()).unwrap();
188                
189                println!("Filled empty file: {:?}", day_path);
190            }
191        }
192    }
193}
194
195fn get_title(year: u32, day: u32) -> Option<String> {
196    // Construct the URL for the day's challenge
197    let url = format!("https://adventofcode.com/{}/day/{}", year, day);
198    
199    // Make an HTTP request to the URL
200    let body = match reqwest::blocking::get(&url) {
201        Ok(response) => {
202            if !response.status().is_success() {
203                return None;
204            }
205            match response.text() {
206                Ok(text) => text,
207                Err(_) => return None,
208            }
209        }
210        Err(_) => return None,
211    };
212    
213    // Parse the HTML
214    let document = scraper::Html::parse_document(&body);
215    
216    // Select the article with class "day-desc"
217    let article_selector = match scraper::Selector::parse("article.day-desc") {
218        Ok(selector) => selector,
219        Err(_) => return None,
220    };
221    
222    // Select the first h2 within the article
223    let h2_selector = match scraper::Selector::parse("h2") {
224        Ok(selector) => selector,
225        Err(_) => return None,
226    };
227    
228    // Extract the title
229    document.select(&article_selector)
230        .next()
231        .and_then(|article| article.select(&h2_selector).next())
232        .map(|h2| {
233            // Get the text content of the h2 tag
234            let title = h2.text().collect::<Vec<_>>().join("");
235            // Return the full title including the "---" parts, with a space prefix
236            format!(" {}", title)
237        })
238}