use std::{collections::HashMap, io};
use anyhow::Context as _;
use clap::{CommandFactory as _, FromArgMatches as _};
use clap_complete::generate as generate_completions;
use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder};
use itertools::Itertools;
use lazy_format::lazy_format;
use typeshare_model::prelude::{CrateName, FilesMode, Language};
use crate::{
args::{
self, add_lang_argument, add_language_params_to_clap, add_personalizations, Command,
OutputLocation, StandardArgs,
},
config::{
self, compute_args_set, load_config, load_language_config_from_file_and_args, CliArgsSet,
},
parser::{parse_input, parser_inputs, ParsedData},
writer::write_output,
};
pub trait LanguageSet<'config> {
type LanguageMetas: 'static;
fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas>;
fn add_lang_argument(command: clap::Command) -> clap::Command;
fn add_language_specific_arguments(
command: clap::Command,
metas: &Self::LanguageMetas,
) -> clap::Command;
fn execute_typeshare_for_language(
language: &str,
config: &'config config::Config,
args: &'config clap::ArgMatches,
metas: &Self::LanguageMetas,
data: HashMap<Option<CrateName>, ParsedData>,
destination: &OutputLocation<'_>,
) -> anyhow::Result<()>;
}
macro_rules! metas {
([$($CliArgsSet:ident)*]) => {
($($CliArgsSet,)*)
};
([$($CliArgsSet:ident)*] $Language:ident $($Tail:ident)*) => {
metas! {[$($CliArgsSet)* CliArgsSet] $($Tail)*}
};
}
macro_rules! language_set_for {
($Language:ident $($Tail:ident)*) => {
language_set_for! {
[$Language] $($Tail)*
}
};
([$($Language:ident)*] $Head:ident $($Tail:ident)*) => {
language_set_for! {[$($Language)*]}
language_set_for! {
[$($Language)* $Head] $($Tail)*
}
};
([$($Language:ident)+]) => {
impl<'config, $($Language,)*> LanguageSet<'config> for ($($Language,)*)
where $(
$Language: Language<'config>,
)*
{
type LanguageMetas = metas!([] $($Language)*);
fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas> {
Ok(
($(
compute_args_set::<$Language>()?,
)*),
)
}
fn add_lang_argument(command: clap::Command)->clap::Command {
add_lang_argument(command, &[$(<$Language as Language>::NAME,)*])
}
fn add_language_specific_arguments(
command: clap::Command,
metas: &Self::LanguageMetas,
) -> clap::Command {
#[allow(non_snake_case)]
let ($($Language,)*) = metas;
$(
let command = add_language_params_to_clap(
command,
<$Language as Language>::NAME,
$Language
);
)*
command
}
fn execute_typeshare_for_language(
language: &str,
config: &'config config::Config,
args: &'config clap::ArgMatches,
metas: &Self::LanguageMetas,
data: HashMap<Option<CrateName>, ParsedData>,
destination: &OutputLocation<'_>,
) -> anyhow::Result<()> {
#[allow(non_snake_case)]
let ($($Language,)*) = metas;
$(
if language == <$Language as Language>::NAME {
execute_typeshare_for_language::<$Language>(
config,
args,
$Language,
data,
destination
)
} else
)*
{
anyhow::bail!("{language} isn't a valid language; clap should have prevented this")
}
}
}
}
}
fn execute_typeshare_for_language<'config, 'a, L: Language<'config>>(
config: &'config config::Config,
args: &'config clap::ArgMatches,
meta: &'a CliArgsSet,
data: HashMap<Option<CrateName>, ParsedData>,
destination: &OutputLocation<'_>,
) -> anyhow::Result<()> {
let name = L::NAME;
let config = load_language_config_from_file_and_args::<L>(&config, &args, meta)
.with_context(|| format!("failed to load configuration for language {name}"))?;
let language_implementation = <L>::new_from_config(config)
.with_context(|| format!("failed to load configuration for language {name}"))?;
write_output(&language_implementation, data, destination)
.with_context(|| format!("failed to generate typeshared code for language {name}"))?;
Ok(())
}
language_set_for! {
A B C D
E F G H
I J K L
M N O P
}
pub trait LanguageHelper {
type LanguageSet<'config>: LanguageSet<'config>;
}
pub fn main_body<Helper>(personalizations: args::PersonalizeClap) -> anyhow::Result<()>
where
Helper: LanguageHelper,
{
let language_metas = Helper::LanguageSet::compute_language_metas()?;
let command = StandardArgs::command();
let command = add_personalizations(command, personalizations);
let command = Helper::LanguageSet::add_lang_argument(command);
let command = Helper::LanguageSet::add_language_specific_arguments(command, &language_metas);
let args = command.clone().get_matches();
let standard_args = StandardArgs::from_arg_matches(&args)
.expect("StandardArgs should always be loadable from a `command`");
if let Some(options) = standard_args.subcommand {
match options {
Command::Completions { shell } => {
let mut command = command;
let bin_name = command.get_name().to_string();
generate_completions(shell, &mut command, bin_name, &mut io::stdout());
}
}
return Ok(());
}
let config = load_config(standard_args.config.as_deref())?;
let target_os = standard_args
.target_os
.as_ref()
.or_else(|| config.global_config().target_os.as_ref())
.map(|targets| targets.iter().map(|target| target.as_str()).collect_vec());
eprintln!("TARGET {target_os:?}");
let walker = {
let directories = standard_args.directories.as_slice();
let (first_dir, other_dirs) = directories
.split_first()
.expect("clap should guarantee that there's at least one input directory");
let mut types = TypesBuilder::new();
types.add("rust", "*.rs").unwrap();
types.select("rust");
let mut overrides = OverrideBuilder::new("");
overrides.add("**/*.rs").unwrap();
overrides.add("!**/tests/**").unwrap();
overrides.add("!**/examples/**").unwrap();
overrides.add("!**/benches/**").unwrap();
overrides.add("!build.rs").unwrap();
overrides.add("**/src/**/*.rs").unwrap();
let overrides = overrides.build().unwrap();
let mut walker_builder = WalkBuilder::new(first_dir);
walker_builder.types(types.build().unwrap());
walker_builder.overrides(overrides);
other_dirs.iter().for_each(|dir| {
walker_builder.add(dir);
});
walker_builder.build()
};
let parser_inputs = parser_inputs(walker);
let data = parse_input(
parser_inputs,
&[],
if standard_args.output.file.is_some() {
FilesMode::Single
} else {
FilesMode::Multi(())
},
target_os.as_deref(),
)
.map_err(|errors| {
let errors = &errors;
let message = lazy_format!("{error}\n" for error in errors);
anyhow::anyhow!("{message}")
})
.context("error parsing input files")?;
let destination = standard_args.output.location();
let language: &String = args
.get_one("language")
.expect("clap should guarantee that --lang is provided");
Helper::LanguageSet::execute_typeshare_for_language(
&language,
&config,
&args,
&language_metas,
data,
&destination,
)
}