#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RoleAssignment {
Single(String),
Multiple(Vec<String>),
}
impl RoleAssignment {
pub fn as_vec(&self) -> Vec<&str> {
match self {
RoleAssignment::Single(s) => vec![s.as_str()],
RoleAssignment::Multiple(v) => v.iter().map(String::as_str).collect(),
}
}
pub fn is_empty(&self) -> bool {
match self {
RoleAssignment::Single(s) => s.is_empty(),
RoleAssignment::Multiple(v) => v.is_empty(),
}
}
pub fn len(&self) -> usize {
match self {
RoleAssignment::Single(s) => {
if s.is_empty() {
0
} else {
1
}
}
RoleAssignment::Multiple(v) => v.len(),
}
}
}
impl Default for RoleAssignment {
fn default() -> Self {
RoleAssignment::Multiple(Vec::new())
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RoleExtends {
Single(String),
Multiple(Vec<String>),
}
impl RoleExtends {
pub fn as_vec(&self) -> Vec<&str> {
match self {
RoleExtends::Single(s) => vec![s.as_str()],
RoleExtends::Multiple(v) => v.iter().map(String::as_str).collect(),
}
}
pub fn is_empty(&self) -> bool {
match self {
RoleExtends::Single(s) => s.is_empty(),
RoleExtends::Multiple(v) => v.is_empty(),
}
}
}
impl Default for RoleExtends {
fn default() -> Self {
RoleExtends::Multiple(Vec::new())
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RoleToolSpec {
Detailed {
version: String,
#[serde(default)]
version_manager: Option<bool>,
#[serde(default)]
use_sudo: Option<bool>,
},
Simple(String),
}
impl RoleToolSpec {
pub fn version(&self) -> &str {
match self {
RoleToolSpec::Detailed { version, .. } => version,
RoleToolSpec::Simple(v) => v,
}
}
pub fn version_manager(&self) -> bool {
match self {
RoleToolSpec::Detailed {
version_manager, ..
} => version_manager.unwrap_or(true),
RoleToolSpec::Simple(_) => true,
}
}
pub fn use_sudo(&self) -> Option<bool> {
match self {
RoleToolSpec::Detailed { use_sudo, .. } => *use_sudo,
RoleToolSpec::Simple(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
pub struct RoleDefinition {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub extends: Option<RoleExtends>,
#[serde(default)]
pub tools: Vec<String>,
#[serde(flatten)]
pub tool_versions: HashMap<String, RoleToolSpec>,
}
impl RoleDefinition {
pub fn has_extends(&self) -> bool {
self.extends
.as_ref()
.map(|e| !e.is_empty())
.unwrap_or(false)
}
pub fn get_extends(&self) -> Vec<&str> {
self.extends
.as_ref()
.map(|e| e.as_vec())
.unwrap_or_default()
}
pub fn get_tools(&self) -> HashMap<String, String> {
let mut result = HashMap::new();
for tool in &self.tools {
result.insert(tool.clone(), "latest".to_string());
}
for (name, spec) in &self.tool_versions {
if name != "tools" {
result.insert(name.clone(), spec.version().to_string());
}
}
result
}
pub fn tool_count(&self) -> usize {
let mut count = self.tools.len();
for name in self.tool_versions.keys() {
if name != "tools" && !self.tools.contains(name) {
count += 1;
}
}
count
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct RolesConfig {
#[serde(flatten)]
pub roles: HashMap<String, RoleDefinitionWrapper>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RoleDefinitionWrapper {
WithTools {
#[serde(default)]
description: Option<String>,
#[serde(default)]
extends: Option<RoleExtends>,
#[serde(default)]
tools: RoleToolsSection,
},
Simple(RoleDefinition),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RoleToolsSection {
Names(Vec<String>),
Versions(HashMap<String, RoleToolSpec>),
}
impl Default for RoleToolsSection {
fn default() -> Self {
RoleToolsSection::Names(Vec::new())
}
}
impl RoleDefinitionWrapper {
pub fn into_definition(self) -> RoleDefinition {
match self {
RoleDefinitionWrapper::WithTools {
description,
extends,
tools,
} => {
let (tool_list, tool_versions) = match tools {
RoleToolsSection::Names(names) => (names, HashMap::new()),
RoleToolsSection::Versions(versions) => (Vec::new(), versions),
};
RoleDefinition {
description,
extends,
tools: tool_list,
tool_versions,
}
}
RoleDefinitionWrapper::Simple(def) => def,
}
}
pub fn description(&self) -> Option<&str> {
match self {
RoleDefinitionWrapper::WithTools { description, .. } => description.as_deref(),
RoleDefinitionWrapper::Simple(def) => def.description.as_deref(),
}
}
pub fn has_extends(&self) -> bool {
match self {
RoleDefinitionWrapper::WithTools { extends, .. } => {
extends.as_ref().map(|e| !e.is_empty()).unwrap_or(false)
}
RoleDefinitionWrapper::Simple(def) => def.has_extends(),
}
}
pub fn get_extends(&self) -> Vec<&str> {
match self {
RoleDefinitionWrapper::WithTools { extends, .. } => {
extends.as_ref().map(|e| e.as_vec()).unwrap_or_default()
}
RoleDefinitionWrapper::Simple(def) => def.get_extends(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_role_assignment_single() {
let assignment = RoleAssignment::Single("frontend".to_string());
assert_eq!(assignment.as_vec(), vec!["frontend"]);
assert_eq!(assignment.len(), 1);
assert!(!assignment.is_empty());
}
#[test]
fn test_role_assignment_multiple() {
let assignment =
RoleAssignment::Multiple(vec!["frontend".to_string(), "devops".to_string()]);
assert_eq!(assignment.as_vec(), vec!["frontend", "devops"]);
assert_eq!(assignment.len(), 2);
assert!(!assignment.is_empty());
}
#[test]
fn test_role_assignment_empty() {
let assignment = RoleAssignment::Multiple(Vec::new());
assert!(assignment.is_empty());
assert_eq!(assignment.len(), 0);
let single_empty = RoleAssignment::Single(String::new());
assert!(single_empty.is_empty());
assert_eq!(single_empty.len(), 0);
}
#[test]
fn test_role_extends_single() {
let extends = RoleExtends::Single("base".to_string());
assert_eq!(extends.as_vec(), vec!["base"]);
assert!(!extends.is_empty());
}
#[test]
fn test_role_extends_multiple() {
let extends = RoleExtends::Multiple(vec!["base".to_string(), "common".to_string()]);
assert_eq!(extends.as_vec(), vec!["base", "common"]);
assert!(!extends.is_empty());
}
#[test]
fn test_role_tool_spec_simple() {
let spec = RoleToolSpec::Simple("20".to_string());
assert_eq!(spec.version(), "20");
assert!(spec.version_manager());
assert!(spec.use_sudo().is_none());
}
#[test]
fn test_role_tool_spec_detailed() {
let spec = RoleToolSpec::Detailed {
version: "18".to_string(),
version_manager: Some(false),
use_sudo: Some(true),
};
assert_eq!(spec.version(), "18");
assert!(!spec.version_manager());
assert_eq!(spec.use_sudo(), Some(true));
}
#[test]
fn test_role_definition_get_tools() {
let mut def = RoleDefinition {
tools: vec!["node".to_string(), "bun".to_string()],
..Default::default()
};
def.tool_versions
.insert("node".to_string(), RoleToolSpec::Simple("20".to_string()));
def.tool_versions
.insert("pnpm".to_string(), RoleToolSpec::Simple("8".to_string()));
let tools = def.get_tools();
assert_eq!(tools.get("node"), Some(&"20".to_string())); assert_eq!(tools.get("bun"), Some(&"latest".to_string()));
assert_eq!(tools.get("pnpm"), Some(&"8".to_string()));
}
#[test]
fn test_role_definition_has_extends() {
let mut def = RoleDefinition::default();
assert!(!def.has_extends());
def.extends = Some(RoleExtends::Single("base".to_string()));
assert!(def.has_extends());
}
#[test]
fn test_role_assignment_deserialize_single() {
let toml_str = r#"role = "frontend""#;
#[derive(Deserialize)]
struct Test {
role: RoleAssignment,
}
let test: Test = toml::from_str(toml_str).unwrap();
assert!(matches!(test.role, RoleAssignment::Single(_)));
}
#[test]
fn test_role_assignment_deserialize_multiple() {
let toml_str = r#"role = ["frontend", "devops"]"#;
#[derive(Deserialize)]
struct Test {
role: RoleAssignment,
}
let test: Test = toml::from_str(toml_str).unwrap();
assert!(matches!(test.role, RoleAssignment::Multiple(_)));
}
}