Skip to content

Commit 8f7ee63

Browse files
fix: add JSON output support to profile and version commands (#394)
Profile commands (list, show, path) and version command now properly support -o json and -o yaml flags for structured output. This enables automation and CI/CD integration for: - profile list: Returns config_path, profiles array with full details - profile show: Returns profile details with deployment-specific fields - profile path: Returns config_path location - version: Returns name and version All commands maintain backward compatibility with human-readable table output as default. Addresses #340 (JSON output audit)
1 parent 4cec3b4 commit 8f7ee63

File tree

1 file changed

+222
-80
lines changed

1 file changed

+222
-80
lines changed

crates/redisctl/src/main.rs

Lines changed: 222 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,25 @@ async fn execute_command(cli: &Cli, conn_mgr: &ConnectionManager) -> Result<(),
7575
let result = match &cli.command {
7676
Commands::Version => {
7777
debug!("Showing version information");
78-
println!("redisctl {}", env!("CARGO_PKG_VERSION"));
78+
match cli.output {
79+
cli::OutputFormat::Json | cli::OutputFormat::Yaml => {
80+
let output_data = serde_json::json!({
81+
"version": env!("CARGO_PKG_VERSION"),
82+
"name": env!("CARGO_PKG_NAME"),
83+
});
84+
85+
let fmt = match cli.output {
86+
cli::OutputFormat::Json => output::OutputFormat::Json,
87+
cli::OutputFormat::Yaml => output::OutputFormat::Yaml,
88+
_ => output::OutputFormat::Json,
89+
};
90+
91+
crate::output::print_output(&output_data, fmt, None)?;
92+
}
93+
_ => {
94+
println!("redisctl {}", env!("CARGO_PKG_VERSION"));
95+
}
96+
}
7997
Ok(())
8098
}
8199
Commands::Completions { shell } => {
@@ -86,7 +104,7 @@ async fn execute_command(cli: &Cli, conn_mgr: &ConnectionManager) -> Result<(),
86104

87105
Commands::Profile(profile_cmd) => {
88106
debug!("Executing profile command");
89-
execute_profile_command(profile_cmd, conn_mgr).await
107+
execute_profile_command(profile_cmd, conn_mgr, cli.output).await
90108
}
91109

92110
Commands::FilesKey(files_key_cmd) => {
@@ -736,6 +754,7 @@ async fn handle_enterprise_workflow_command(
736754
async fn execute_profile_command(
737755
profile_cmd: &cli::ProfileCommands,
738756
conn_mgr: &ConnectionManager,
757+
output_format: cli::OutputFormat,
739758
) -> Result<(), RedisCtlError> {
740759
use cli::ProfileCommands::*;
741760

@@ -745,109 +764,232 @@ async fn execute_profile_command(
745764
let profiles = conn_mgr.config.list_profiles();
746765
trace!("Found {} profiles", profiles.len());
747766

748-
// Show config file path at the top
749-
if let Ok(config_path) = config::Config::config_path() {
750-
println!("Configuration file: {}", config_path.display());
751-
println!();
752-
}
767+
match output_format {
768+
cli::OutputFormat::Json | cli::OutputFormat::Yaml => {
769+
let config_path = config::Config::config_path()
770+
.ok()
771+
.and_then(|p| p.to_str().map(String::from));
772+
773+
let profile_list: Vec<serde_json::Value> = profiles
774+
.iter()
775+
.map(|(name, profile)| {
776+
let is_default_enterprise =
777+
conn_mgr.config.default_enterprise.as_deref() == Some(name);
778+
let is_default_cloud =
779+
conn_mgr.config.default_cloud.as_deref() == Some(name);
780+
781+
let mut obj = serde_json::json!({
782+
"name": name,
783+
"deployment_type": profile.deployment_type.to_string(),
784+
"is_default_enterprise": is_default_enterprise,
785+
"is_default_cloud": is_default_cloud,
786+
});
787+
788+
match profile.deployment_type {
789+
config::DeploymentType::Cloud => {
790+
if let Some((_, _, url)) = profile.cloud_credentials() {
791+
obj["api_url"] = serde_json::json!(url);
792+
}
793+
}
794+
config::DeploymentType::Enterprise => {
795+
if let Some((url, username, _, insecure)) =
796+
profile.enterprise_credentials()
797+
{
798+
obj["url"] = serde_json::json!(url);
799+
obj["username"] = serde_json::json!(username);
800+
obj["insecure"] = serde_json::json!(insecure);
801+
}
802+
}
803+
}
753804

754-
if profiles.is_empty() {
755-
info!("No profiles configured");
756-
println!("No profiles configured.");
757-
println!("Use 'redisctl profile set' to create a profile.");
758-
return Ok(());
759-
}
805+
obj
806+
})
807+
.collect();
760808

761-
println!("{:<15} {:<12} DETAILS", "NAME", "TYPE");
762-
println!("{:-<15} {:-<12} {:-<30}", "", "", "");
809+
let output_data = serde_json::json!({
810+
"config_path": config_path,
811+
"profiles": profile_list,
812+
"count": profiles.len()
813+
});
763814

764-
for (name, profile) in profiles {
765-
let mut details = String::new();
766-
match profile.deployment_type {
767-
config::DeploymentType::Cloud => {
768-
if let Some((_, _, url)) = profile.cloud_credentials() {
769-
details = format!("URL: {}", url);
770-
}
815+
let fmt = match output_format {
816+
cli::OutputFormat::Json => output::OutputFormat::Json,
817+
cli::OutputFormat::Yaml => output::OutputFormat::Yaml,
818+
_ => output::OutputFormat::Json,
819+
};
820+
821+
crate::output::print_output(&output_data, fmt, None)?;
822+
}
823+
_ => {
824+
// Show config file path at the top
825+
if let Ok(config_path) = config::Config::config_path() {
826+
println!("Configuration file: {}", config_path.display());
827+
println!();
771828
}
772-
config::DeploymentType::Enterprise => {
773-
if let Some((url, username, _, insecure)) = profile.enterprise_credentials()
774-
{
775-
details = format!(
776-
"URL: {}, User: {}{}",
777-
url,
778-
username,
779-
if insecure { " (insecure)" } else { "" }
780-
);
781-
}
829+
830+
if profiles.is_empty() {
831+
info!("No profiles configured");
832+
println!("No profiles configured.");
833+
println!("Use 'redisctl profile set' to create a profile.");
834+
return Ok(());
782835
}
783-
}
784836

785-
let is_default_enterprise =
786-
conn_mgr.config.default_enterprise.as_deref() == Some(name);
787-
let is_default_cloud = conn_mgr.config.default_cloud.as_deref() == Some(name);
788-
let name_display = if is_default_enterprise || is_default_cloud {
789-
format!("{}*", name)
790-
} else {
791-
name.to_string()
792-
};
837+
println!("{:<15} {:<12} DETAILS", "NAME", "TYPE");
838+
println!("{:-<15} {:-<12} {:-<30}", "", "", "");
793839

794-
println!(
795-
"{:<15} {:<12} {}",
796-
name_display, profile.deployment_type, details
797-
);
840+
for (name, profile) in profiles {
841+
let mut details = String::new();
842+
match profile.deployment_type {
843+
config::DeploymentType::Cloud => {
844+
if let Some((_, _, url)) = profile.cloud_credentials() {
845+
details = format!("URL: {}", url);
846+
}
847+
}
848+
config::DeploymentType::Enterprise => {
849+
if let Some((url, username, _, insecure)) =
850+
profile.enterprise_credentials()
851+
{
852+
details = format!(
853+
"URL: {}, User: {}{}",
854+
url,
855+
username,
856+
if insecure { " (insecure)" } else { "" }
857+
);
858+
}
859+
}
860+
}
861+
862+
let is_default_enterprise =
863+
conn_mgr.config.default_enterprise.as_deref() == Some(name);
864+
let is_default_cloud =
865+
conn_mgr.config.default_cloud.as_deref() == Some(name);
866+
let name_display = if is_default_enterprise || is_default_cloud {
867+
format!("{}*", name)
868+
} else {
869+
name.to_string()
870+
};
871+
872+
println!(
873+
"{:<15} {:<12} {}",
874+
name_display, profile.deployment_type, details
875+
);
876+
}
877+
}
798878
}
799879

800880
Ok(())
801881
}
802882

803883
Path => {
804884
let config_path = config::Config::config_path()?;
805-
println!("{}", config_path.display());
885+
886+
match output_format {
887+
cli::OutputFormat::Json | cli::OutputFormat::Yaml => {
888+
let output_data = serde_json::json!({
889+
"config_path": config_path.to_str()
890+
});
891+
892+
let fmt = match output_format {
893+
cli::OutputFormat::Json => output::OutputFormat::Json,
894+
cli::OutputFormat::Yaml => output::OutputFormat::Yaml,
895+
_ => output::OutputFormat::Json,
896+
};
897+
898+
crate::output::print_output(&output_data, fmt, None)?;
899+
}
900+
_ => {
901+
println!("{}", config_path.display());
902+
}
903+
}
806904
Ok(())
807905
}
808906

809907
Show { name } => match conn_mgr.config.profiles.get(name) {
810908
Some(profile) => {
811-
println!("Profile: {}", name);
812-
println!("Type: {}", profile.deployment_type);
909+
let is_default_enterprise =
910+
conn_mgr.config.default_enterprise.as_deref() == Some(name);
911+
let is_default_cloud = conn_mgr.config.default_cloud.as_deref() == Some(name);
813912

814-
match profile.deployment_type {
815-
config::DeploymentType::Cloud => {
816-
if let Some((api_key, _, api_url)) = profile.cloud_credentials() {
817-
println!(
818-
"API Key: {}...",
819-
&api_key[..std::cmp::min(8, api_key.len())]
820-
);
821-
println!("API URL: {}", api_url);
913+
match output_format {
914+
cli::OutputFormat::Json | cli::OutputFormat::Yaml => {
915+
let mut output_data = serde_json::json!({
916+
"name": name,
917+
"deployment_type": profile.deployment_type.to_string(),
918+
"is_default_enterprise": is_default_enterprise,
919+
"is_default_cloud": is_default_cloud,
920+
});
921+
922+
match profile.deployment_type {
923+
config::DeploymentType::Cloud => {
924+
if let Some((api_key, _, api_url)) = profile.cloud_credentials() {
925+
output_data["api_key_preview"] = serde_json::json!(format!(
926+
"{}...",
927+
&api_key[..std::cmp::min(8, api_key.len())]
928+
));
929+
output_data["api_url"] = serde_json::json!(api_url);
930+
}
931+
}
932+
config::DeploymentType::Enterprise => {
933+
if let Some((url, username, has_password, insecure)) =
934+
profile.enterprise_credentials()
935+
{
936+
output_data["url"] = serde_json::json!(url);
937+
output_data["username"] = serde_json::json!(username);
938+
output_data["password_configured"] =
939+
serde_json::json!(has_password.is_some());
940+
output_data["insecure"] = serde_json::json!(insecure);
941+
}
942+
}
822943
}
944+
945+
let fmt = match output_format {
946+
cli::OutputFormat::Json => output::OutputFormat::Json,
947+
cli::OutputFormat::Yaml => output::OutputFormat::Yaml,
948+
_ => output::OutputFormat::Json,
949+
};
950+
951+
crate::output::print_output(&output_data, fmt, None)?;
823952
}
824-
config::DeploymentType::Enterprise => {
825-
if let Some((url, username, has_password, insecure)) =
826-
profile.enterprise_credentials()
827-
{
828-
println!("URL: {}", url);
829-
println!("Username: {}", username);
830-
println!(
831-
"Password: {}",
832-
if has_password.is_some() {
833-
"configured"
834-
} else {
835-
"not set"
953+
_ => {
954+
println!("Profile: {}", name);
955+
println!("Type: {}", profile.deployment_type);
956+
957+
match profile.deployment_type {
958+
config::DeploymentType::Cloud => {
959+
if let Some((api_key, _, api_url)) = profile.cloud_credentials() {
960+
println!(
961+
"API Key: {}...",
962+
&api_key[..std::cmp::min(8, api_key.len())]
963+
);
964+
println!("API URL: {}", api_url);
836965
}
837-
);
838-
println!("Insecure: {}", insecure);
966+
}
967+
config::DeploymentType::Enterprise => {
968+
if let Some((url, username, has_password, insecure)) =
969+
profile.enterprise_credentials()
970+
{
971+
println!("URL: {}", url);
972+
println!("Username: {}", username);
973+
println!(
974+
"Password: {}",
975+
if has_password.is_some() {
976+
"configured"
977+
} else {
978+
"not set"
979+
}
980+
);
981+
println!("Insecure: {}", insecure);
982+
}
983+
}
839984
}
840-
}
841-
}
842985

843-
let is_default_enterprise =
844-
conn_mgr.config.default_enterprise.as_deref() == Some(name);
845-
let is_default_cloud = conn_mgr.config.default_cloud.as_deref() == Some(name);
846-
if is_default_enterprise {
847-
println!("Default for enterprise: yes");
848-
}
849-
if is_default_cloud {
850-
println!("Default for cloud: yes");
986+
if is_default_enterprise {
987+
println!("Default for enterprise: yes");
988+
}
989+
if is_default_cloud {
990+
println!("Default for cloud: yes");
991+
}
992+
}
851993
}
852994

853995
Ok(())

0 commit comments

Comments
 (0)