diff --git a/Cargo.lock b/Cargo.lock index 3e2b9a12ac..f56eaf4e88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1774,6 +1774,7 @@ dependencies = [ "anyhow", "async-trait", "carbide-test-support", + "carbide-utils", "carbide-uuid", "clap", "dashmap", @@ -1865,6 +1866,7 @@ dependencies = [ "carbide-dpu-agent-utils", "carbide-rpc", "carbide-tls", + "carbide-utils", "carbide-uuid", "eyre", "rand 0.10.1", @@ -2837,6 +2839,7 @@ dependencies = [ "carbide-health-report", "carbide-rpc", "carbide-test-support", + "carbide-utils", "carbide-uuid", "chrono", "config-version", diff --git a/crates/admin-cli/src/machine/nvlink_info/cmd.rs b/crates/admin-cli/src/machine/nvlink_info/cmd.rs index 3aefe4139d..3ca2e6a1c7 100644 --- a/crates/admin-cli/src/machine/nvlink_info/cmd.rs +++ b/crates/admin-cli/src/machine/nvlink_info/cmd.rs @@ -17,6 +17,7 @@ use ::rpc::admin_cli::OutputFormat; use ::rpc::forge as forgerpc; +use carbide_utils::none_if_empty::NoneIfEmpty; use super::args::{NvlinkInfoArgs, NvlinkInfoPopulateArgs}; use crate::errors::{CarbideCliError, CarbideCliResult}; @@ -149,7 +150,7 @@ pub async fn handle_nvlink_info_populate( .get("server_header") .and_then(|h| h.get("domain_uuid")) .and_then(|v| v.as_str()) - .filter(|s| !s.is_empty()) + .none_if_empty() .ok_or_else(|| { CarbideCliError::GenericError("No domain_uuid in NMX-C server_header".to_string()) })? diff --git a/crates/admin-cli/src/managed_switch/show/cmd.rs b/crates/admin-cli/src/managed_switch/show/cmd.rs index 8aff627502..90a4c0d1e1 100644 --- a/crates/admin-cli/src/managed_switch/show/cmd.rs +++ b/crates/admin-cli/src/managed_switch/show/cmd.rs @@ -18,6 +18,7 @@ use std::collections::HashMap; use std::fmt::Write; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::switch::SwitchId; use prettytable::{Cell, Row, Table}; use rpc::admin_cli::OutputFormat; @@ -386,19 +387,19 @@ fn show_managed_switch_details_view(m: ManagedSwitchOutput) -> CarbideCliResult< (" ID", m.switch_id), (" Slot Number", m.slot_number.map(|n| n.to_string())), (" Tray Index", m.tray_index.map(|n| n.to_string())), - (" Serial Number", non_empty(m.serial_number)), + (" Serial Number", m.serial_number.none_if_empty()), (" Rack ID", m.rack_id), (" Power State", m.power_state), (" Health", m.health_status), ( " NVOS MAC Addresses", - non_empty(m.nvos_mac_addresses.join(", ")), + m.nvos_mac_addresses.join(", ").none_if_empty(), ), (" BMC", Some(String::new())), (" Version", m.bmc_version), (" Firmware Version", m.bmc_firmware_version), (" IP", m.bmc_ip), - (" MAC", non_empty(m.bmc_mac)), + (" MAC", m.bmc_mac.none_if_empty()), ]; for (key, value) in data { @@ -418,10 +419,6 @@ fn show_managed_switch_details_view(m: ManagedSwitchOutput) -> CarbideCliResult< Ok(()) } -fn non_empty(s: String) -> Option { - if s.is_empty() { None } else { Some(s) } -} - pub async fn handle_show( output_file: &mut Box, args: Args, diff --git a/crates/admin-cli/src/site_explorer/get_report/cmd.rs b/crates/admin-cli/src/site_explorer/get_report/cmd.rs index c46de19f3b..c9049a4ae4 100644 --- a/crates/admin-cli/src/site_explorer/get_report/cmd.rs +++ b/crates/admin-cli/src/site_explorer/get_report/cmd.rs @@ -21,6 +21,7 @@ use ::rpc::admin_cli::OutputFormat; use ::rpc::site_explorer::{ EndpointExplorationReport, ExploredEndpoint, ExploredManagedHost, SiteExplorationReport, }; +use carbide_utils::none_if_empty::NoneIfEmpty; use prettytable::{Cell, Row, Table, format, row}; use super::args::Args; @@ -484,7 +485,7 @@ fn endpoint_to_row(endpoint: &ExploredEndpoint) -> Row { .map(|a| { let mac = a.mac_address.as_deref().unwrap_or_default(); if a.interface_enabled() { - if let Some(ls) = a.link_status.as_deref().filter(|v| !v.is_empty()) { + if let Some(ls) = a.link_status.as_deref().none_if_empty() { format!("{mac} [{}]", ls) } else { mac.to_string() diff --git a/crates/admin-cli/src/sku/bulk_update_metadata/cmd.rs b/crates/admin-cli/src/sku/bulk_update_metadata/cmd.rs index e0c4d9fe30..0b45b9fe26 100644 --- a/crates/admin-cli/src/sku/bulk_update_metadata/cmd.rs +++ b/crates/admin-cli/src/sku/bulk_update_metadata/cmd.rs @@ -15,6 +15,8 @@ * limitations under the License. */ +use carbide_utils::none_if_empty::NoneIfEmpty; + use super::super::update_metadata; use super::args::Args; use crate::errors::{CarbideCliError, CarbideCliResult}; @@ -43,8 +45,8 @@ pub async fn bulk_update_metadata(args: Args, api_client: &ApiClient) -> Carbide tracing::error!("No SKU ID at line {current_line}"); continue; }; - let device_type = data.get(1).filter(|s| !s.is_empty()).map(str::to_owned); - let description = data.get(2).filter(|s| !s.is_empty()).map(str::to_owned); + let device_type = data.get(1).none_if_empty().map(str::to_owned); + let description = data.get(2).none_if_empty().map(str::to_owned); // log errors but don't stop the processing if let Err(e) = api_client diff --git a/crates/agent/src/astra_weave.rs b/crates/agent/src/astra_weave.rs index 79d4b8ca95..b3f4c3b969 100644 --- a/crates/agent/src/astra_weave.rs +++ b/crates/agent/src/astra_weave.rs @@ -21,6 +21,7 @@ use ::rpc::forge::{ AstraAttachmentStatus, AstraConfig, AstraConfigStatus, AstraPhase, AstraStatus, SpxAttachmentType, }; +use carbide_utils::none_if_empty::NoneIfEmpty; use eyre::WrapErr; use crate::weave_ew_vpc_client::proto::state::Phase; @@ -164,7 +165,7 @@ async fn create_weave_ew_vpc_virtual_networks( .metadata .as_ref() .and_then(|metadata| metadata.id.clone()) - .filter(|id| !id.is_empty()); + .none_if_empty(); let state = virtual_network .status .as_ref() @@ -323,7 +324,7 @@ async fn delete_stale_weave_ew_vpc_virtual_networks( .metadata .as_ref() .and_then(|metadata| metadata.id.clone()) - .filter(|id| !id.is_empty()) + .none_if_empty() else { tracing::error!( ?virtual_network, @@ -602,7 +603,7 @@ async fn delete_stale_weave_ew_vpc_astra_attachments( .metadata .as_ref() .and_then(|metadata| metadata.id.clone()) - .filter(|id| !id.is_empty()) + .none_if_empty() else { tracing::error!( ?virtual_network_attachment, @@ -668,7 +669,7 @@ pub async fn delete_match_attachment_with_vni_changed( let Some(delete_attachment_id) = match_attachment .and_then(|attachment| attachment.metadata.as_ref()) .and_then(|metadata| metadata.id.clone()) - .filter(|id| !id.is_empty()) + .none_if_empty() else { tracing::error!( ?match_attachment, diff --git a/crates/agent/src/dpu/interface.rs b/crates/agent/src/dpu/interface.rs index 9765e371c6..073fba625b 100644 --- a/crates/agent/src/dpu/interface.rs +++ b/crates/agent/src/dpu/interface.rs @@ -19,6 +19,7 @@ use std::fmt::Debug; use std::net::IpAddr; use std::path::PathBuf; +use carbide_utils::none_if_empty::NoneIfEmpty; use eyre::WrapErr; use ipnetwork::IpNetwork; use serde::{Deserialize, Serialize}; @@ -225,7 +226,7 @@ impl Interface { .copied() .collect::>(); - if res.is_empty() { None } else { Some(res) } + res.none_if_empty() } } diff --git a/crates/agent/src/ethernet_virtualization.rs b/crates/agent/src/ethernet_virtualization.rs index 2b9cd8be18..89975ebe27 100644 --- a/crates/agent/src/ethernet_virtualization.rs +++ b/crates/agent/src/ethernet_virtualization.rs @@ -1937,6 +1937,7 @@ mod tests { use ::rpc::{common as rpc_common, forge as rpc}; use carbide_network::virtualization::{VpcVirtualizationType, get_svi_ip}; use carbide_rpc_utils::dhcp::{DhcpConfig, HostConfig}; + use carbide_utils::none_if_empty::NoneIfEmpty; use eyre::WrapErr; use ipnetwork::IpNetwork; @@ -3863,26 +3864,26 @@ mod tests { let interface_prefix_v6 = Some("2001:db8::/127".to_string()); let addresses: Vec = std::iter::once(ip.clone()) - .chain(ip6.filter(|s| !s.is_empty())) + .chain(ip6.none_if_empty()) .collect(); assert_eq!(addresses, vec!["10.0.0.1", "2001:db8::1"]); let prefixes: Vec = std::iter::once(interface_prefix) - .chain(interface_prefix_v6.filter(|s| !s.is_empty())) + .chain(interface_prefix_v6.none_if_empty()) .collect(); assert_eq!(prefixes, vec!["10.0.0.0/31", "2001:db8::/127"]); // Verify empty ip6 is not included. let empty_ip6: Option = Some("".to_string()); let addresses2: Vec = std::iter::once(ip) - .chain(empty_ip6.filter(|s| !s.is_empty())) + .chain(empty_ip6.none_if_empty()) .collect(); assert_eq!(addresses2, vec!["10.0.0.1"]); // Verify None ip6 is not included. let none_ip6: Option = None; let addresses3: Vec = std::iter::once("10.0.0.1".to_string()) - .chain(none_ip6.filter(|s| !s.is_empty())) + .chain(none_ip6.none_if_empty()) .collect(); assert_eq!(addresses3, vec!["10.0.0.1"]); } diff --git a/crates/api-core/src/cfg/file.rs b/crates/api-core/src/cfg/file.rs index 2d657ebc07..e01ab5ecdc 100644 --- a/crates/api-core/src/cfg/file.rs +++ b/crates/api-core/src/cfg/file.rs @@ -40,6 +40,7 @@ use carbide_rack_controller::config::{RackValidationConfig, RmsConfig}; use carbide_site_explorer::config::SiteExplorerConfig; use carbide_state_controller_common::config::StateControllerConfig; use carbide_utils::config::{as_duration, as_option_duration, as_std_duration}; +use carbide_utils::none_if_empty::NoneIfEmpty; use chrono::Duration; use db::host_naming::HostNamingStrategyKind; use duration_str::{deserialize_duration, deserialize_duration_chrono}; @@ -2991,7 +2992,7 @@ impl TrafficInterceptBridging { .join(","); // An empty map, or one with only skipped entries, means no provisioning config. - (!config.is_empty()).then_some(config) + config.none_if_empty() } } diff --git a/crates/api-core/src/ethernet_virtualization.rs b/crates/api-core/src/ethernet_virtualization.rs index 984ac9034f..9c60ba66c5 100644 --- a/crates/api-core/src/ethernet_virtualization.rs +++ b/crates/api-core/src/ethernet_virtualization.rs @@ -18,6 +18,7 @@ use std::net::{IpAddr, Ipv4Addr}; use ::rpc::forge as rpc; use carbide_network::virtualization::{VpcVirtualizationType, get_svi_ip}; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::instance::InstanceId; use carbide_uuid::machine::{MachineId, MachineInterfaceId}; use carbide_uuid::network::NetworkSegmentId; @@ -71,11 +72,7 @@ impl SiteFabricPrefixList { // the VPC isolation feature built on top of it, and it is better not // to construct one of these at all (and thus the Option-wrapped return // type). - if prefixes.is_empty() { - None - } else { - Some(Self { prefixes }) - } + prefixes.none_if_empty().map(|prefixes| Self { prefixes }) } pub fn as_ip_slice(&self) -> &[IpNetwork] { diff --git a/crates/api-core/src/handlers/bmc_endpoint_explorer.rs b/crates/api-core/src/handlers/bmc_endpoint_explorer.rs index a430721ab6..ea710ed652 100644 --- a/crates/api-core/src/handlers/bmc_endpoint_explorer.rs +++ b/crates/api-core/src/handlers/bmc_endpoint_explorer.rs @@ -20,6 +20,7 @@ use std::net::SocketAddr; use ::rpc::forge as rpc; use ::rpc::model::machine::machine_id::try_parse_machine_id; use carbide_redfish::boot_interface::BootInterfaceTarget; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::machine::MachineId; use db::WithTransaction; use db::machine_interface::find_by_ip; @@ -438,7 +439,7 @@ pub(crate) async fn machine_setup( .boot_interface_mac .as_deref() .map(str::trim) - .filter(|m| !m.is_empty()) + .none_if_empty() .map(|m| m.parse::()) .transpose() .map_err(|e| CarbideError::InvalidArgument(format!("invalid boot_interface_mac: {e}")))?; @@ -494,7 +495,7 @@ pub(crate) async fn set_dpu_first_boot_order( .boot_interface_mac .as_deref() .map(str::trim) - .filter(|m| !m.is_empty()) + .none_if_empty() .map(|m| m.parse::()) .transpose() .map_err(|e| CarbideError::InvalidArgument(format!("invalid boot_interface_mac: {e}")))?; diff --git a/crates/api-core/src/handlers/component_manager.rs b/crates/api-core/src/handlers/component_manager.rs index 5dbb72fa7c..2ce59fbadc 100644 --- a/crates/api-core/src/handlers/component_manager.rs +++ b/crates/api-core/src/handlers/component_manager.rs @@ -24,6 +24,7 @@ use carbide_rack::firmware_object::rms_access_token_or_noauth; use carbide_secrets::credentials::{ BmcCredentialType, CredentialKey, CredentialManager, Credentials, }; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::machine::MachineId; use carbide_uuid::power_shelf::PowerShelfId; use carbide_uuid::rack::RackId; @@ -976,7 +977,7 @@ async fn resolve_switch_endpoints( nvos_mac, bmc_credentials, nvos_credentials, - nvos_host_name: row.nvos_hostname.filter(|hostname| !hostname.is_empty()), + nvos_host_name: row.nvos_hostname.none_if_empty(), }); } diff --git a/crates/api-core/src/handlers/extension_service.rs b/crates/api-core/src/handlers/extension_service.rs index 56a7c7dc47..b78e655782 100644 --- a/crates/api-core/src/handlers/extension_service.rs +++ b/crates/api-core/src/handlers/extension_service.rs @@ -17,6 +17,7 @@ use ::rpc::errors::RpcDataConversionError; use ::rpc::forge as rpc; use carbide_secrets::credentials::{CredentialKey, Credentials}; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::extension_service::ExtensionServiceId; use config_version::ConfigVersion; use db::{WithTransaction, extension_service, instance}; @@ -635,22 +636,18 @@ pub(crate) async fn get_versions_info( let req = request.into_inner(); // Parse versions from strings to ConfigVersions - let versions: Option> = if !req.versions.is_empty() { - let versions = req - .versions - .iter() - .map(|v| v.parse::()) - .collect::, _>>() - .map_err(|e| { - CarbideError::from(RpcDataConversionError::InvalidConfigVersion(format!( - "Failed to parse version: {}", - e - ))) - })?; - Some(versions) - } else { - None - }; + let versions: Option> = req + .versions + .iter() + .map(|v| v.parse::()) + .collect::, _>>() + .map_err(|e| { + CarbideError::from(RpcDataConversionError::InvalidConfigVersion(format!( + "Failed to parse version: {}", + e + ))) + })? + .none_if_empty(); let mut txn = api.txn_begin().await?; diff --git a/crates/api-core/src/handlers/machine_discovery.rs b/crates/api-core/src/handlers/machine_discovery.rs index d9c81b9d3b..e0cff00ef2 100644 --- a/crates/api-core/src/handlers/machine_discovery.rs +++ b/crates/api-core/src/handlers/machine_discovery.rs @@ -21,6 +21,7 @@ use std::sync::Arc; use std::sync::atomic::Ordering; pub use ::rpc::forge as rpc; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::machine::MachineIdSource; use carbide_uuid::nvlink::NvLinkDomainId; use db::WithTransaction; @@ -433,7 +434,7 @@ pub(crate) async fn discover_machine( && let Some(scout_version) = machine_discovery_info .discovery_reporter_version .as_deref() - .filter(|v| !v.is_empty()) + .none_if_empty() { db::machine::update_last_scout_observed_version( &stable_machine_id, diff --git a/crates/api-core/src/handlers/machine_identity.rs b/crates/api-core/src/handlers/machine_identity.rs index 58b465f90a..00ae95ed57 100644 --- a/crates/api-core/src/handlers/machine_identity.rs +++ b/crates/api-core/src/handlers/machine_identity.rs @@ -23,6 +23,7 @@ use ::rpc::forge::{ self as rpc, Jwks, JwksKind, JwksRequest, MachineIdentityResponse, OpenIdConfigRequest, OpenIdConfiguration, }; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::machine::MachineId; use chrono::Utc; use db::{WithTransaction, tenant_identity_config}; @@ -211,14 +212,11 @@ pub(crate) async fn sign_machine_identity( let now = Utc::now().timestamp(); if let (Some(token_endpoint), Some(subject_token_audience), Some(auth_method)) = ( - identity_row - .token_endpoint - .as_deref() - .filter(|u| !u.is_empty()), + identity_row.token_endpoint.as_deref().none_if_empty(), identity_row .subject_token_audience .as_deref() - .filter(|a| !a.is_empty()), + .none_if_empty(), identity_row.auth_method, ) { let subject_ttl = i64::from(identity_row.token_ttl_sec); diff --git a/crates/api-core/src/handlers/tenant_identity_config.rs b/crates/api-core/src/handlers/tenant_identity_config.rs index 8ccf2c170b..443317ecd0 100644 --- a/crates/api-core/src/handlers/tenant_identity_config.rs +++ b/crates/api-core/src/handlers/tenant_identity_config.rs @@ -32,6 +32,7 @@ use ::rpc::forge::{ }; use carbide_secrets::credentials::CredentialReader; use carbide_secrets::key_encryption; +use carbide_utils::none_if_empty::NoneIfEmpty; use db::{WithTransaction, tenant, tenant_identity_config}; use model::tenant::identity_config::TenantIdentityCurrentSigningKeySlot; use model::tenant::{ @@ -608,7 +609,7 @@ async fn reencrypt_one_field( target_aes: &key_encryption::Aes256Key, dry_run: bool, ) -> Result { - let Some(ciphertext) = ciphertext.filter(|s| !s.is_empty()) else { + let Some(ciphertext) = ciphertext.none_if_empty() else { return Ok(ReencryptFieldResult::Absent); }; match reencrypt_ciphertext_if_needed( diff --git a/crates/api-core/src/machine_identity/token_exchange.rs b/crates/api-core/src/machine_identity/token_exchange.rs index 789cfdcef6..07bb1b06d8 100644 --- a/crates/api-core/src/machine_identity/token_exchange.rs +++ b/crates/api-core/src/machine_identity/token_exchange.rs @@ -21,6 +21,7 @@ use std::time::Duration; use ::rpc::forge::MachineIdentityResponse; use base64::Engine; +use carbide_utils::none_if_empty::NoneIfEmpty; use serde::Deserialize; use tonic::Status; @@ -49,7 +50,7 @@ pub(crate) fn token_exchange_http_client( let mut builder = reqwest::Client::builder() .timeout(Duration::from_secs(30)) .redirect(reqwest::redirect::Policy::none()); - if let Some(proxy_url) = token_endpoint_http_proxy.filter(|s| !s.is_empty()) { + if let Some(proxy_url) = token_endpoint_http_proxy.none_if_empty() { let proxy = reqwest::Proxy::all(proxy_url).map_err(|e| { CarbideError::InvalidArgument(format!( "invalid machine_identity.token_endpoint_http_proxy: {e}" diff --git a/crates/api-core/src/setup.rs b/crates/api-core/src/setup.rs index bb0986ac18..8f880bbb82 100644 --- a/crates/api-core/src/setup.rs +++ b/crates/api-core/src/setup.rs @@ -62,6 +62,7 @@ use carbide_switch_controller::context::SwitchStateHandlerServices; use carbide_switch_controller::handler::SwitchStateHandler; use carbide_switch_controller::io::SwitchStateControllerIO; use carbide_utils::HostPortPair; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_vpc_prefix_controller::context::VpcPrefixStateHandlerServices; use carbide_vpc_prefix_controller::handler::VpcPrefixStateHandler; use carbide_vpc_prefix_controller::io::VpcPrefixStateControllerIO; @@ -1244,22 +1245,23 @@ async fn initialize_and_start_controllers<'a>( emitter_builder.build() }; - let switch_system_image_rms_client = carbide_config - .rms - .api_url - .as_deref() - .filter(|url| !url.is_empty()) - .map(|url| { - let rms_client_config = librms::client_config::RmsClientConfig::new( - carbide_config.rms.root_ca_path.clone(), - carbide_config.rms.client_cert.clone(), - carbide_config.rms.client_key.clone(), - carbide_config.rms.enforce_tls, - ); - let rms_api_config = librms::client::RmsApiConfig::new(url, &rms_client_config); - Arc::new(librms::RackManagerApi::new(&rms_api_config)) - as Arc - }); + let switch_system_image_rms_client = + carbide_config + .rms + .api_url + .as_deref() + .none_if_empty() + .map(|url| { + let rms_client_config = librms::client_config::RmsClientConfig::new( + carbide_config.rms.root_ca_path.clone(), + carbide_config.rms.client_cert.clone(), + carbide_config.rms.client_key.clone(), + carbide_config.rms.enforce_tls, + ); + let rms_api_config = librms::client::RmsApiConfig::new(url, &rms_client_config); + Arc::new(librms::RackManagerApi::new(&rms_api_config)) + as Arc + }); // Use the hostname as cluster-wide state controller ID // The expectation here is that either the host only runs a single diff --git a/crates/api-db/src/machine_validation_suites.rs b/crates/api-db/src/machine_validation_suites.rs index f3da86944f..79f0f8f7cf 100644 --- a/crates/api-db/src/machine_validation_suites.rs +++ b/crates/api-db/src/machine_validation_suites.rs @@ -15,6 +15,7 @@ * limitations under the License. */ +use carbide_utils::none_if_empty::NoneIfEmpty; use config_version::ConfigVersion; use model::machine_validation::{ MachineValidationTest, MachineValidationTestAddRequest, MachineValidationTestUpdatePayload, @@ -30,11 +31,7 @@ use crate::{DatabaseError, DatabaseResult}; /// For slice fields from a patch payload, an empty slice is treated like "omit" (same as historical /// dynamic SQL that skipped empty JSON arrays). fn patch_vec(values: &[String]) -> Option> { - if values.is_empty() { - None - } else { - Some(values.to_vec()) - } + values.to_vec().none_if_empty() } pub async fn find( @@ -117,11 +114,7 @@ pub async fn save( let extra_output_file = req.extra_output_file.clone(); let extra_err_file = req.extra_err_file.clone(); let pre_condition = req.pre_condition.clone(); - let custom_tags = if req.custom_tags.is_empty() { - None::> - } else { - Some(req.custom_tags.clone()) - }; + let custom_tags = req.custom_tags.clone().none_if_empty(); let components = if req.components.is_empty() { vec!["Compute".to_string()] } else { diff --git a/crates/api-model/src/instance/status/extension_service.rs b/crates/api-model/src/instance/status/extension_service.rs index 28c10f6bb0..b4b7d8f39f 100644 --- a/crates/api-model/src/instance/status/extension_service.rs +++ b/crates/api-model/src/instance/status/extension_service.rs @@ -17,6 +17,7 @@ use std::collections::HashMap; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::extension_service::ExtensionServiceId; use carbide_uuid::machine::MachineId; use chrono::{DateTime, Utc}; @@ -85,11 +86,7 @@ impl InstanceExtensionServicesStatus { dpu_statuses.push(MachineExtensionServiceStatus { machine_id: *dpu_id, status: service_status.overall_state.clone(), - error_message: if service_status.message.is_empty() { - None - } else { - Some(service_status.message.clone()) - }, + error_message: service_status.message.clone().none_if_empty(), components: service_status.components.clone(), }); } else { diff --git a/crates/api-model/src/machine_boot_interface.rs b/crates/api-model/src/machine_boot_interface.rs index 8925bca975..02e2abda3f 100644 --- a/crates/api-model/src/machine_boot_interface.rs +++ b/crates/api-model/src/machine_boot_interface.rs @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +use carbide_utils::none_if_empty::NoneIfEmpty; use mac_address::MacAddress; use serde::{Deserialize, Serialize}; @@ -54,7 +55,7 @@ impl MachineBootInterface { ) -> Option { Some(Self { mac_address: mac_address?, - interface_id: interface_id.filter(|s| !s.is_empty())?, + interface_id: interface_id.none_if_empty()?, }) } diff --git a/crates/api-model/src/site_explorer/mod.rs b/crates/api-model/src/site_explorer/mod.rs index 0278f5ea9c..6f5b81fdc7 100644 --- a/crates/api-model/src/site_explorer/mod.rs +++ b/crates/api-model/src/site_explorer/mod.rs @@ -22,6 +22,7 @@ use std::sync::Arc; use carbide_network::BaseMac; use carbide_utils::arch::CpuArchitecture; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::machine::{MachineId, MachineType}; use carbide_uuid::power_shelf::{PowerShelfId, PowerShelfIdSource, PowerShelfType}; use carbide_uuid::switch::{SwitchId, SwitchIdSource, SwitchType}; @@ -171,7 +172,7 @@ impl EndpointExplorationReport { .iter() .flat_map(|s| s.ethernet_interfaces.iter()) .find(|e| e.mac_address == Some(mac)) - .and_then(|e| e.id.as_deref().filter(|id| !id.is_empty())) + .and_then(|e| e.id.as_deref().none_if_empty()) } /// Yields a [`MachineBootInterface`] for every host ethernet interface that @@ -944,7 +945,7 @@ impl EndpointExplorationReport { self.systems .first() .and_then(|system| system.serial_number.as_deref().map(str::trim)) - .filter(|sn| !sn.is_empty()) + .none_if_empty() .or_else(|| { self.is_dpu().then(|| { // BF4 reports no system serial in Redfish. The stable product serial is @@ -958,7 +959,7 @@ impl EndpointExplorationReport { .serial_number .as_deref() .map(str::trim) - .filter(|serial| !serial.is_empty()) + .none_if_empty() }) })? }) @@ -1798,15 +1799,11 @@ fn chassis_part_number(chassis: &Chassis) -> Option<&str> { .part_number .as_deref() .map(str::trim) - .filter(|part_number| !part_number.is_empty()) + .none_if_empty() } fn chassis_model(chassis: &Chassis) -> Option<&str> { - chassis - .model - .as_deref() - .map(str::trim) - .filter(|model| !model.is_empty()) + chassis.model.as_deref().map(str::trim).none_if_empty() } // returns true if the passed in string is a BlueField part number @@ -1996,7 +1993,7 @@ impl EndpointExplorationReport { .first() .and_then(|system| system.serial_number.as_deref()) .map(str::trim) - .filter(|serial| !serial.is_empty()) + .none_if_empty() .or_else(|| { // BF4 Redfish does not currently expose the product serial or // DPU/NIC mode on the system object. The stable product serial @@ -2010,7 +2007,7 @@ impl EndpointExplorationReport { .serial_number .as_deref() .map(str::trim) - .filter(|serial| !serial.is_empty()) + .none_if_empty() }) }) } @@ -2054,7 +2051,7 @@ pub fn collect_explored_mlx_devices(endpoints: &[ExploredEndpoint]) -> Vec Result { - let url = artifact - .url - .as_deref() - .map(str::trim) - .filter(|url| !url.is_empty()); - - let filename = artifact - .filename - .as_deref() - .map(str::trim) - .filter(|filename| !filename.is_empty()); + let url = artifact.url.as_deref().map(str::trim).none_if_empty(); + + let filename = artifact.filename.as_deref().map(str::trim).none_if_empty(); let url = if let Some(url) = url { url.to_owned() diff --git a/crates/rack-controller/src/fabric_manager.rs b/crates/rack-controller/src/fabric_manager.rs index 5f23ac0594..3258a601e0 100644 --- a/crates/rack-controller/src/fabric_manager.rs +++ b/crates/rack-controller/src/fabric_manager.rs @@ -19,6 +19,7 @@ use std::collections::{HashMap, HashSet}; use carbide_rack::firmware_update::build_new_node_info; use carbide_rack_controller::config::RmsConfig; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::rack::RackId; use carbide_uuid::switch::SwitchId; use db::switch as db_switch; @@ -74,7 +75,7 @@ pub(super) async fn batch_get_scale_up_fabric_service_status( switches: &[FirmwareUpgradeDeviceInfo], node_type: rms::NodeType, ) -> Result { - let Some(url) = rms_config.api_url.as_deref().filter(|url| !url.is_empty()) else { + let Some(url) = rms_config.api_url.as_deref().none_if_empty() else { return Err("RMS client not configured".to_string()); }; diff --git a/crates/rack-controller/src/maintenance.rs b/crates/rack-controller/src/maintenance.rs index 7a6bf722d1..a66c624307 100644 --- a/crates/rack-controller/src/maintenance.rs +++ b/crates/rack-controller/src/maintenance.rs @@ -36,6 +36,7 @@ use carbide_rack_controller::fabric_manager::{ }; use carbide_rack_controller::validating::strip_rv_labels; use carbide_secrets::credentials::{CredentialManager, Credentials}; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::rack::{RackId, RackProfileId}; use db::{ host_machine_update as db_host_machine_update, machine as db_machine, @@ -629,10 +630,7 @@ fn build_switch_device_info_request( const NMX_CONFIGURE_RMS_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); fn build_nmx_configure_rms_client(rms_config: &RmsConfig) -> Option { - let url = rms_config - .api_url - .as_deref() - .filter(|url| !url.is_empty())?; + let url = rms_config.api_url.as_deref().none_if_empty()?; let mut rms_client_config = librms::client_config::RmsClientConfig::new( rms_config.root_ca_path.clone(), rms_config.client_cert.clone(), diff --git a/crates/rack-controller/src/nmx_certificate.rs b/crates/rack-controller/src/nmx_certificate.rs index b2486a370d..6641db2065 100644 --- a/crates/rack-controller/src/nmx_certificate.rs +++ b/crates/rack-controller/src/nmx_certificate.rs @@ -22,6 +22,7 @@ use std::str::FromStr; use std::sync::Arc; use carbide_secrets::credentials::Credentials; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::switch::SwitchId; use component_manager::component_manager::ComponentManager; use component_manager::nv_switch_manager::SwitchEndpoint; @@ -63,10 +64,7 @@ pub fn switch_endpoint_from_firmware_device( nvos_mac, bmc_credentials, nvos_credentials, - nvos_host_name: device - .os_hostname - .clone() - .filter(|hostname| !hostname.is_empty()), + nvos_host_name: device.os_hostname.clone().none_if_empty(), }) } diff --git a/crates/rpc-utils/Cargo.toml b/crates/rpc-utils/Cargo.toml index 1d50ee72b8..611e6c130e 100644 --- a/crates/rpc-utils/Cargo.toml +++ b/crates/rpc-utils/Cargo.toml @@ -24,6 +24,7 @@ authors.workspace = true [dependencies] carbide-rpc = { path = "../rpc" } carbide-health-report = { path = "../health-report" } +carbide-utils = { path = "../utils" } carbide-uuid = { path = "../uuid" } config-version = { path = "../config-version" } diff --git a/crates/rpc-utils/src/managed_host_display.rs b/crates/rpc-utils/src/managed_host_display.rs index 4a1b671e18..58552f3cc2 100644 --- a/crates/rpc-utils/src/managed_host_display.rs +++ b/crates/rpc-utils/src/managed_host_display.rs @@ -21,6 +21,7 @@ use std::sync::Arc; use std::time::SystemTime; use byte_unit::UnitType; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::machine::MachineId; use chrono::{DateTime, Utc}; use itertools::Itertools; @@ -599,10 +600,10 @@ impl From> for BmcInfoDisplay { fn from(value: Option) -> Self { if let Some(bmc_info) = value { Self { - ip: bmc_info.ip.if_non_empty(), - mac: bmc_info.mac.if_non_empty(), - version: bmc_info.version.if_non_empty(), - firmware_version: bmc_info.firmware_version.if_non_empty(), + ip: bmc_info.ip.none_if_empty(), + mac: bmc_info.mac.none_if_empty(), + version: bmc_info.version.none_if_empty(), + firmware_version: bmc_info.firmware_version.none_if_empty(), } } else { Self::default() @@ -622,9 +623,9 @@ impl From> for DmiDataDisplay { fn from(value: Option<&DmiData>) -> Self { if let Some(dmi_data) = value { Self { - product_serial: dmi_data.product_serial.clone().if_non_empty(), - chassis_serial: dmi_data.chassis_serial.clone().if_non_empty(), - bios_version: dmi_data.bios_version.clone().if_non_empty(), + product_serial: dmi_data.product_serial.clone().none_if_empty(), + chassis_serial: dmi_data.chassis_serial.clone().none_if_empty(), + bios_version: dmi_data.bios_version.clone().none_if_empty(), } } else { Self { @@ -636,32 +637,6 @@ impl From> for DmiDataDisplay { } } -/// Simple if_non_empty() function to map an empty String (or a Option::None) into None -trait IfNonEmpty { - type SomeVal; - fn if_non_empty(self) -> Option; -} - -impl IfNonEmpty for Option { - type SomeVal = String; - fn if_non_empty(self) -> Option { - if let Some(s) = self - && !s.is_empty() - { - Some(s) - } else { - None - } - } -} - -impl IfNonEmpty for String { - type SomeVal = String; - fn if_non_empty(self) -> Option { - if !self.is_empty() { Some(self) } else { None } - } -} - #[cfg(test)] mod tests { use std::time::{Duration, UNIX_EPOCH}; @@ -711,24 +686,4 @@ mod tests { } ); } - - #[test] - fn filters_empty_display_fields() { - value_scenarios!( - run = |value| value.if_non_empty(); - "option string" { - None:: => None, - Some(String::new()) => None, - Some("value".to_string()) => Some("value".to_string()), - } - ); - - value_scenarios!( - run = |value: String| value.if_non_empty(); - "string" { - String::new() => None, - "value".to_string() => Some("value".to_string()), - } - ); - } } diff --git a/crates/rpc/src/libmlx.rs b/crates/rpc/src/libmlx.rs index 74a21aa66d..b2e1642b33 100644 --- a/crates/rpc/src/libmlx.rs +++ b/crates/rpc/src/libmlx.rs @@ -19,6 +19,7 @@ use std::str::FromStr; use carbide_libmlx_model::device::info::MlxDeviceInfo; use carbide_libmlx_model::firmware::result::FirmwareFlashReport; +use carbide_utils::none_if_empty::NoneIfEmpty; use mac_address::MacAddress; use crate::protos::mlx_device::{ @@ -63,26 +64,18 @@ impl TryFrom for MlxDeviceInfo { ) }; - // Similar to parse_optional_xml_field, have a little helper - // for handling it with Rust <-> proto type conversion as well. - let parse_optional_field = |s: String| if s.is_empty() { None } else { Some(s) }; - Ok(MlxDeviceInfo { pci_name: proto.pci_name, device_type: proto.device_type, - psid: parse_optional_field(proto.psid), - device_description: parse_optional_field(proto.device_description), - part_number: parse_optional_field(proto.part_number), - fw_version_current: parse_optional_field(proto.fw_version_current), - pxe_version_current: parse_optional_field(proto.pxe_version_current), - uefi_version_current: parse_optional_field(proto.uefi_version_current), - uefi_version_virtio_blk_current: parse_optional_field( - proto.uefi_version_virtio_blk_current, - ), - uefi_version_virtio_net_current: parse_optional_field( - proto.uefi_version_virtio_net_current, - ), - status: parse_optional_field(proto.status), + psid: proto.psid.none_if_empty(), + device_description: proto.device_description.none_if_empty(), + part_number: proto.part_number.none_if_empty(), + fw_version_current: proto.fw_version_current.none_if_empty(), + pxe_version_current: proto.pxe_version_current.none_if_empty(), + uefi_version_current: proto.uefi_version_current.none_if_empty(), + uefi_version_virtio_blk_current: proto.uefi_version_virtio_blk_current.none_if_empty(), + uefi_version_virtio_net_current: proto.uefi_version_virtio_net_current.none_if_empty(), + status: proto.status.none_if_empty(), base_mac, }) } diff --git a/crates/rpc/src/model/compute_allocation.rs b/crates/rpc/src/model/compute_allocation.rs index 2c8ef7a591..c161cabfdc 100644 --- a/crates/rpc/src/model/compute_allocation.rs +++ b/crates/rpc/src/model/compute_allocation.rs @@ -15,6 +15,7 @@ * limitations under the License. */ +use carbide_utils::none_if_empty::NoneIfEmpty; use model::compute_allocation::ComputeAllocation; use crate::errors::RpcDataConversionError; @@ -46,11 +47,7 @@ impl TryFrom for rpc::ComputeAllocation { .iter() .map(|(key, value)| rpc::Label { key: key.to_owned(), - value: if value.is_empty() { - None - } else { - Some(value.to_owned()) - }, + value: value.to_owned().none_if_empty(), }) .collect(), }), diff --git a/crates/rpc/src/model/expected_switch.rs b/crates/rpc/src/model/expected_switch.rs index f8e11b0c34..286e89bae1 100644 --- a/crates/rpc/src/model/expected_switch.rs +++ b/crates/rpc/src/model/expected_switch.rs @@ -15,6 +15,7 @@ * limitations under the License. */ +use carbide_utils::none_if_empty::NoneIfEmpty; use mac_address::MacAddress; use model::expected_switch::{ExpectedSwitch, ExpectedSwitchRequest, LinkedExpectedSwitch}; use model::metadata::Metadata; @@ -84,7 +85,7 @@ impl TryFrom for ExpectedSwitch { let nvos_ip_address = rpc .nvos_ip_address .as_deref() - .filter(|s| !s.is_empty()) + .none_if_empty() .map(|s| { s.parse() .map_err(|_| RpcDataConversionError::InvalidArgument(s.to_string())) diff --git a/crates/rpc/src/model/instance_type.rs b/crates/rpc/src/model/instance_type.rs index 2d28972eec..8af16e921e 100644 --- a/crates/rpc/src/model/instance_type.rs +++ b/crates/rpc/src/model/instance_type.rs @@ -15,6 +15,7 @@ * limitations under the License. */ +use carbide_utils::none_if_empty::NoneIfEmpty; use model::instance_type::{InstanceType, InstanceTypeMachineCapabilityFilter}; use crate::errors::RpcDataConversionError; @@ -111,11 +112,7 @@ impl TryFrom for rpc::InstanceType { .iter() .map(|(key, value)| rpc::Label { key: key.to_owned(), - value: if value.is_empty() { - None - } else { - Some(value.to_owned()) - }, + value: value.to_owned().none_if_empty(), }) .collect(), }), diff --git a/crates/rpc/src/model/machine_validation.rs b/crates/rpc/src/model/machine_validation.rs index 9f9118364a..93c9360583 100644 --- a/crates/rpc/src/model/machine_validation.rs +++ b/crates/rpc/src/model/machine_validation.rs @@ -16,6 +16,7 @@ */ use std::str::FromStr; +use carbide_utils::none_if_empty::NoneIfEmpty; use chrono::{DateTime, Utc}; use config_version::ConfigVersion; use model::machine_validation::{ @@ -302,11 +303,7 @@ impl TryFrom for MachineValidationTest { modified_by: value.modified_by, verified: value.verified, read_only: value.read_only, - custom_tags: if value.custom_tags.is_empty() { - None - } else { - Some(value.custom_tags) - }, + custom_tags: value.custom_tags.none_if_empty(), components: value.components, last_modified_at: Utc::now(), is_enabled: value.is_enabled, diff --git a/crates/rpc/src/model/metadata.rs b/crates/rpc/src/model/metadata.rs index 914ea9ccc8..64b2da7aaa 100644 --- a/crates/rpc/src/model/metadata.rs +++ b/crates/rpc/src/model/metadata.rs @@ -15,6 +15,7 @@ * limitations under the License. */ +use carbide_utils::none_if_empty::NoneIfEmpty; use model::metadata::{LabelFilter, Metadata}; use crate as rpc; @@ -30,11 +31,7 @@ impl From for rpc::Metadata { .iter() .map(|(key, value)| rpc::forge::Label { key: key.clone(), - value: if value.is_empty() { - None - } else { - Some(value.clone()) - }, + value: value.clone().none_if_empty(), }) .collect(), } diff --git a/crates/rpc/src/model/network_security_group.rs b/crates/rpc/src/model/network_security_group.rs index 8891a2e743..07f1a43c7c 100644 --- a/crates/rpc/src/model/network_security_group.rs +++ b/crates/rpc/src/model/network_security_group.rs @@ -15,6 +15,7 @@ * limitations under the License. */ +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::network_security_group::NetworkSecurityGroupId; use config_version::ConfigVersion; use model::network_security_group::{ @@ -518,11 +519,7 @@ impl TryFrom for rpc::NetworkSecurityGroup { .iter() .map(|(key, value)| rpc::Label { key: key.to_owned(), - value: if value.is_empty() { - None - } else { - Some(value.to_owned()) - }, + value: value.to_owned().none_if_empty(), }) .collect(), }), diff --git a/crates/rpc/src/model/vpc.rs b/crates/rpc/src/model/vpc.rs index cfb7909676..4008a0f7b2 100644 --- a/crates/rpc/src/model/vpc.rs +++ b/crates/rpc/src/model/vpc.rs @@ -16,6 +16,7 @@ */ use carbide_network::virtualization::DEFAULT_NETWORK_VIRTUALIZATION_TYPE; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::network_security_group::NetworkSecurityGroupIdParseError; use config_version::ConfigVersion; use model::metadata::{LabelFilter, Metadata}; @@ -56,11 +57,7 @@ impl From for rpc::forge::Vpc { .iter() .map(|(key, value)| rpc::forge::Label { key: key.clone(), - value: if value.clone().is_empty() { - None - } else { - Some(value.clone()) - }, + value: value.clone().none_if_empty(), }) .collect(), }); diff --git a/crates/scout/src/firmware_upgrade.rs b/crates/scout/src/firmware_upgrade.rs index 828fcc6d88..d1ab3e7de4 100644 --- a/crates/scout/src/firmware_upgrade.rs +++ b/crates/scout/src/firmware_upgrade.rs @@ -18,6 +18,7 @@ use std::path::{Path, PathBuf}; use std::time::Duration; +use carbide_utils::none_if_empty::NoneIfEmpty; use futures_util::TryStreamExt; use rpc::scout_firmware_upgrade::ScoutFirmwareUpgradeTask as FirmwareUpgradeTask; use sha2::{Digest, Sha256}; @@ -223,7 +224,7 @@ async fn download_file( let segment = parsed .path_segments() .and_then(|mut s| s.next_back()) - .filter(|s| !s.is_empty()) + .none_if_empty() .ok_or_else(|| format!("cannot extract filename from URL: {url}"))?; let filename = Path::new(segment) diff --git a/crates/site-explorer/src/lib.rs b/crates/site-explorer/src/lib.rs index cced6bad5e..d16886f741 100644 --- a/crates/site-explorer/src/lib.rs +++ b/crates/site-explorer/src/lib.rs @@ -29,6 +29,7 @@ use carbide_firmware::{FirmwareConfig, FirmwareConfigSnapshot}; use carbide_network::{is_locally_administered_mac, sanitized_mac}; use carbide_redfish::libredfish::conv::IntoModel; use carbide_secrets::credentials::CredentialManager; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_utils::periodic_timer::PeriodicTimer; use carbide_uuid::machine::MachineType; use carbide_uuid::power_shelf::{PowerShelfIdSource, PowerShelfType}; @@ -3733,7 +3734,7 @@ fn duplicate_bluefield_serial<'a>( return None; } - let serial_number = serial_number.map(str::trim).filter(|s| !s.is_empty())?; + let serial_number = serial_number.map(str::trim).none_if_empty()?; (!seen.insert(serial_number)).then_some(serial_number) } diff --git a/crates/site-explorer/src/machine_creator.rs b/crates/site-explorer/src/machine_creator.rs index 9605c74206..f264ef54b2 100644 --- a/crates/site-explorer/src/machine_creator.rs +++ b/crates/site-explorer/src/machine_creator.rs @@ -21,6 +21,7 @@ use carbide_rack::rms_node_type::compute_node_type_for_profile; use carbide_secrets::credentials::{ BmcCredentialType, CredentialKey, CredentialManager, Credentials, }; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::machine::MachineId; use db::{ObjectColumnFilter, Transaction}; use itertools::Itertools; @@ -1062,7 +1063,7 @@ fn host_mac_addresses_for_predicted_machine( [] => machine_data .filter(|_| !(report.is_dpu() || report.is_switch() || report.is_power_shelf())) .map(|data| data.host_nics.as_slice()) - .filter(|host_nics| !host_nics.is_empty()) + .none_if_empty() .map(|host_nics| { tracing::info!( host_nic_count = host_nics.len(), diff --git a/crates/switch-controller/src/endpoint.rs b/crates/switch-controller/src/endpoint.rs index 9a15f9cf13..d45da3d178 100644 --- a/crates/switch-controller/src/endpoint.rs +++ b/crates/switch-controller/src/endpoint.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use carbide_secrets::credentials::{CredentialKey, CredentialManager, Credentials}; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::switch::SwitchId; use component_manager::nv_switch_manager::SwitchEndpoint; use db::switch::SwitchEndpointRow; @@ -66,10 +67,7 @@ pub fn switch_endpoint_from_row( nvos_mac, bmc_credentials: nvos_credentials.clone(), nvos_credentials, - nvos_host_name: row - .nvos_hostname - .clone() - .filter(|hostname| !hostname.is_empty()), + nvos_host_name: row.nvos_hostname.clone().none_if_empty(), }) } diff --git a/crates/switch-controller/src/maintenance.rs b/crates/switch-controller/src/maintenance.rs index 4bed87170f..038fedf13f 100644 --- a/crates/switch-controller/src/maintenance.rs +++ b/crates/switch-controller/src/maintenance.rs @@ -20,6 +20,7 @@ use carbide_secrets::credentials::{ BmcCredentialType, CredentialKey, CredentialManager, Credentials, }; +use carbide_utils::none_if_empty::NoneIfEmpty; use carbide_uuid::switch::SwitchId; use component_manager::nv_switch_manager::{SwitchComponentResult, SwitchEndpoint}; use db::switch as db_switch; @@ -374,9 +375,7 @@ pub(super) async fn build_switch_endpoint( nvos_mac, bmc_credentials, nvos_credentials, - nvos_host_name: endpoint - .nvos_hostname - .filter(|hostname| !hostname.is_empty()), + nvos_host_name: endpoint.nvos_hostname.none_if_empty(), }) } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 0ad1a5d1c6..03d7f84d66 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -25,6 +25,7 @@ pub mod config; mod host_port_pair; pub mod managed_loop; pub mod metrics; +pub mod none_if_empty; pub mod periodic_timer; pub mod redfish; pub mod sku; diff --git a/crates/utils/src/none_if_empty.rs b/crates/utils/src/none_if_empty.rs new file mode 100644 index 0000000000..1291c8a686 --- /dev/null +++ b/crates/utils/src/none_if_empty.rs @@ -0,0 +1,211 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +use std::collections::HashMap; + +/// Converts an empty value, or an optional empty value, to [`None`]. +pub trait NoneIfEmpty { + type Val; + + fn none_if_empty(self) -> Option; +} + +impl NoneIfEmpty for Option { + type Val = String; + + fn none_if_empty(self) -> Option { + self.filter(|value| !value.is_empty()) + } +} + +impl NoneIfEmpty for String { + type Val = String; + + fn none_if_empty(self) -> Option { + if self.is_empty() { None } else { Some(self) } + } +} + +impl NoneIfEmpty for Option> { + type Val = Vec; + fn none_if_empty(self) -> Option { + match self { + None => None, + Some(v) if v.is_empty() => None, + Some(v) => Some(v), + } + } +} + +impl NoneIfEmpty for Vec { + type Val = Vec; + fn none_if_empty(self) -> Option { + if self.is_empty() { None } else { Some(self) } + } +} + +impl NoneIfEmpty for Option> { + type Val = HashMap; + fn none_if_empty(self) -> Option { + match self { + None => None, + Some(h) if h.is_empty() => None, + Some(h) => Some(h), + } + } +} + +impl NoneIfEmpty for HashMap { + type Val = HashMap; + fn none_if_empty(self) -> Option { + if self.is_empty() { None } else { Some(self) } + } +} + +impl<'a> NoneIfEmpty for &'a str { + type Val = &'a str; + fn none_if_empty(self) -> Option { + if self.is_empty() { None } else { Some(self) } + } +} + +impl<'a> NoneIfEmpty for Option<&'a str> { + type Val = &'a str; + fn none_if_empty(self) -> Option { + self.filter(|value| !value.is_empty()) + } +} + +impl<'a, T> NoneIfEmpty for &'a [T] { + type Val = &'a [T]; + fn none_if_empty(self) -> Option { + if self.is_empty() { None } else { Some(self) } + } +} + +impl<'a, T> NoneIfEmpty for Option<&'a [T]> { + type Val = &'a [T]; + fn none_if_empty(self) -> Option { + self.filter(|value| !value.is_empty()) + } +} + +#[cfg(test)] +mod tests { + use carbide_test_support::value_scenarios; + + use super::*; + + #[test] + fn filters_empty_strings() { + value_scenarios!( + run = |value| value.none_if_empty(); + "option string" { + None:: => None, + Some(String::new()) => None, + Some("value".to_string()) => Some("value".to_string()), + } + ); + + value_scenarios!( + run = |value: String| value.none_if_empty(); + "string" { + String::new() => None, + "value".to_string() => Some("value".to_string()), + } + ); + } + + #[test] + fn filters_empty_borrowed_strings() { + value_scenarios!( + run = |value| value.none_if_empty(); + "optional borrowed string" { + None::<&str> => None, + Some("") => None, + Some("value") => Some("value"), + } + ); + + value_scenarios!( + run = |value: &str| value.none_if_empty(); + "borrowed string" { + "" => None, + "value" => Some("value"), + } + ); + } + + #[test] + fn filters_empty_vectors() { + value_scenarios!( + run = |value| value.none_if_empty(); + "option vector" { + None::> => None, + Some(Vec::new()) => None, + Some(vec![1, 2]) => Some(vec![1, 2]), + } + ); + + value_scenarios!( + run = |value: Vec| value.none_if_empty(); + "vector" { + Vec::new() => None, + vec![1, 2] => Some(vec![1, 2]), + } + ); + } + + #[test] + fn filters_empty_borrowed_slices() { + value_scenarios!( + run = |value| value.none_if_empty(); + "optional borrowed slice" { + None::<&[u8]> => None, + Some(&[] as &[u8]) => None, + Some(&[1_u8, 2] as &[u8]) => Some(&[1_u8, 2] as &[u8]), + } + ); + + value_scenarios!( + run = |value: &[u8]| value.none_if_empty(); + "borrowed slice" { + &[] as &[u8] => None, + &[1_u8, 2] as &[u8] => Some(&[1_u8, 2] as &[u8]), + } + ); + } + + #[test] + fn filters_empty_hash_maps() { + value_scenarios!( + run = |value| value.none_if_empty(); + "option hash map" { + None::> => None, + Some(HashMap::new()) => None, + Some(HashMap::from([("key", 1)])) => Some(HashMap::from([("key", 1)])), + } + ); + + value_scenarios!( + run = |value: HashMap<&str, u8>| value.none_if_empty(); + "hash map" { + HashMap::new() => None, + HashMap::from([("key", 1)]) => Some(HashMap::from([("key", 1)])), + } + ); + } +}