typeshare-engine 0.4.1

Behavioral engine for typeshare: parsing, writing, configuration, and everything in between.
Documentation
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;

    /// Each language has a set of configuration metadata, describing all
    /// of its configuration parameters. This metadata is used to populate
    /// the clap command with language specific parameters for each language,
    /// and to load a fully configured language. It is computed based on the
    /// serde serialization of a config.
    fn compute_language_metas() -> anyhow::Result<Self::LanguageMetas>;

    /// Add the `--language` argument to the command, such that all of the
    /// languages in this set are possible values for that argument
    fn add_lang_argument(command: clap::Command) -> clap::Command;

    /// Add all of the language-specific arguments to the 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(())
}

// We support typeshare binaries for up to 16 languages. Fork us and make your
// own if that's not enough for you.
language_set_for! {
    A B C D
    E F G H
    I J K L
    M N O P
}

/// This trait is used by the driver macro to unify the 'config lifetime
/// across all of the language types. I'm open to suggesstions for getting
/// rid of this.
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);

    // Parse command line arguments. Need to clone here because we
    // need to be able to generate completions later.
    let args = command.clone().get_matches();

    // Load the standard arguments from the parsed arguments. Generally
    // we expect that this won't fail, because the `command` has been
    // configured to only give us valid arrangements of args
    let standard_args = StandardArgs::from_arg_matches(&args)
        .expect("StandardArgs should always be loadable from a `command`");

    // If we asked for completions, do that before anything else
    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(());
    }

    // Load all of the language configurations
    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:?}");

    // Construct the directory walker that will produce the list of
    // files to typeshare
    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("");
        // We need this global match because an override, by default, rejects
        // files. We need to *accept* all files, *except* those that are
        // explicitly rejected by the subsequent lines.
        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()
    };

    // Collect all of the files we intend to parse with typeshare
    let parser_inputs = parser_inputs(walker);

    // Parse those files
    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| {
        // TODO: switch to miette
        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,
    )
}