typeshare_engine/
driver.rs

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    /// Each language has a set of configuration metadata, describing all
29    /// of its configuration parameters. This metadata is used to populate
30    /// the clap command with language specific parameters for each language,
31    /// and to load a fully configured language. It is computed based on the
32    /// serde serialization of a config.
33    fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas>;
34
35    /// Add the `--language` argument to the command, such that all of the
36    /// languages in this set are possible values for that argument
37    fn add_lang_argument(command: clap::Command) -> clap::Command;
38
39    /// Add all of the language-specific arguments to the clap command.
40    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
165// We support typeshare binaries for up to 16 languages. Fork us and make your
166// own if that's not enough for you.
167language_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 = command
182    //     .name(personalize.name)
183    //     .version(personalize.version)
184    //     .author(personalize.author)
185    //     .about(personalize.about);
186
187    let command = Languages::add_lang_argument(command);
188    let command = Languages::add_language_specific_arguments(command, &language_metas);
189
190    // Parse command line arguments. Need to clone here because we
191    // need to be able to generate completions later.
192    let args = command.clone().get_matches();
193
194    // Load the standard arguments from the parsed arguments. Generally
195    // we expect that this won't fail, because the `command` has been
196    // configured to only give us valid arrangements of args
197    let standard_args = StandardArgs::from_arg_matches(&args)
198        .expect("StandardArgs should always be loadable from a `command`");
199
200    // If we asked for completions, do that before anything else
201    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    // Load all of the language configurations
214    let config = load_config(standard_args.config.as_deref())?;
215
216    // Construct the directory walker that will produce the list of
217    // files to typeshare
218    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    // Collect all of the files we intend to parse with typeshare
237    let parser_inputs = parser_inputs(walker);
238
239    // Parse those files
240    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}