From 42dacc6ee34e4a6fa0a9b1c31aeb3282b7b6f88e Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Tue, 15 Sep 2026 14:04:17 -0500 Subject: [PATCH 1/3] chore: rustfmt env contract helper --- src/bin/zed-env-contract.rs | 290 +++++++++++++++++++++++++++--------- 1 file changed, 221 insertions(+), 69 deletions(-) diff --git a/src/bin/zed-env-contract.rs b/src/bin/zed-env-contract.rs index c01c9429..c7997fe2 100644 --- a/src/bin/zed-env-contract.rs +++ b/src/bin/zed-env-contract.rs @@ -1,6 +1,10 @@ -use std::{collections::{BTreeMap, BTreeSet}, env, fs, path::{Path, PathBuf}}; +use std::{ + collections::{BTreeMap, BTreeSet}, + env, fs, + path::{Path, PathBuf}, +}; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use toml::Value; #[derive(Debug, Clone)] @@ -35,7 +39,13 @@ fn run() -> Result<()> { "check" => check_values(&vars), "list" => { for var in vars.values() { - println!("{}\t{}\t{}\t{}", var.name, var.kind, requirement(var), if var.secret { "secret" } else { "plain" }); + println!( + "{}\t{}\t{}\t{}", + var.name, + var.kind, + requirement(var), + if var.secret { "secret" } else { "plain" } + ); } Ok(()) } @@ -45,67 +55,123 @@ fn run() -> Result<()> { println!("# {description}"); } println!("# {} ({})", requirement(var), var.kind); - let value = if var.secret { "" } else { var.default.as_deref().unwrap_or("") }; + let value = if var.secret { + "" + } else { + var.default.as_deref().unwrap_or("") + }; println!("{}={}\n", var.name, value); } Ok(()) } "json" => { - let rows = vars.values().map(|v| serde_json::json!({ - "name": v.name, - "type": v.kind, - "required": v.required, - "required_when": v.required_when, - "default": if v.secret { None:: } else { v.default.clone() }, - "secret": v.secret, - "description": v.description, - "enum": v.allowed, - "deprecated": v.deprecated, - "replacement": v.replacement, - })).collect::>(); + let rows = vars + .values() + .map(|v| { + serde_json::json!({ + "name": v.name, + "type": v.kind, + "required": v.required, + "required_when": v.required_when, + "default": if v.secret { None:: } else { v.default.clone() }, + "secret": v.secret, + "description": v.description, + "enum": v.allowed, + "deprecated": v.deprecated, + "replacement": v.replacement, + }) + }) + .collect::>(); println!("{}", serde_json::to_string_pretty(&rows)?); Ok(()) } - other => bail!("unknown env-contract command `{other}`; expected check, list, template, or json"), + other => { + bail!("unknown env-contract command `{other}`; expected check, list, template, or json") + } } } fn manifest_path(explicit: Option) -> Result { - if let Some(path) = explicit { return Ok(path); } + if let Some(path) = explicit { + return Ok(path); + } let mut current = env::current_dir()?; loop { let candidate = current.join(".zpkg.toml"); - if candidate.is_file() { return Ok(candidate); } - if !current.pop() { break; } + if candidate.is_file() { + return Ok(candidate); + } + if !current.pop() { + break; + } } bail!("could not find .zpkg.toml in this directory or any parent") } fn load_contract(path: &Path) -> Result> { - let text = fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; - let root: Value = text.parse().with_context(|| format!("invalid TOML in {}", path.display()))?; - let Some(env_table) = root.get("env").and_then(Value::as_table) else { return Ok(BTreeMap::new()); }; - let Some(rows) = env_table.get("vars").and_then(Value::as_array) else { return Ok(BTreeMap::new()); }; + let text = + fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + let root: Value = text + .parse() + .with_context(|| format!("invalid TOML in {}", path.display()))?; + let Some(env_table) = root.get("env").and_then(Value::as_table) else { + return Ok(BTreeMap::new()); + }; + let Some(rows) = env_table.get("vars").and_then(Value::as_array) else { + return Ok(BTreeMap::new()); + }; let mut out = BTreeMap::new(); for row in rows { - let table = row.as_table().context("each [[env.vars]] entry must be a table")?; + let table = row + .as_table() + .context("each [[env.vars]] entry must be a table")?; let name = string(table, "name")?.to_string(); - if out.contains_key(&name) { bail!("duplicate env contract variable `{name}`"); } - let allowed = table.get("enum").and_then(Value::as_array).map(|values| { - values.iter().map(|v| v.as_str().context("env var enum values must be strings").map(str::to_owned)).collect::>>() - }).transpose()?.unwrap_or_default(); - out.insert(name.clone(), EnvVar { - name, - kind: table.get("type").and_then(Value::as_str).unwrap_or("string").to_string(), - required: table.get("required").and_then(Value::as_bool).unwrap_or(false), - required_when: opt_string(table, "required_when")?, - default: opt_scalar_string(table, "default")?, - secret: table.get("secret").and_then(Value::as_bool).unwrap_or(false), - description: opt_string(table, "description")?, - allowed, - deprecated: table.get("deprecated").and_then(Value::as_bool).unwrap_or(false), - replacement: opt_string(table, "replacement")?, - }); + if out.contains_key(&name) { + bail!("duplicate env contract variable `{name}`"); + } + let allowed = table + .get("enum") + .and_then(Value::as_array) + .map(|values| { + values + .iter() + .map(|v| { + v.as_str() + .context("env var enum values must be strings") + .map(str::to_owned) + }) + .collect::>>() + }) + .transpose()? + .unwrap_or_default(); + out.insert( + name.clone(), + EnvVar { + name, + kind: table + .get("type") + .and_then(Value::as_str) + .unwrap_or("string") + .to_string(), + required: table + .get("required") + .and_then(Value::as_bool) + .unwrap_or(false), + required_when: opt_string(table, "required_when")?, + default: opt_scalar_string(table, "default")?, + secret: table + .get("secret") + .and_then(Value::as_bool) + .unwrap_or(false), + description: opt_string(table, "description")?, + allowed, + deprecated: table + .get("deprecated") + .and_then(Value::as_bool) + .unwrap_or(false), + replacement: opt_string(table, "replacement")?, + }, + ); } Ok(out) } @@ -113,19 +179,41 @@ fn load_contract(path: &Path) -> Result> { fn validate_contract(vars: &BTreeMap) -> Result<()> { const TYPES: &[&str] = &["string", "integer", "boolean", "url", "json"]; for var in vars.values() { - if !valid_env_name(&var.name) { bail!("invalid environment variable name `{}`", var.name); } - if !TYPES.contains(&var.kind.as_str()) { bail!("unsupported type `{}` for `{}`", var.kind, var.name); } - if var.secret && var.default.is_some() { bail!("secret `{}` must not declare a default value", var.name); } + if !valid_env_name(&var.name) { + bail!("invalid environment variable name `{}`", var.name); + } + if !TYPES.contains(&var.kind.as_str()) { + bail!("unsupported type `{}` for `{}`", var.kind, var.name); + } + if var.secret && var.default.is_some() { + bail!("secret `{}` must not declare a default value", var.name); + } if let Some(default) = &var.default { - validate_value(var, default).with_context(|| format!("invalid default for `{}`", var.name))?; + validate_value(var, default) + .with_context(|| format!("invalid default for `{}`", var.name))?; } if let Some(replacement) = &var.replacement { - if !var.deprecated { bail!("`{}` declares replacement without deprecated=true", var.name); } - if !vars.contains_key(replacement) { bail!("replacement `{replacement}` for `{}` is not declared", var.name); } + if !var.deprecated { + bail!( + "`{}` declares replacement without deprecated=true", + var.name + ); + } + if !vars.contains_key(replacement) { + bail!( + "replacement `{replacement}` for `{}` is not declared", + var.name + ); + } } if let Some(condition) = &var.required_when { let (dependency, _, _) = parse_condition(condition)?; - if !vars.contains_key(dependency) { bail!("required_when for `{}` references undeclared `{dependency}`", var.name); } + if !vars.contains_key(dependency) { + bail!( + "required_when for `{}` references undeclared `{dependency}`", + var.name + ); + } } } Ok(()) @@ -135,24 +223,53 @@ fn check_values(vars: &BTreeMap) -> Result<()> { let snapshot = env::vars().collect::>(); let mut missing = BTreeSet::new(); for var in vars.values() { - let needed = var.required || var.required_when.as_deref().map(|c| condition_matches(c, &snapshot)).transpose()?.unwrap_or(false); + let needed = var.required + || var + .required_when + .as_deref() + .map(|c| condition_matches(c, &snapshot)) + .transpose()? + .unwrap_or(false); match snapshot.get(&var.name) { - Some(value) => validate_value(var, value).with_context(|| format!("invalid value for `{}`", var.name))?, - None if needed && var.default.is_none() => { missing.insert(var.name.clone()); }, + Some(value) => validate_value(var, value) + .with_context(|| format!("invalid value for `{}`", var.name))?, + None if needed && var.default.is_none() => { + missing.insert(var.name.clone()); + } None => {} } } - if !missing.is_empty() { bail!("missing required environment variables: {}", missing.into_iter().collect::>().join(", ")); } + if !missing.is_empty() { + bail!( + "missing required environment variables: {}", + missing.into_iter().collect::>().join(", ") + ); + } println!("environment contract ok ({} variables)", vars.len()); Ok(()) } fn validate_value(var: &EnvVar, value: &str) -> Result<()> { match var.kind.as_str() { - "integer" => { value.parse::().context("expected integer")?; } - "boolean" => { if !matches!(value.to_ascii_lowercase().as_str(), "true"|"false"|"1"|"0"|"yes"|"no"|"on"|"off") { bail!("expected boolean"); } } - "json" => { serde_json::from_str::(value).context("expected JSON")?; } - "url" => { if !value.contains("://") { bail!("expected absolute URL"); } } + "integer" => { + value.parse::().context("expected integer")?; + } + "boolean" => { + if !matches!( + value.to_ascii_lowercase().as_str(), + "true" | "false" | "1" | "0" | "yes" | "no" | "on" | "off" + ) { + bail!("expected boolean"); + } + } + "json" => { + serde_json::from_str::(value).context("expected JSON")?; + } + "url" => { + if !value.contains("://") { + bail!("expected absolute URL"); + } + } "string" => {} _ => unreachable!(), } @@ -165,15 +282,27 @@ fn validate_value(var: &EnvVar, value: &str) -> Result<()> { fn condition_matches(condition: &str, values: &BTreeMap) -> Result { let (name, op, expected) = parse_condition(condition)?; let actual = values.get(name).map(String::as_str).unwrap_or(""); - Ok(if op == "==" { actual == expected } else { actual != expected }) + Ok(if op == "==" { + actual == expected + } else { + actual != expected + }) } fn parse_condition(input: &str) -> Result<(&str, &str, &str)> { - let (name, op, rhs) = if let Some((a,b)) = input.split_once(" == ") { (a.trim(), "==", b.trim()) } - else if let Some((a,b)) = input.split_once(" != ") { (a.trim(), "!=", b.trim()) } - else { bail!("required_when supports only `VAR == 'value'` and `VAR != 'value'`"); }; - if !valid_env_name(name) { bail!("invalid variable `{name}` in required_when"); } - let expected = rhs.strip_prefix('\'').and_then(|v| v.strip_suffix('\'')) + let (name, op, rhs) = if let Some((a, b)) = input.split_once(" == ") { + (a.trim(), "==", b.trim()) + } else if let Some((a, b)) = input.split_once(" != ") { + (a.trim(), "!=", b.trim()) + } else { + bail!("required_when supports only `VAR == 'value'` and `VAR != 'value'`"); + }; + if !valid_env_name(name) { + bail!("invalid variable `{name}` in required_when"); + } + let expected = rhs + .strip_prefix('\'') + .and_then(|v| v.strip_suffix('\'')) .or_else(|| rhs.strip_prefix('"').and_then(|v| v.strip_suffix('"'))) .context("required_when value must be quoted")?; Ok((name, op, expected)) @@ -181,21 +310,44 @@ fn parse_condition(input: &str) -> Result<(&str, &str, &str)> { fn valid_env_name(name: &str) -> bool { let mut chars = name.chars(); - matches!(chars.next(), Some('A'..='Z' | '_')) && chars.all(|c| matches!(c, 'A'..='Z' | '0'..='9' | '_')) + matches!(chars.next(), Some('A'..='Z' | '_')) + && chars.all(|c| matches!(c, 'A'..='Z' | '0'..='9' | '_')) } fn requirement(var: &EnvVar) -> String { - if var.required { "required".into() } - else if let Some(c) = &var.required_when { format!("required when {c}") } - else { "optional".into() } + if var.required { + "required".into() + } else if let Some(c) = &var.required_when { + format!("required when {c}") + } else { + "optional".into() + } } fn string<'a>(table: &'a toml::map::Map, key: &str) -> Result<&'a str> { - table.get(key).and_then(Value::as_str).with_context(|| format!("missing string `{key}` in [[env.vars]]")) + table + .get(key) + .and_then(Value::as_str) + .with_context(|| format!("missing string `{key}` in [[env.vars]]")) } fn opt_string(table: &toml::map::Map, key: &str) -> Result> { - table.get(key).map(|v| v.as_str().map(str::to_owned).with_context(|| format!("`{key}` must be a string"))).transpose() + table + .get(key) + .map(|v| { + v.as_str() + .map(str::to_owned) + .with_context(|| format!("`{key}` must be a string")) + }) + .transpose() } fn opt_scalar_string(table: &toml::map::Map, key: &str) -> Result> { - table.get(key).map(|v| match v { Value::String(s) => Ok(s.clone()), Value::Integer(v) => Ok(v.to_string()), Value::Boolean(v) => Ok(v.to_string()), _ => bail!("`{key}` must be a string, integer, or boolean") }).transpose() + table + .get(key) + .map(|v| match v { + Value::String(s) => Ok(s.clone()), + Value::Integer(v) => Ok(v.to_string()), + Value::Boolean(v) => Ok(v.to_string()), + _ => bail!("`{key}` must be a string, integer, or boolean"), + }) + .transpose() } From b24afd0c1401b3d8e727c21f4c7627b88e85d1c4 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Tue, 15 Sep 2026 14:04:30 -0500 Subject: [PATCH 2/3] chore: rustfmt env contract tests --- tests/env_contract.rs | 52 ++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/tests/env_contract.rs b/tests/env_contract.rs index 3680dc7f..8051fdc2 100644 --- a/tests/env_contract.rs +++ b/tests/env_contract.rs @@ -1,7 +1,14 @@ -use std::{fs, process::Command, time::{SystemTime, UNIX_EPOCH}}; +use std::{ + fs, + process::Command, + time::{SystemTime, UNIX_EPOCH}, +}; fn fixture(contents: &str) -> std::path::PathBuf { - let suffix = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); let dir = std::env::temp_dir().join(format!("zed-env-contract-{suffix}")); fs::create_dir_all(&dir).unwrap(); let path = dir.join(".zpkg.toml"); @@ -11,7 +18,8 @@ fn fixture(contents: &str) -> std::path::PathBuf { #[test] fn accepts_conditional_requirements_when_not_selected() { - let manifest = fixture(r#" + let manifest = fixture( + r#" [package] name = "locks" @@ -25,17 +33,22 @@ name = "REDIS_URL" type = "url" secret = true required_when = "ORES_LOCK_BACKEND == 'redis'" -"#); +"#, + ); let status = Command::new(env!("CARGO_BIN_EXE_zed-env-contract")) - .arg("check").arg(&manifest) - .env_remove("ORES_LOCK_BACKEND").env_remove("REDIS_URL") - .status().unwrap(); + .arg("check") + .arg(&manifest) + .env_remove("ORES_LOCK_BACKEND") + .env_remove("REDIS_URL") + .status() + .unwrap(); assert!(status.success()); } #[test] fn requires_conditionally_selected_secret() { - let manifest = fixture(r#" + let manifest = fixture( + r#" [[env.vars]] name = "ORES_LOCK_BACKEND" enum = ["postgres", "redis"] @@ -45,25 +58,34 @@ name = "REDIS_URL" type = "url" secret = true required_when = "ORES_LOCK_BACKEND == 'redis'" -"#); +"#, + ); let output = Command::new(env!("CARGO_BIN_EXE_zed-env-contract")) - .arg("check").arg(&manifest) - .env("ORES_LOCK_BACKEND", "redis").env_remove("REDIS_URL") - .output().unwrap(); + .arg("check") + .arg(&manifest) + .env("ORES_LOCK_BACKEND", "redis") + .env_remove("REDIS_URL") + .output() + .unwrap(); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("REDIS_URL")); } #[test] fn rejects_secret_defaults() { - let manifest = fixture(r#" + let manifest = fixture( + r#" [[env.vars]] name = "TOKEN" secret = true default = "do-not-do-this" -"#); +"#, + ); let output = Command::new(env!("CARGO_BIN_EXE_zed-env-contract")) - .arg("check").arg(&manifest).output().unwrap(); + .arg("check") + .arg(&manifest) + .output() + .unwrap(); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("must not declare a default")); } From 55deca4734f6b450722f3ecf188bbc1ead04e621 Mon Sep 17 00:00:00 2001 From: Alexander Mills Date: Tue, 15 Sep 2026 14:04:47 -0500 Subject: [PATCH 3/3] chore: rustfmt TOML contract tests --- tests/toml_contract.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/toml_contract.rs b/tests/toml_contract.rs index 04b406f4..1a4ec8bb 100644 --- a/tests/toml_contract.rs +++ b/tests/toml_contract.rs @@ -9,8 +9,7 @@ fn root() -> PathBuf { fn parse_toml(path: &Path) -> toml::Value { let text = fs::read_to_string(path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); - toml::from_str(&text) - .unwrap_or_else(|error| panic!("invalid TOML {}: {error}", path.display())) + toml::from_str(&text).unwrap_or_else(|error| panic!("invalid TOML {}: {error}", path.display())) } fn string_at<'a>(value: &'a toml::Value, path: &[&str]) -> &'a str { @@ -133,7 +132,10 @@ fn global_flags_do_not_reuse_environment_keys_or_public_aliases() { if alias == canonical_alias { continue; } - assert!(aliases.insert(alias.to_owned()), "duplicate global flag alias: {alias}"); + assert!( + aliases.insert(alias.to_owned()), + "duplicate global flag alias: {alias}" + ); } } }