From bfa4a0bd6f17ca399bbc1917df15241df792550c Mon Sep 17 00:00:00 2001 From: Luca Ferrazzini Date: Thu, 20 Nov 2025 08:08:32 +0100 Subject: [PATCH] feat: add config validation sub command Signed-off-by: Luca Ferrazzini --- Cargo.lock | 152 ++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + crates/wash/src/cli/config.rs | 125 ++++++++++++++++++++++++++-- crates/wash/src/config.rs | 2 +- 4 files changed, 274 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fa286a6..a22314e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,7 +44,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.3", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -589,6 +591,12 @@ dependencies = [ "piper", ] +[[package]] +name = "borrow-or-share" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" + [[package]] name = "bumpalo" version = "3.19.0" @@ -598,6 +606,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytemuck" version = "1.23.2" @@ -1609,6 +1623,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -1728,6 +1751,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1814,6 +1848,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1868,6 +1913,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static 1.5.0", + "num", +] + [[package]] name = "fs-set-times" version = "0.20.3" @@ -2840,6 +2895,33 @@ dependencies = [ "serde", ] +[[package]] +name = "jsonschema" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d46662859bc5f60a145b75f4632fbadc84e829e45df6c5de74cfc8e05acb96b5" +dependencies = [ + "ahash", + "base64 0.22.1", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "num-traits", + "once_cell", + "percent-encoding", + "referencing", + "regex", + "regex-syntax", + "reqwest", + "serde", + "serde_json", + "uuid-simd", +] + [[package]] name = "jwt" version = "0.16.0" @@ -3382,6 +3464,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + [[package]] name = "num-complex" version = "0.4.6" @@ -3693,6 +3781,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "p256" version = "0.13.2" @@ -4549,6 +4643,20 @@ dependencies = [ "syn", ] +[[package]] +name = "referencing" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9c261f7ce75418b3beadfb3f0eb1299fe8eb9640deba45ffa2cb783098697d" +dependencies = [ + "ahash", + "fluent-uri", + "once_cell", + "parking_lot", + "percent-encoding", + "serde_json", +] + [[package]] name = "regalloc2" version = "0.13.3" @@ -4601,6 +4709,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -4894,10 +5003,23 @@ checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ "dyn-clone", "ref-cast", + "schemars_derive", "serde", "serde_json", ] +[[package]] +name = "schemars_derive" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -5022,6 +5144,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" version = "1.0.143" @@ -6072,6 +6205,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + [[package]] name = "valuable" version = "0.1.1" @@ -6084,6 +6228,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -6261,10 +6411,12 @@ dependencies = [ "http", "http-body-util", "indicatif", + "jsonschema", "notify", "pbjson-build 0.8.0", "rcgen", "reqwest", + "schemars 1.0.4", "scopeguard", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index 00f0850f..2b537c33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,6 +148,8 @@ anyhow = { workspace = true, default-features = true } tonic-prost-build = { workspace = true, default-features = true } pbjson-build = { workspace = true, default-features = true } +jsonschema = "0.33.0" +schemars = "1.0.4" [dev-dependencies] bytes = { version = "1", default-features = false } diff --git a/crates/wash/src/cli/config.rs b/crates/wash/src/cli/config.rs index c7ae331e..b0d50574 100644 --- a/crates/wash/src/cli/config.rs +++ b/crates/wash/src/cli/config.rs @@ -1,11 +1,12 @@ -use anyhow::Context as _; -use clap::Subcommand; -use tracing::instrument; - use crate::{ cli::{CliCommand, CliContext, CommandOutput}, - config::{generate_default_config, local_config_path}, + config::{Config, generate_default_config, local_config_path}, }; +use anyhow::Context as _; +use clap::Subcommand; +use std::path::PathBuf; +use tracing::instrument; +use url::Url; /// Create a new component project from a template, git repository, or local path #[derive(Subcommand, Debug, Clone)] @@ -23,7 +24,18 @@ pub enum ConfigCommand { Info {}, /// Print the current configuration file for wash Show {}, - // TODO(#27): validate config command + /// Validate the current configuration + #[clap(group = clap::ArgGroup::new("validate_targets") + .required(true) + .multiple(false))] + Validate { + /// Path to specific config file to validate (optional) + #[clap(long, group = "validate_targets")] + file: Option, + /// Validate project config instead of global config + #[clap(long, group = "validate_targets")] + project: bool, + }, // TODO(#29): cleanup config command, to clean the dirs we use } @@ -83,6 +95,107 @@ impl CliCommand for ConfigCommand { Some(serde_json::to_value(&config).context("failed to serialize config")?), )) } + ConfigCommand::Validate { file, project } => { + let validation_path: PathBuf = match file { + Some(path) => path.clone(), + None => { + if *project { + local_config_path( + &std::env::current_dir().context("failed to get current dir")?, + ) + } else { + ctx.config_path() + } + } + }; + + if !validation_path.exists() { + return Ok(CommandOutput::error( + format!( + "No configuration file found at {}", + validation_path.display() + ), + Some(serde_json::json!({ + "message": "Configuration file not found.", + "success": false, + })), + )); + }; + + let content = std::fs::read_to_string(&validation_path).context(format!( + "Failed to read file: {}", + validation_path.display() + ))?; + + let config: Config = match serde_json::from_str(&content) { + Ok(config) => config, + Err(e) => { + return Ok(CommandOutput::error( + format!("Json is NOT valid and cannot be parsed. With error {e}"), + Some(serde_json::json!({ + "message": "Json is NOT valid and cannot be parsed.", + "success": false, + + })), + )); + } + }; + + for template in config.templates { + match Url::parse(&template.repository) { + Ok(_) => {} + Err(_) => { + return Ok(CommandOutput::error( + format!( + "The repository Url is NOT valid for template {}", + template.name + ), + Some(serde_json::json!({ + "message": format!("The repository Url is NOT valid for template {}", template.name), + "success": false, + })), + )); + } + } + } + if let Some(wit_config) = config.wit { + for reg in wit_config.registries { + match Url::parse(®.url) { + Ok(_) => {} + Err(_) => { + return Ok(CommandOutput::error( + format!("The wit registry {} is not a valid Url", reg.url), + Some(serde_json::json!({ + "message": format!("The wit registry {} is not a valid Url", reg.url), + "success": false, + })), + )); + } + } + } + if let Some(dir_path) = &wit_config.wit_dir + && !dir_path.exists() + { + return Ok(CommandOutput::error( + format!("Wit directory {} does not exist", dir_path.display()), + Some(serde_json::json!({ + "message": format!("Wit directory {} does not exist", dir_path.display()), + "success": false, + })), + )); + } + } + let success_message = + format!("Configuration file is valid: {}", validation_path.display()); + + Ok(CommandOutput::ok( + &success_message, + Some(serde_json::json!({ + "message": success_message, + "success": true, + })), + )) + } } } } diff --git a/crates/wash/src/config.rs b/crates/wash/src/config.rs index fa4dbaaa..c9b58d95 100644 --- a/crates/wash/src/config.rs +++ b/crates/wash/src/config.rs @@ -241,7 +241,7 @@ where /// Get the local project configuration file path pub fn local_config_path(project_dir: &Path) -> PathBuf { - project_dir.join(".wash").join(CONFIG_FILE_NAME) + project_dir.join(PROJECT_CONFIG_DIR).join(CONFIG_FILE_NAME) } /// Generate a default configuration file with all explicit defaults