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::{types::TypesBuilder, WalkBuilder};
7use typeshare_model::prelude::{CrateName, FilesMode, Language};
8
9use crate::{
10 args::{add_lang_argument, add_language_params_to_clap, Command, OutputLocation, StandardArgs},
11 config::{
12 self, compute_args_set, load_config, load_language_config_from_file_and_args, CliArgsSet,
13 },
14 parser::{parse_input, parser_inputs, ParsedData},
15 writer::write_output,
16};
17
18pub struct PersonalizeClap {
19 pub name: &'static str,
20 pub version: &'static str,
21 pub author: &'static str,
22 pub about: &'static str,
23}
24
25pub trait LanguageSet {
26 type LanguageMetas;
27
28 fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas>;
34
35 fn add_lang_argument(command: clap::Command) -> clap::Command;
38
39 fn add_language_specific_arguments(
41 command: clap::Command,
42 metas: &Self::LanguageMetas,
43 ) -> clap::Command;
44
45 fn execute_typeshare_for_language<'c, 'a: 'c>(
46 language: &str,
47 config: &'c config::Config,
48 args: &'c clap::ArgMatches,
49 metas: &'a Self::LanguageMetas,
50 data: HashMap<Option<CrateName>, ParsedData>,
51 destination: &OutputLocation<'_>,
52 ) -> anyhow::Result<()>;
53}
54
55macro_rules! metas {
56 ([$($CliArgsSet:ident)*]) => {
57 ($($CliArgsSet,)*)
58 };
59
60 ([$($CliArgsSet:ident)*] $Language:ident $($Tail:ident)*) => {
61 metas! {[$($CliArgsSet)* CliArgsSet] $($Tail)*}
62 };
63}
64
65macro_rules! language_set_for {
66 ($Language:ident $($Tail:ident)*) => {
67 language_set_for! {
68 [$Language] $($Tail)*
69 }
70 };
71
72 ([$($Language:ident)*] $Head:ident $($Tail:ident)*) => {
73 language_set_for! {[$($Language)*]}
74
75 language_set_for! {
76 [$($Language)* $Head] $($Tail)*
77 }
78 };
79
80 ([$($Language:ident)+]) => {
81 impl< $($Language,)*> LanguageSet for ($($Language,)*)
82 where $(
83 for<'config> $Language: Language<'config>,
84 )*
85 {
86 type LanguageMetas = metas!([] $($Language)*);
87
88 fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas> {
89 Ok(
90 ($(
91 compute_args_set::<$Language>()?,
92 )*),
93 )
94 }
95
96 fn add_lang_argument(command: clap::Command)->clap::Command {
97 add_lang_argument(command, &[$(<$Language as Language>::NAME,)*])
98 }
99
100 fn add_language_specific_arguments(
101 command: clap::Command,
102 metas: &Self::LanguageMetas,
103 ) -> clap::Command {
104 #[allow(non_snake_case)]
105 let ($($Language,)*) = metas;
106
107 $(
108 let command = add_language_params_to_clap(command, $Language);
109 )*
110
111 command
112 }
113
114 fn execute_typeshare_for_language<'c, 'a: 'c>(
115 language: &str,
116 config: &'c config::Config,
117 args: &'c clap::ArgMatches,
118 metas: &'a Self::LanguageMetas,
119 data: HashMap<Option<CrateName>, ParsedData>,
120 destination: &OutputLocation<'_>,
121 ) -> anyhow::Result<()> {
122 #[allow(non_snake_case)]
123 let ($($Language,)*) = metas;
124
125 $(
126 if language == <$Language as Language>::NAME {
127 execute_typeshare_for_language::<$Language>(
128 config,
129 args,
130 $Language,
131 data,
132 destination
133 )
134 } else
135 )*
136 {
137 anyhow::bail!("{language} isn't a valid language; clap should have prevented this")
138 }
139 }
140 }
141 }
142}
143
144fn execute_typeshare_for_language<'config, 'a: 'config, L: Language<'config>>(
145 config: &'config config::Config,
146 args: &'config clap::ArgMatches,
147 meta: &'a CliArgsSet,
148 data: HashMap<Option<CrateName>, ParsedData>,
149 destination: &OutputLocation<'_>,
150) -> anyhow::Result<()> {
151 let name = L::NAME;
152
153 let config = load_language_config_from_file_and_args::<L>(&config, &args, meta)
154 .with_context(|| format!("failed to load configuration for language {name}"))?;
155
156 let language_implementation = <L>::new_from_config(config)
157 .with_context(|| format!("failed to load configuration for language {name}"))?;
158
159 write_output(&language_implementation, data, destination)
160 .with_context(|| format!("failed to generate typeshared code for language {name}"))?;
161
162 Ok(())
163}
164
165language_set_for! {
168 A B C D
169 E F G H
170 I J K L
171 M N O P
172}
173
174pub fn main_body<Languages>() -> anyhow::Result<()>
175where
176 Languages: LanguageSet,
177{
178 let language_metas = Languages::compute_language_metas()?;
179 let command = StandardArgs::command();
180
181 let command = Languages::add_lang_argument(command);
188 let command = Languages::add_language_specific_arguments(command, &language_metas);
189
190 let args = command.clone().get_matches();
193
194 let standard_args = StandardArgs::from_arg_matches(&args)
198 .expect("StandardArgs should always be loadable from a `command`");
199
200 if let Some(options) = standard_args.subcommand {
202 match options {
203 Command::Completions { shell } => {
204 let mut command = command;
205 let bin_name = command.get_name().to_string();
206 generate_completions(shell, &mut command, bin_name, &mut io::stdout());
207 }
208 }
209
210 return Ok(());
211 }
212
213 let config = load_config(standard_args.config.as_deref())?;
215
216 let walker = {
219 let directories = standard_args.directories.as_slice();
220 let (first_dir, other_dirs) = directories
221 .split_first()
222 .expect("clap should guarantee that there's at least one input directory");
223
224 let mut types = TypesBuilder::new();
225 types.add("rust", "*.rs").unwrap();
226 types.select("rust");
227
228 let mut walker_builder = WalkBuilder::new(first_dir);
229 walker_builder.types(types.build().unwrap());
230 other_dirs.iter().for_each(|dir| {
231 walker_builder.add(dir);
232 });
233 walker_builder.build()
234 };
235
236 let parser_inputs = parser_inputs(walker);
238
239 let data = parse_input(
241 parser_inputs,
242 &[],
243 if standard_args.output.file.is_some() {
244 FilesMode::Single
245 } else {
246 FilesMode::Multi(())
247 },
248 )
249 .unwrap();
250
251 let destination = standard_args.output.location();
252
253 let language: &String = args
254 .get_one("language")
255 .expect("clap should guarantee that --lang is provided");
256
257 let out = Languages::execute_typeshare_for_language(
258 &language,
259 &config,
260 &args,
261 &language_metas,
262 data,
263 &destination,
264 );
265
266 out
267}