use crate::commands::convert_engine_error;
use crate::commands::env_file::{discover_env_cue_directories, find_cue_module_root};
use cuengine::ModuleEvalOptions;
use cuenv_core::cue::discovery::{adjust_meta_key_path, compute_relative_path};
use cuenv_core::{ModuleEvaluation, Result};
use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Write;
use std::path::Path;
#[derive(Debug, Serialize)]
struct InfoOutput {
module_root: String,
base_count: usize,
project_count: usize,
projects: Vec<ProjectInfo>,
}
#[derive(Debug, Serialize)]
struct MetaOutput {
module_root: String,
instances: std::collections::HashMap<String, serde_json::Value>,
meta: std::collections::HashMap<String, cuengine::FieldMeta>,
}
#[derive(Debug, Serialize)]
struct ProjectInfo {
name: String,
path: String,
}
#[derive(Clone, Copy, Debug)]
pub struct InfoOptions<'a> {
pub path: Option<&'a str>,
pub package: &'a str,
pub json_output: bool,
pub with_meta: bool,
}
#[allow(clippy::too_many_lines)]
pub fn execute_info(options: InfoOptions<'_>) -> Result<String> {
let scan_all = options.path.is_none();
let effective_path = options.path.unwrap_or(".");
let start_path =
Path::new(effective_path)
.canonicalize()
.map_err(|e| cuenv_core::Error::Io {
source: e,
path: Some(Path::new(effective_path).to_path_buf().into_boxed_path()),
operation: "canonicalize path".to_string(),
})?;
let module_root = find_cue_module_root(&start_path).ok_or_else(|| {
cuenv_core::Error::configuration(format!(
"No CUE module found (looking for cue.mod/) starting from: {}",
start_path.display()
))
})?;
let raw_result = if scan_all {
let env_cue_dirs = discover_env_cue_directories(&module_root, options.package);
if env_cue_dirs.is_empty() {
return Err(cuenv_core::Error::configuration(format!(
"No env.cue files with package '{}' found in module: {}",
options.package,
module_root.display()
)));
}
let mut all_instances = HashMap::new();
let mut all_projects = Vec::new();
let mut all_meta = HashMap::new();
for dir in env_cue_dirs {
let dir_rel_path = compute_relative_path(&dir, &module_root);
let eval_options = ModuleEvalOptions {
recursive: false,
with_meta: options.with_meta,
target_dir: Some(dir.to_string_lossy().to_string()),
..Default::default()
};
let Ok(raw) =
cuengine::evaluate_module(&module_root, options.package, Some(&eval_options))
.map_err(convert_engine_error)
else {
continue;
};
for (path_str, value) in raw.instances {
let rel_path = if path_str == "." {
dir_rel_path.clone()
} else {
path_str
};
all_instances.insert(rel_path.clone(), value);
}
for project_path in raw.projects {
let rel_project_path = if project_path == "." {
dir_rel_path.clone()
} else {
project_path
};
if !all_projects.contains(&rel_project_path) {
all_projects.push(rel_project_path);
}
}
for (meta_key, meta_value) in raw.meta {
let adjusted_key = adjust_meta_key_path(&meta_key, &dir_rel_path);
all_meta.insert(adjusted_key, meta_value);
}
}
if all_instances.is_empty() {
return Err(cuenv_core::Error::configuration(
"No instances could be evaluated. All directories failed.",
));
}
cuengine::ModuleResult {
instances: all_instances,
projects: all_projects,
meta: all_meta,
}
} else {
let eval_options = ModuleEvalOptions {
with_meta: options.with_meta,
recursive: false,
target_dir: Some(start_path.to_string_lossy().to_string()),
..Default::default()
};
cuengine::evaluate_module(&module_root, options.package, Some(&eval_options))
.map_err(convert_engine_error)?
};
if options.with_meta {
let output = MetaOutput {
module_root: module_root.display().to_string(),
instances: raw_result.instances,
meta: raw_result.meta,
};
return serde_json::to_string_pretty(&output).map_err(|e| {
cuenv_core::Error::configuration(format!("Failed to serialize JSON: {e}"))
});
}
let module = ModuleEvaluation::from_raw(
module_root.clone(),
raw_result.instances,
raw_result.projects,
None,
);
let mut projects: Vec<ProjectInfo> = module
.projects()
.filter_map(|instance| {
instance.project_name().map(|name| ProjectInfo {
name: name.to_string(),
path: instance.path.display().to_string(),
})
})
.collect();
projects.sort_by(|a, b| a.name.cmp(&b.name));
if options.json_output {
let output = InfoOutput {
module_root: module_root.display().to_string(),
base_count: module.base_count(),
project_count: module.project_count(),
projects,
};
serde_json::to_string_pretty(&output)
.map_err(|e| cuenv_core::Error::configuration(format!("Failed to serialize JSON: {e}")))
} else {
let mut output = String::new();
let _ = writeln!(output, "Module: {}\n", module_root.display());
let _ = writeln!(output, "Bases: {}", module.base_count());
let _ = writeln!(output, "Projects: {}", module.project_count());
if !projects.is_empty() {
output.push_str("\nProjects:\n");
let max_name_len = projects
.iter()
.map(|p| p.name.len())
.max()
.unwrap_or(0)
.max(20);
for project in &projects {
let _ = writeln!(
output,
" {:<width$} {}",
project.name,
project.path,
width = max_name_len
);
}
}
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_project_info_serialization() {
let info = ProjectInfo {
name: "test-project".to_string(),
path: "projects/test".to_string(),
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("test-project"));
assert!(json.contains("projects/test"));
}
#[test]
fn test_info_output_serialization() {
let output = InfoOutput {
module_root: "/test/repo".to_string(),
base_count: 2,
project_count: 5,
projects: vec![ProjectInfo {
name: "api".to_string(),
path: "projects/api".to_string(),
}],
};
let json = serde_json::to_string_pretty(&output).unwrap();
assert!(json.contains("/test/repo"));
assert!(json.contains("\"base_count\": 2"));
assert!(json.contains("\"project_count\": 5"));
}
#[test]
fn test_project_info_debug() {
let info = ProjectInfo {
name: "test-project".to_string(),
path: "projects/test".to_string(),
};
let debug = format!("{info:?}");
assert!(debug.contains("ProjectInfo"));
assert!(debug.contains("test-project"));
}
#[test]
fn test_info_output_debug() {
let output = InfoOutput {
module_root: "/test/repo".to_string(),
base_count: 0,
project_count: 0,
projects: vec![],
};
let debug = format!("{output:?}");
assert!(debug.contains("InfoOutput"));
assert!(debug.contains("/test/repo"));
}
#[test]
fn test_meta_output_serialization() {
let mut instances = std::collections::HashMap::new();
instances.insert("./".to_string(), serde_json::json!({"name": "test"}));
let output = MetaOutput {
module_root: "/test/repo".to_string(),
instances,
meta: std::collections::HashMap::new(),
};
let json = serde_json::to_string_pretty(&output).unwrap();
assert!(json.contains("/test/repo"));
assert!(json.contains("instances"));
}
#[test]
fn test_meta_output_debug() {
let output = MetaOutput {
module_root: "/test".to_string(),
instances: std::collections::HashMap::new(),
meta: std::collections::HashMap::new(),
};
let debug = format!("{output:?}");
assert!(debug.contains("MetaOutput"));
}
#[test]
fn test_info_output_multiple_projects() {
let output = InfoOutput {
module_root: "/repo".to_string(),
base_count: 1,
project_count: 3,
projects: vec![
ProjectInfo {
name: "api".to_string(),
path: "services/api".to_string(),
},
ProjectInfo {
name: "web".to_string(),
path: "services/web".to_string(),
},
ProjectInfo {
name: "worker".to_string(),
path: "services/worker".to_string(),
},
],
};
let json = serde_json::to_string_pretty(&output).unwrap();
assert!(json.contains("api"));
assert!(json.contains("web"));
assert!(json.contains("worker"));
assert!(json.contains("\"project_count\": 3"));
}
#[test]
fn test_execute_info_invalid_path() {
let result = execute_info(InfoOptions {
path: Some("/nonexistent/path"),
package: "cuenv",
json_output: false,
with_meta: false,
});
assert!(result.is_err());
}
#[test]
fn test_execute_info_no_cue_module() {
let temp = std::env::temp_dir();
let result = execute_info(InfoOptions {
path: Some(temp.to_str().unwrap()),
package: "cuenv",
json_output: false,
with_meta: false,
});
assert!(result.is_err());
}
}