use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::{Metadata, MetadataCommand, Package};
use serde::Deserialize;
use crate::cli::Args;
use crate::error::OrthohelpError;
use crate::powershell::{ExportAlias, HelpInfoUri, ModuleName};
use crate::schema::WindowsMetadata;
#[derive(Debug, Default, Deserialize)]
pub struct OrthoConfigMetadata {
pub root_type: Option<String>,
pub locales: Option<Vec<String>>,
#[serde(default)]
pub windows: Option<WindowsMetadataOverrides>,
}
#[derive(Debug, Clone)]
pub struct OrthoConfigDependency {
pub requirement: String,
pub path: Option<Utf8PathBuf>,
}
#[derive(Debug, Clone)]
pub struct PackageSelection {
pub package_name: String,
pub package_root: Utf8PathBuf,
pub target_directory: Utf8PathBuf,
pub package_version: String,
pub root_type: String,
pub locales: Option<Vec<String>>,
pub windows: Option<WindowsMetadataOverrides>,
pub ortho_config_dependency: OrthoConfigDependency,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct WindowsMetadataOverrides {
pub module_name: Option<String>,
pub export_aliases: Option<Vec<String>>,
#[serde(rename = "include_common_parameters")]
pub should_include_common_parameters: Option<bool>,
#[serde(rename = "split_subcommands_into_functions")]
pub should_split_subcommands_into_functions: Option<bool>,
pub help_info_uri: Option<String>,
}
impl WindowsMetadataOverrides {
#[must_use]
pub fn resolve(&self, base: Option<&WindowsMetadata>) -> ResolvedWindowsMetadata {
let mut resolved = base
.cloned()
.map(ResolvedWindowsMetadata::from)
.unwrap_or_default();
resolved.module_name = self
.module_name
.clone()
.map(Into::into)
.or(resolved.module_name);
resolved.export_aliases = self
.export_aliases
.clone()
.map(|aliases| aliases.into_iter().map(Into::into).collect())
.unwrap_or(resolved.export_aliases);
resolved.should_include_common_parameters = self
.should_include_common_parameters
.unwrap_or(resolved.should_include_common_parameters);
resolved.should_split_subcommands_into_functions = self
.should_split_subcommands_into_functions
.unwrap_or(resolved.should_split_subcommands_into_functions);
resolved.help_info_uri = self
.help_info_uri
.clone()
.map(Into::into)
.or(resolved.help_info_uri);
resolved
}
}
#[derive(Debug, Clone)]
pub struct ResolvedWindowsMetadata {
pub module_name: Option<ModuleName>,
pub export_aliases: Vec<ExportAlias>,
pub should_include_common_parameters: bool,
pub should_split_subcommands_into_functions: bool,
pub help_info_uri: Option<HelpInfoUri>,
}
impl Default for ResolvedWindowsMetadata {
fn default() -> Self {
Self {
module_name: None,
export_aliases: Vec::new(),
should_include_common_parameters: true,
should_split_subcommands_into_functions: false,
help_info_uri: None,
}
}
}
impl From<WindowsMetadata> for ResolvedWindowsMetadata {
fn from(metadata: WindowsMetadata) -> Self {
Self {
module_name: metadata.module_name.map(Into::into),
export_aliases: metadata
.export_aliases
.into_iter()
.map(Into::into)
.collect(),
should_include_common_parameters: metadata.include_common_parameters,
should_split_subcommands_into_functions: metadata.split_subcommands_into_functions,
help_info_uri: metadata.help_info_uri.map(Into::into),
}
}
}
pub fn load_metadata() -> Result<Metadata, OrthohelpError> {
let mut command = MetadataCommand::new();
command.no_deps();
Ok(command.exec()?)
}
pub fn select_package(
metadata: &Metadata,
args: &Args,
) -> Result<PackageSelection, OrthohelpError> {
if args.is_lib && args.bin.is_some() {
return Err(OrthohelpError::Message(
"cannot use --lib and --bin together".to_owned(),
));
}
let package = match args.package.as_ref() {
Some(name) => find_package(metadata, name)?,
None => metadata
.root_package()
.ok_or(OrthohelpError::WorkspaceRootMissing)?,
};
let package_name = package.name.clone();
let package_root = package
.manifest_path
.parent()
.map(Utf8Path::to_path_buf)
.ok_or_else(|| OrthohelpError::Message("package manifest has no parent".to_owned()))?;
let target_directory = metadata.target_directory.clone();
let package_version = package.version.to_string();
let crate_ident = package_name.replace('-', "_");
let metadata_defaults = parse_ortho_config_metadata(package)?;
let raw_root_type = args
.root_type
.clone()
.or_else(|| metadata_defaults.root_type.clone())
.ok_or(OrthohelpError::MissingRootType)?;
let root_type = normalize_root_type(&raw_root_type, &crate_ident);
ensure_library_target(package)?;
if let Some(bin) = args.bin.as_ref() {
ensure_bin_target(package, bin)?;
}
let ortho_config_dependency = find_ortho_config_dependency(package)?;
Ok(PackageSelection {
package_name,
package_root,
target_directory,
package_version,
root_type,
locales: metadata_defaults.locales,
windows: metadata_defaults.windows,
ortho_config_dependency,
})
}
fn find_package<'a>(metadata: &'a Metadata, name: &str) -> Result<&'a Package, OrthohelpError> {
metadata
.packages
.iter()
.find(|package| package.name == name)
.ok_or_else(|| OrthohelpError::PackageNotFound(name.to_owned()))
}
fn parse_ortho_config_metadata(package: &Package) -> Result<OrthoConfigMetadata, OrthohelpError> {
let Some(value) = package.metadata.get("ortho_config") else {
return Ok(OrthoConfigMetadata::default());
};
serde_json::from_value(value.clone()).map_err(OrthohelpError::MetadataJson)
}
fn ensure_library_target(package: &Package) -> Result<(), OrthohelpError> {
let has_lib = package
.targets
.iter()
.any(|target| target.kind.iter().any(|kind| kind == "lib"));
if has_lib {
Ok(())
} else {
Err(OrthohelpError::MissingLibraryTarget(package.name.clone()))
}
}
fn ensure_bin_target(package: &Package, bin: &str) -> Result<(), OrthohelpError> {
let has_bin = package
.targets
.iter()
.any(|target| target.name == bin && target.kind.iter().any(|kind| kind == "bin"));
if has_bin {
return Ok(());
}
Err(OrthohelpError::MissingBinTarget {
package: package.name.clone(),
bin: bin.to_owned(),
})
}
fn find_ortho_config_dependency(
package: &Package,
) -> Result<OrthoConfigDependency, OrthohelpError> {
let dependency = package
.dependencies
.iter()
.find(|dep| dep.name == "ortho_config")
.ok_or_else(|| OrthohelpError::MissingOrthoConfigDependency(package.name.clone()))?;
Ok(OrthoConfigDependency {
requirement: dependency.req.to_string(),
path: dependency.path.clone(),
})
}
fn normalize_root_type(raw: &str, crate_ident: &str) -> String {
if let Some(stripped) = raw.strip_prefix("crate::") {
return format!("{crate_ident}::{stripped}");
}
if raw.contains("::") {
return raw.to_owned();
}
format!("{crate_ident}::{raw}")
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case::crate_prefix("crate::Config", "demo", "demo::Config")]
#[case::bare_type("Config", "demo", "demo::Config")]
#[case::qualified("demo::Config", "ignored", "demo::Config")]
fn normalizes_root_type(#[case] raw: &str, #[case] crate_ident: &str, #[case] expected: &str) {
let normalized = normalize_root_type(raw, crate_ident);
assert_eq!(normalized, expected);
}
}