#![forbid(unsafe_code)]
#![deny(clippy::all)]
pub mod cli;
pub mod commands;
pub mod config;
pub mod docker;
pub mod environment;
pub mod error;
pub mod template;
pub use error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvFeaturizedName {
pub name: String,
pub features: Vec<String>,
}
impl EnvFeaturizedName {
pub fn parse(input: &str) -> Result<Self> {
let parts: Vec<&str> = input.split('+').collect();
if parts.is_empty() || parts[0].is_empty() {
return Err(Error::InvalidEnvironment(
"Environment name cannot be empty".into(),
));
}
let name = parts[0].to_string();
let features: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
for f in &features {
if f.is_empty() {
return Err(Error::InvalidFeature("Feature name cannot be empty".into()));
}
if !is_valid_name(f) {
return Err(Error::InvalidFeature(format!(
"'{}' contains invalid characters",
f
)));
}
}
Ok(Self { name, features })
}
}
pub fn is_valid_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
pub fn sanitize_name(name: &str) -> String {
name.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
pub fn default_container_name() -> String {
std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.map(|n| sanitize_name(&n))
.filter(|n| !n.is_empty())
.unwrap_or_else(|| "0".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_name_simple() {
assert!(is_valid_name("test"));
assert!(is_valid_name("my-project"));
assert!(is_valid_name("my_project"));
assert!(is_valid_name("Test123"));
}
#[test]
fn test_is_valid_name_rejects_invalid() {
assert!(!is_valid_name(""));
assert!(!is_valid_name("my/project"));
assert!(!is_valid_name("my:project"));
assert!(!is_valid_name("hello world"));
assert!(!is_valid_name("test@123"));
assert!(!is_valid_name("café"));
}
#[test]
fn test_sanitize_name_simple() {
assert_eq!(sanitize_name("test"), "test");
}
#[test]
fn test_sanitize_name_removes_special_chars() {
assert_eq!(sanitize_name("Spa&9 p"), "Spa9p");
}
#[test]
fn test_sanitize_name_keeps_valid_chars() {
assert_eq!(sanitize_name("my-project_123"), "my-project_123");
}
#[test]
fn test_sanitize_name_empty_result() {
assert_eq!(sanitize_name("@#$%"), "");
}
#[test]
fn test_sanitize_name_mixed() {
assert_eq!(sanitize_name("hello world!"), "helloworld");
}
#[test]
fn test_sanitize_name_unicode() {
assert_eq!(sanitize_name("café-123"), "caf-123");
}
#[test]
fn test_parse_env_spec_no_features() {
let spec = EnvFeaturizedName::parse("rust").unwrap();
assert_eq!(spec.name, "rust");
assert!(spec.features.is_empty());
}
#[test]
fn test_parse_env_spec_single_feature() {
let spec = EnvFeaturizedName::parse("rust+gpu").unwrap();
assert_eq!(spec.name, "rust");
assert_eq!(spec.features, vec!["gpu"]);
}
#[test]
fn test_parse_env_spec_multiple_features() {
let spec = EnvFeaturizedName::parse("rust+gpu+network+dev").unwrap();
assert_eq!(spec.name, "rust");
assert_eq!(spec.features, vec!["gpu", "network", "dev"]);
}
#[test]
fn test_parse_env_spec_with_namespace() {
let spec = EnvFeaturizedName::parse("my_collection/rust+gpu").unwrap();
assert_eq!(spec.name, "my_collection/rust");
assert_eq!(spec.features, vec!["gpu"]);
}
#[test]
fn test_parse_env_spec_empty_name_error() {
let result = EnvFeaturizedName::parse("+gpu");
assert!(result.is_err());
}
#[test]
fn test_parse_env_spec_empty_feature_error() {
let result = EnvFeaturizedName::parse("rust++gpu");
assert!(result.is_err());
}
#[test]
fn test_parse_env_spec_invalid_feature_chars() {
let result = EnvFeaturizedName::parse("rust+bad/feature");
assert!(result.is_err());
}
}