use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[allow(dead_code)] pub struct PackagesConfig {
pub npm: Option<NpmConfig>,
pub pip: Option<PipConfig>,
pub cargo: Option<CargoConfig>,
pub nuget: Option<NugetConfig>,
pub gem: Option<GemConfig>,
pub go: Option<GoConfig>,
}
#[derive(Debug, Clone, Copy)]
pub struct PackagesConfigRef<'a> {
pub npm: Option<&'a NpmConfig>,
pub pip: Option<&'a PipConfig>,
pub cargo: Option<&'a CargoConfig>,
pub nuget: Option<&'a NugetConfig>,
pub gem: Option<&'a GemConfig>,
pub go: Option<&'a GoConfig>,
pub origin: crate::ai_hooks::ConfigOrigin,
pub allow_remote_packages: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PackageSpec {
Version(String),
Detailed {
version: String,
#[serde(default)]
optional: bool,
#[serde(default)]
features: Vec<String>,
},
}
impl PackageSpec {
pub fn version(&self) -> &str {
match self {
PackageSpec::Version(v) => v,
PackageSpec::Detailed { version, .. } => version,
}
}
pub fn is_optional(&self) -> bool {
match self {
PackageSpec::Version(_) => false,
PackageSpec::Detailed { optional, .. } => *optional,
}
}
pub fn features(&self) -> &[String] {
match self {
PackageSpec::Version(_) => &[],
PackageSpec::Detailed { features, .. } => features,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct NpmConfig {
#[serde(flatten)]
pub packages: HashMap<String, PackageSpec>,
#[serde(default)]
pub package_manager: Option<NpmPackageManager>,
#[serde(default)]
pub from_lockfile: bool,
#[serde(default = "default_true")]
pub install_dev: bool,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum NpmPackageManager {
#[default]
Npm,
Yarn,
Pnpm,
}
impl NpmPackageManager {
pub fn command(&self) -> &'static str {
match self {
NpmPackageManager::Npm => "npm",
NpmPackageManager::Yarn => "yarn",
NpmPackageManager::Pnpm => "pnpm",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PipConfig {
#[serde(flatten)]
pub packages: HashMap<String, PackageSpec>,
#[serde(default)]
pub venv: Option<String>,
#[serde(default = "default_true")]
pub create_venv: bool,
#[serde(default)]
pub from_lockfile: bool,
#[serde(default)]
pub lockfile: Option<String>,
#[serde(default = "default_true")]
pub activate_hint: bool,
#[serde(default)]
pub system_site_packages: bool,
#[serde(default)]
pub python_version: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct CargoConfig {
#[serde(flatten)]
pub packages: HashMap<String, PackageSpec>,
#[serde(default)]
pub locked: bool,
}
pub const NPM_KNOBS: &[&str] = &["package_manager", "from_lockfile", "install_dev"];
pub const PIP_KNOBS: &[&str] = &[
"venv",
"create_venv",
"from_lockfile",
"lockfile",
"activate_hint",
"system_site_packages",
"python_version",
];
pub const CARGO_KNOBS: &[&str] = &["locked"];
pub const NUGET_KNOBS: &[&str] = &[];
pub const GEM_KNOBS: &[&str] = &[];
pub const GO_KNOBS: &[&str] = &[];
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct NugetConfig {
#[serde(flatten)]
pub packages: HashMap<String, PackageSpec>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct GemConfig {
#[serde(flatten)]
pub packages: HashMap<String, PackageSpec>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct GoConfig {
#[serde(flatten)]
pub packages: HashMap<String, PackageSpec>,
}
fn default_true() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn packages_ref_field_set_matches_owned_ecosystems() {
fn _enforce_owned(c: PackagesConfig) {
let PackagesConfig {
npm: _,
pip: _,
cargo: _,
nuget: _,
gem: _,
go: _,
} = c;
}
fn _enforce_ref(r: PackagesConfigRef<'_>) {
let PackagesConfigRef {
npm: _,
pip: _,
cargo: _,
nuget: _,
gem: _,
go: _,
origin: _,
allow_remote_packages: _,
} = r;
}
}
#[test]
fn npm_knobs_match_npm_config_fields() {
fn _enforce(c: NpmConfig) {
let NpmConfig {
packages: _,
package_manager: _,
from_lockfile: _,
install_dev: _,
} = c;
}
for k in ["package_manager", "from_lockfile", "install_dev"] {
assert!(
NPM_KNOBS.contains(&k),
"NpmConfig has knob `{}` but NPM_KNOBS is missing it",
k
);
}
}
#[test]
fn pip_knobs_match_pip_config_fields() {
fn _enforce(c: PipConfig) {
let PipConfig {
packages: _,
venv: _,
create_venv: _,
from_lockfile: _,
lockfile: _,
activate_hint: _,
system_site_packages: _,
python_version: _,
} = c;
}
for k in [
"venv",
"create_venv",
"from_lockfile",
"lockfile",
"activate_hint",
"system_site_packages",
"python_version",
] {
assert!(
PIP_KNOBS.contains(&k),
"PipConfig has knob `{}` but PIP_KNOBS is missing it",
k
);
}
}
#[test]
fn cargo_knobs_match_cargo_config_fields() {
fn _enforce(c: CargoConfig) {
let CargoConfig {
packages: _,
locked: _,
} = c;
}
assert!(
CARGO_KNOBS.contains(&"locked"),
"CargoConfig has knob `locked` but CARGO_KNOBS is missing it"
);
}
#[test]
fn nuget_knobs_match_nuget_config_fields() {
fn _enforce(c: NugetConfig) {
let NugetConfig { packages: _ } = c;
}
assert!(NUGET_KNOBS.is_empty(), "NugetConfig has no knobs yet");
}
#[test]
fn gem_knobs_match_gem_config_fields() {
fn _enforce(c: GemConfig) {
let GemConfig { packages: _ } = c;
}
assert!(GEM_KNOBS.is_empty(), "GemConfig has no knobs yet");
}
#[test]
fn go_knobs_match_go_config_fields() {
fn _enforce(c: GoConfig) {
let GoConfig { packages: _ } = c;
}
assert!(GO_KNOBS.is_empty(), "GoConfig has no knobs yet");
}
#[test]
fn test_parse_simple_npm_config() {
let toml_str = r#"
typescript = "^5.0"
eslint = "latest"
"#;
let config: NpmConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.packages.len(), 2);
assert!(matches!(
config.packages.get("typescript"),
Some(PackageSpec::Version(v)) if v == "^5.0"
));
}
#[test]
fn test_parse_npm_with_package_manager() {
let toml_str = r#"
typescript = "^5.0"
package_manager = "pnpm"
"#;
let config: NpmConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.package_manager, Some(NpmPackageManager::Pnpm));
}
#[test]
fn test_parse_pip_with_venv() {
let toml_str = r#"
pytest = ">=7.0"
venv = ".venv"
create_venv = true
"#;
let config: PipConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.venv, Some(".venv".to_string()));
assert!(config.create_venv);
assert!(config.packages.contains_key("pytest"));
}
#[test]
fn test_parse_nuget_config() {
let toml_str = r#"
dotnet-ef = "latest"
csharpier = "0.30.0"
"#;
let config: NugetConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.packages.len(), 2);
assert!(matches!(
config.packages.get("dotnet-ef"),
Some(PackageSpec::Version(v)) if v == "latest"
));
}
#[test]
fn test_parse_cargo_config() {
let toml_str = r#"
cargo-watch = "latest"
cargo-nextest = "latest"
locked = true
"#;
let config: CargoConfig = toml::from_str(toml_str).unwrap();
assert!(config.locked);
assert_eq!(config.packages.len(), 2);
}
#[test]
fn test_package_spec_detailed() {
let toml_str = r#"
[some-crate]
version = "1.0.0"
optional = true
features = ["feature1", "feature2"]
"#;
#[derive(Deserialize)]
struct Test {
#[serde(rename = "some-crate")]
some_crate: PackageSpec,
}
let test: Test = toml::from_str(toml_str).unwrap();
assert!(test.some_crate.is_optional());
assert_eq!(test.some_crate.features().len(), 2);
}
}