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