mod about;
mod maml;
mod manifest;
#[cfg(test)]
mod test_fixtures;
mod text;
mod types;
mod wrapper;
mod writer;
pub use types::{
BinaryName, ExportAlias, HelpInfoUri, ModuleName, ModuleVersion, PowerShellConfig,
PowerShellOutput,
};
use camino::{Utf8Path, Utf8PathBuf};
use cap_std::fs_utf8::Dir;
use crate::error::OrthohelpError;
use crate::ir::LocalizedDocMetadata;
struct GenerationPaths<'a> {
root_dir: &'a Dir,
module_root: &'a Utf8PathBuf,
}
struct LocaleWriteRequest<'a> {
metadata: &'a LocalizedDocMetadata,
locale_name: &'a str,
}
pub fn generate(
locales: &[LocalizedDocMetadata],
config: &PowerShellConfig,
) -> Result<PowerShellOutput, OrthohelpError> {
let root_metadata = locales
.first()
.ok_or_else(|| OrthohelpError::Message("no locales provided".to_owned()))?;
let module_root = config.module_root();
let root_dir = writer::ensure_dir(&module_root)?;
let paths = GenerationPaths {
root_dir: &root_dir,
module_root: &module_root,
};
let mut output = PowerShellOutput::new();
write_core_files(&paths, config, root_metadata, &mut output)?;
let (locales_to_write, fallback_locale) = resolve_locales(locales, config.should_ensure_en_us);
for locale in locales_to_write {
let request = LocaleWriteRequest {
metadata: locale,
locale_name: &locale.locale,
};
write_locale_files(&paths, config, &request, &mut output)?;
}
if let Some(locale) = fallback_locale {
let request = LocaleWriteRequest {
metadata: locale,
locale_name: "en-US",
};
write_locale_files(&paths, config, &request, &mut output)?;
}
Ok(output)
}
fn write_core_files(
paths: &GenerationPaths<'_>,
config: &PowerShellConfig,
metadata: &LocalizedDocMetadata,
output: &mut PowerShellOutput,
) -> Result<(), OrthohelpError> {
let functions_to_export = build_functions_to_export(metadata, config);
let bin_name = wrapper::BinName::new(config.bin_name.as_ref());
let export_aliases = config
.export_aliases
.iter()
.map(|alias| wrapper::Alias::new(alias.as_ref()))
.collect::<Vec<_>>();
let wrapper_content = wrapper::render_wrapper(
metadata,
&bin_name,
&export_aliases,
config.should_split_subcommands,
);
let wrapper_relative = Utf8PathBuf::from(format!("{}.psm1", config.module_name.as_ref()));
output.add_file(write_module_file(
paths,
&wrapper_relative,
&wrapper_content,
false,
)?);
let manifest_content = manifest::render_manifest(&manifest::ManifestConfig {
module_name: config.module_name.as_ref(),
module_version: config.module_version.as_ref(),
functions_to_export: &functions_to_export,
aliases_to_export: &config.export_aliases,
help_info_uri: config.help_info_uri.as_ref().map(AsRef::as_ref),
});
let manifest_relative = Utf8PathBuf::from(format!("{}.psd1", config.module_name.as_ref()));
output.add_file(write_module_file(
paths,
&manifest_relative,
&manifest_content,
false,
)?);
Ok(())
}
fn write_locale_files(
paths: &GenerationPaths<'_>,
config: &PowerShellConfig,
request: &LocaleWriteRequest<'_>,
output: &mut PowerShellOutput,
) -> Result<(), OrthohelpError> {
let locale_dir_relative = Utf8PathBuf::from(request.locale_name);
ensure_module_subdir(paths, &locale_dir_relative)?;
let commands = build_command_specs(request.metadata, config);
let maml_content = maml::render_help(
&commands,
maml::MamlOptions {
should_include_common_parameters: config.should_include_common_parameters,
},
);
let help_relative =
locale_dir_relative.join(format!("{}-help.xml", config.module_name.as_ref()));
output.add_file(write_module_file(
paths,
&help_relative,
&maml_content,
true,
)?);
let about_content = about::render_about(request.metadata, config.module_name.as_ref());
let about_relative =
locale_dir_relative.join(format!("about_{}.help.txt", config.module_name.as_ref()));
output.add_file(write_module_file(
paths,
&about_relative,
&about_content,
false,
)?);
Ok(())
}
fn write_module_file(
paths: &GenerationPaths<'_>,
relative_path: &Utf8Path,
content: &str,
should_include_bom: bool,
) -> Result<Utf8PathBuf, OrthohelpError> {
writer::write_crlf_text(
paths.root_dir,
&writer::WriteTarget {
root: paths.module_root,
relative_path,
},
content,
should_include_bom,
)
}
fn ensure_module_subdir(
paths: &GenerationPaths<'_>,
relative_path: &Utf8Path,
) -> Result<(), OrthohelpError> {
paths
.root_dir
.create_dir_all(relative_path)
.map_err(|io_err| OrthohelpError::Io {
path: paths.module_root.join(relative_path),
source: io_err,
})?;
Ok(())
}
fn build_functions_to_export(
metadata: &LocalizedDocMetadata,
config: &PowerShellConfig,
) -> Vec<String> {
let mut functions = Vec::new();
functions.push(config.bin_name.as_ref().to_owned());
if config.should_split_subcommands {
for (sub_name, _) in iter_subcommands(metadata) {
functions.push(format!("{}_{}", config.bin_name, sub_name));
}
}
functions
}
fn build_command_specs<'a>(
metadata: &'a LocalizedDocMetadata,
config: &PowerShellConfig,
) -> Vec<maml::CommandSpec<'a>> {
let mut commands = Vec::new();
commands.push(maml::CommandSpec {
name: config.bin_name.as_ref().to_owned(),
metadata,
});
if config.should_split_subcommands {
for (sub_name, subcommand) in iter_subcommands(metadata) {
commands.push(maml::CommandSpec {
name: format!("{}_{}", config.bin_name, sub_name),
metadata: subcommand,
});
}
}
commands
}
fn iter_subcommands(
metadata: &LocalizedDocMetadata,
) -> impl Iterator<Item = (&str, &LocalizedDocMetadata)> {
metadata.subcommands.iter().map(|subcommand| {
(
subcommand
.bin_name
.as_deref()
.unwrap_or(&subcommand.app_name),
subcommand,
)
})
}
fn resolve_locales(
locales: &[LocalizedDocMetadata],
should_ensure_en_us: bool,
) -> (&[LocalizedDocMetadata], Option<&LocalizedDocMetadata>) {
let has_en_us = locales.iter().any(|locale| locale.locale == "en-US");
let fallback = if should_ensure_en_us && !has_en_us {
locales.first()
} else {
None
};
(locales, fallback)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::powershell::test_fixtures;
use rstest::rstest;
#[rstest]
#[case(&["fr-FR"], true, true)]
#[case(&["en-US"], true, false)]
fn resolve_locales_handles_en_us_fallback(
#[case] locale_names: &[&str],
#[case] should_ensure_en_us: bool,
#[case] should_have_fallback: bool,
) {
let locales = locale_names
.iter()
.map(|locale| test_fixtures::minimal_doc(locale, "Fixture"))
.collect::<Vec<_>>();
let (resolved, fallback) = resolve_locales(&locales, should_ensure_en_us);
assert_eq!(resolved.len(), locales.len());
assert_eq!(fallback.is_some(), should_have_fallback);
}
}