1use crate::process::{CommandSpec, ProcessError};
8use clap::ValueEnum;
9use std::{
10 fmt,
11 path::{Path, PathBuf},
12 str::FromStr,
13};
14
15pub const BASE_DIR_NAME: &str = "base";
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
21#[clap(rename_all = "lowercase")]
22pub enum Language {
23 Rust,
25 CSharp,
27 Java,
29 Python,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct LanguageCommands {
36 pub init: Option<CommandSpec>,
38 pub build: Option<CommandSpec>,
40 pub run: CommandSpec,
42 pub run_fallback: Option<CommandSpec>,
44}
45
46impl Language {
47 pub const ALL: &'static [Self] = &[Self::Rust, Self::CSharp, Self::Java, Self::Python];
49
50 #[must_use]
53 pub const fn name(self) -> &'static str {
54 match self {
55 Self::Rust => "rust",
56 Self::CSharp => "csharp",
57 Self::Java => "java",
58 Self::Python => "python",
59 }
60 }
61
62 #[must_use]
65 pub fn base_file(self, config_dir: &Path) -> PathBuf {
66 config_dir.join(BASE_DIR_NAME).join(self.name())
67 }
68
69 #[must_use]
71 pub fn entry_file(self, project: &Path) -> PathBuf {
72 match self {
73 Self::Rust => project.join("src").join("main.rs"),
74 Self::CSharp => project.join("Program.cs"),
75 Self::Java => project.join("Main.java"),
76 Self::Python => project.join("main.py"),
77 }
78 }
79
80 pub fn commands(self, project: &Path) -> Result<LanguageCommands, ProcessError> {
88 let commands = match self {
89 Self::Rust => {
90 let manifest = project.join("Cargo.toml");
91 LanguageCommands {
92 init: Some(
93 CommandSpec::new("cargo")
94 .args(["init", "--bin"])
95 .arg(project),
96 ),
97 build: Some(
98 CommandSpec::new("cargo")
99 .args(["build", "--release", "--manifest-path"])
100 .arg(&manifest),
101 ),
102 run: CommandSpec::new("cargo")
103 .args(["run", "--release", "--quiet", "--manifest-path"])
104 .arg(&manifest),
105 run_fallback: None,
106 }
107 }
108 Self::CSharp => {
109 let name = project
110 .file_name()
111 .ok_or_else(|| ProcessError::NoDirectoryName {
112 path: project.to_path_buf(),
113 })?;
114
115 LanguageCommands {
116 init: Some(
117 CommandSpec::new("dotnet")
118 .args(["new", "console", "--name"])
119 .arg(name)
120 .arg("--output")
121 .arg(project),
122 ),
123 build: Some(
124 CommandSpec::new("dotnet")
125 .args(["build", "-c", "Release", "--nologo", "-v", "q"])
126 .arg(project),
127 ),
128 run: CommandSpec::new("dotnet")
129 .args(["run", "-c", "Release", "--no-build", "--project"])
130 .arg(project),
131 run_fallback: None,
132 }
133 }
134 Self::Java => LanguageCommands {
135 init: None,
136 build: Some(CommandSpec::new("javac").arg(self.entry_file(project))),
137 run: CommandSpec::new("java").arg("-cp").arg(project).arg("Main"),
138 run_fallback: None,
139 },
140 Self::Python => LanguageCommands {
141 init: None,
142 build: None,
143 run: CommandSpec::new("python3").arg(self.entry_file(project)),
144 run_fallback: Some(CommandSpec::new("python").arg(self.entry_file(project))),
145 },
146 };
147
148 Ok(commands.with_working_dir(project))
149 }
150}
151
152impl LanguageCommands {
153 fn with_working_dir(self, project: &Path) -> Self {
154 Self {
155 init: self.init.map(|spec| spec.current_dir(project)),
156 build: self.build.map(|spec| spec.current_dir(project)),
157 run: self.run.current_dir(project),
158 run_fallback: self.run_fallback.map(|spec| spec.current_dir(project)),
159 }
160 }
161}
162
163impl fmt::Display for Language {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 f.write_str(self.name())
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
171#[error("unknown language `{name}` (expected one of: rust, csharp, java, python)")]
172pub struct UnknownLanguage {
173 pub name: String,
175}
176
177impl FromStr for Language {
178 type Err = UnknownLanguage;
179
180 fn from_str(value: &str) -> Result<Self, Self::Err> {
181 Self::ALL
182 .iter()
183 .copied()
184 .find(|language| language.name() == value)
185 .ok_or_else(|| UnknownLanguage {
186 name: value.to_owned(),
187 })
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use std::ffi::OsStr;
195
196 fn project() -> PathBuf {
197 PathBuf::from("/aoc/2024/day07/rust")
198 }
199
200 fn args_of(spec: &CommandSpec) -> Vec<String> {
201 spec.arguments()
202 .iter()
203 .map(|arg| arg.to_string_lossy().into_owned())
204 .collect()
205 }
206
207 #[test]
208 fn every_variant_has_commands_and_a_unique_name() {
209 let mut names: Vec<_> = Language::ALL.iter().map(|l| l.name()).collect();
210 names.sort_unstable();
211 names.dedup();
212 assert_eq!(names.len(), Language::ALL.len());
213
214 for language in Language::ALL {
215 let commands = language
216 .commands(&project())
217 .expect("commands should build");
218 assert_eq!(commands.run.working_dir(), Some(project().as_path()));
219 }
220 }
221
222 #[test]
223 fn names_match_the_command_line_values_clap_accepts() {
224 for language in Language::ALL {
225 let value = language
226 .to_possible_value()
227 .expect("every variant is selectable");
228 assert_eq!(value.get_name(), language.name());
229 }
230 }
231
232 #[test]
233 fn parses_from_its_own_name() {
234 for language in Language::ALL {
235 assert_eq!(language.name().parse(), Ok(*language));
236 }
237 assert!("Rust".parse::<Language>().is_err());
238 assert!("c-sharp".parse::<Language>().is_err());
239 }
240
241 #[test]
242 fn rust_runs_the_optimized_build() {
243 let commands = Language::Rust
244 .commands(&project())
245 .expect("commands should build");
246
247 assert_eq!(commands.run.program(), "cargo");
248 let args = args_of(&commands.run);
249 assert!(args.contains(&"run".to_owned()), "{args:?}");
250 assert!(args.contains(&"--release".to_owned()), "{args:?}");
251 let manifest = project().join("Cargo.toml");
252 assert!(
253 args.contains(&manifest.to_string_lossy().into_owned()),
254 "{args:?}"
255 );
256 }
257
258 #[test]
259 fn csharp_names_the_project_after_its_directory() {
260 let commands = Language::CSharp
261 .commands(Path::new("/aoc/2024/day07/csharp"))
262 .expect("commands should build");
263 let init = commands.init.expect("csharp scaffolds with dotnet new");
264
265 assert_eq!(
266 args_of(&init),
267 [
268 "new",
269 "console",
270 "--name",
271 "csharp",
272 "--output",
273 "/aoc/2024/day07/csharp"
274 ]
275 );
276 }
277
278 #[test]
279 fn csharp_run_reuses_the_build_output() {
280 let commands = Language::CSharp
281 .commands(&project())
282 .expect("commands should build");
283
284 assert!(args_of(&commands.run).contains(&"--no-build".to_owned()));
285 assert!(
286 args_of(&commands.build.expect("csharp is compiled")).contains(&"Release".to_owned())
287 );
288 }
289
290 #[test]
291 fn a_project_path_without_a_directory_name_is_an_error() {
292 let error = Language::CSharp
293 .commands(Path::new("/"))
294 .expect_err("root has no directory name");
295
296 assert!(
297 matches!(error, ProcessError::NoDirectoryName { .. }),
298 "got {error:?}"
299 );
300 }
301
302 #[test]
303 fn python_is_interpreted_and_falls_back_to_python2_naming() {
304 let commands = Language::Python
305 .commands(Path::new("/aoc/2024/day07/python"))
306 .expect("commands should build");
307
308 assert!(commands.build.is_none());
309 assert!(commands.init.is_none());
310 assert_eq!(commands.run.program(), "python3");
311 assert_eq!(
312 commands.run_fallback.as_ref().map(CommandSpec::program),
313 Some(OsStr::new("python"))
314 );
315 }
316
317 #[test]
318 fn java_compiles_and_runs_the_main_class() {
319 let path = Path::new("/aoc/2024/day07/java");
320 let commands = Language::Java
321 .commands(path)
322 .expect("commands should build");
323
324 assert!(commands.init.is_none());
325 assert_eq!(
326 args_of(&commands.build.expect("java is compiled")),
327 [path.join("Main.java").to_string_lossy().into_owned()]
328 );
329 assert_eq!(
330 args_of(&commands.run),
331 ["-cp", "/aoc/2024/day07/java", "Main"]
332 );
333 }
334
335 #[test]
336 fn entry_and_base_files_follow_language_conventions() {
337 let config = Path::new("/home/u/.config/aoc");
338 let project = Path::new("/aoc/2024/day07/x");
339
340 let expected = [
341 (
342 Language::Rust,
343 "/aoc/2024/day07/x/src/main.rs",
344 "/home/u/.config/aoc/base/rust",
345 ),
346 (
347 Language::CSharp,
348 "/aoc/2024/day07/x/Program.cs",
349 "/home/u/.config/aoc/base/csharp",
350 ),
351 (
352 Language::Java,
353 "/aoc/2024/day07/x/Main.java",
354 "/home/u/.config/aoc/base/java",
355 ),
356 (
357 Language::Python,
358 "/aoc/2024/day07/x/main.py",
359 "/home/u/.config/aoc/base/python",
360 ),
361 ];
362
363 for (language, entry, base) in expected {
364 assert_eq!(language.entry_file(project), Path::new(entry));
365 assert_eq!(language.base_file(config), Path::new(base));
366 }
367 }
368}