1use std::{collections::HashMap, io};
2
3use anyhow::Context as _;
4use clap::{CommandFactory as _, FromArgMatches as _};
5use clap_complete::generate as generate_completions;
6use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder};
7use itertools::Itertools;
8use lazy_format::lazy_format;
9use typeshare_model::prelude::{CrateName, FilesMode, Language};
10
11use crate::{
12 args::{add_lang_argument, add_language_params_to_clap, Command, OutputLocation, StandardArgs},
13 config::{
14 self, compute_args_set, load_config, load_language_config_from_file_and_args, CliArgsSet,
15 },
16 parser::{parse_input, parser_inputs, ParsedData},
17 writer::write_output,
18};
19
20pub struct PersonalizeClap {
21 pub name: &'static str,
22 pub version: &'static str,
23 pub author: &'static str,
24 pub about: &'static str,
25}
26
27pub trait LanguageSet<'config> {
28 type LanguageMetas: 'static;
29
30 fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas>;
36
37 fn add_lang_argument(command: clap::Command) -> clap::Command;
40
41 fn add_language_specific_arguments(
43 command: clap::Command,
44 metas: &Self::LanguageMetas,
45 ) -> clap::Command;
46
47 fn execute_typeshare_for_language(
48 language: &str,
49 config: &'config config::Config,
50 args: &'config clap::ArgMatches,
51 metas: &Self::LanguageMetas,
52 data: HashMap<Option<CrateName>, ParsedData>,
53 destination: &OutputLocation<'_>,
54 ) -> anyhow::Result<()>;
55}
56
57macro_rules! metas {
58 ([$($CliArgsSet:ident)*]) => {
59 ($($CliArgsSet,)*)
60 };
61
62 ([$($CliArgsSet:ident)*] $Language:ident $($Tail:ident)*) => {
63 metas! {[$($CliArgsSet)* CliArgsSet] $($Tail)*}
64 };
65}
66
67macro_rules! language_set_for {
68 ($Language:ident $($Tail:ident)*) => {
69 language_set_for! {
70 [$Language] $($Tail)*
71 }
72 };
73
74 ([$($Language:ident)*] $Head:ident $($Tail:ident)*) => {
75 language_set_for! {[$($Language)*]}
76
77 language_set_for! {
78 [$($Language)* $Head] $($Tail)*
79 }
80 };
81
82 ([$($Language:ident)+]) => {
83 impl<'config, $($Language,)*> LanguageSet<'config> for ($($Language,)*)
84 where $(
85 $Language: Language<'config>,
86 )*
87 {
88 type LanguageMetas = metas!([] $($Language)*);
89
90 fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas> {
91 Ok(
92 ($(
93 compute_args_set::<$Language>()?,
94 )*),
95 )
96 }
97
98 fn add_lang_argument(command: clap::Command)->clap::Command {
99 add_lang_argument(command, &[$(<$Language as Language>::NAME,)*])
100 }
101
102 fn add_language_specific_arguments(
103 command: clap::Command,
104 metas: &Self::LanguageMetas,
105 ) -> clap::Command {
106 #[allow(non_snake_case)]
107 let ($($Language,)*) = metas;
108
109 $(
110 let command = add_language_params_to_clap(
111 command,
112 <$Language as Language>::NAME,
113 $Language
114 );
115 )*
116
117 command
118 }
119
120 fn execute_typeshare_for_language(
121 language: &str,
122 config: &'config config::Config,
123 args: &'config clap::ArgMatches,
124 metas: &Self::LanguageMetas,
125 data: HashMap<Option<CrateName>, ParsedData>,
126 destination: &OutputLocation<'_>,
127 ) -> anyhow::Result<()> {
128 #[allow(non_snake_case)]
129 let ($($Language,)*) = metas;
130
131 $(
132 if language == <$Language as Language>::NAME {
133 execute_typeshare_for_language::<$Language>(
134 config,
135 args,
136 $Language,
137 data,
138 destination
139 )
140 } else
141 )*
142 {
143 anyhow::bail!("{language} isn't a valid language; clap should have prevented this")
144 }
145 }
146 }
147 }
148}
149
150fn execute_typeshare_for_language<'config, 'a, L: Language<'config>>(
151 config: &'config config::Config,
152 args: &'config clap::ArgMatches,
153 meta: &'a CliArgsSet,
154 data: HashMap<Option<CrateName>, ParsedData>,
155 destination: &OutputLocation<'_>,
156) -> anyhow::Result<()> {
157 let name = L::NAME;
158
159 let config = load_language_config_from_file_and_args::<L>(&config, &args, meta)
160 .with_context(|| format!("failed to load configuration for language {name}"))?;
161
162 let language_implementation = <L>::new_from_config(config)
163 .with_context(|| format!("failed to load configuration for language {name}"))?;
164
165 write_output(&language_implementation, data, destination)
166 .with_context(|| format!("failed to generate typeshared code for language {name}"))?;
167
168 Ok(())
169}
170
171language_set_for! {
174 A B C D
175 E F G H
176 I J K L
177 M N O P
178}
179
180pub trait LanguageHelper {
184 type LanguageSet<'config>: LanguageSet<'config>;
185}
186
187pub fn main_body<Helper>() -> anyhow::Result<()>
188where
189 Helper: LanguageHelper,
190{
191 let language_metas = Helper::LanguageSet::compute_language_metas()?;
192 let command = StandardArgs::command();
193
194 let command = Helper::LanguageSet::add_lang_argument(command);
201 let command = Helper::LanguageSet::add_language_specific_arguments(command, &language_metas);
202
203 let args = command.clone().get_matches();
206
207 let standard_args = StandardArgs::from_arg_matches(&args)
211 .expect("StandardArgs should always be loadable from a `command`");
212
213 if let Some(options) = standard_args.subcommand {
215 match options {
216 Command::Completions { shell } => {
217 let mut command = command;
218 let bin_name = command.get_name().to_string();
219 generate_completions(shell, &mut command, bin_name, &mut io::stdout());
220 }
221 }
222
223 return Ok(());
224 }
225
226 let config = load_config(standard_args.config.as_deref())?;
228
229 let target_os = standard_args
230 .target_os
231 .as_ref()
232 .or_else(|| config.global_config().target_os.as_ref())
233 .map(|targets| targets.iter().map(|target| target.as_str()).collect_vec());
234
235 eprintln!("TARGET {target_os:?}");
236
237 let walker = {
240 let directories = standard_args.directories.as_slice();
241 let (first_dir, other_dirs) = directories
242 .split_first()
243 .expect("clap should guarantee that there's at least one input directory");
244
245 let mut types = TypesBuilder::new();
246 types.add("rust", "*.rs").unwrap();
247 types.select("rust");
248
249 let mut overrides = OverrideBuilder::new("");
250 overrides.add("**/*.rs").unwrap();
254 overrides.add("!**/tests/**").unwrap();
255 overrides.add("!**/examples/**").unwrap();
256 overrides.add("!**/benches/**").unwrap();
257 overrides.add("!build.rs").unwrap();
258 overrides.add("**/src/**").unwrap();
259 let overrides = overrides.build().unwrap();
260
261 let mut walker_builder = WalkBuilder::new(first_dir);
262 walker_builder.types(types.build().unwrap());
263 walker_builder.overrides(overrides);
264 other_dirs.iter().for_each(|dir| {
265 walker_builder.add(dir);
266 });
267 walker_builder.build()
268 };
269
270 let parser_inputs = parser_inputs(walker);
272
273 let data = parse_input(
275 parser_inputs,
276 &[],
277 if standard_args.output.file.is_some() {
278 FilesMode::Single
279 } else {
280 FilesMode::Multi(())
281 },
282 target_os.as_deref(),
283 )
284 .map_err(|errors| {
285 let errors = &errors;
287 let message = lazy_format!("{error}\n" for error in errors);
288 anyhow::anyhow!("{message}")
289 })
290 .context("error parsing input files")?;
291
292 let destination = standard_args.output.location();
293
294 let language: &String = args
295 .get_one("language")
296 .expect("clap should guarantee that --lang is provided");
297
298 Helper::LanguageSet::execute_typeshare_for_language(
299 &language,
300 &config,
301 &args,
302 &language_metas,
303 data,
304 &destination,
305 )
306}