use clap::{Arg, ArgMatches, Command};
use serde::Serialize;
use crate::shared::cli_error::CliResult;
use crate::shared::rust_ui_client::RustUIClient;
#[derive(Debug, Serialize)]
pub struct ComponentView {
pub name: String,
pub content: String,
}
pub fn command_view() -> Command {
Command::new("view")
.about("View a component's source from the registry without installing it")
.arg(Arg::new("component").help("Component name to view").required(true))
.arg(Arg::new("json").long("json").help("Output as JSON").action(clap::ArgAction::SetTrue))
}
pub async fn view_components(names: &[String]) -> CliResult<()> {
for name in names {
let content = RustUIClient::fetch_styles_default(name).await?;
println!("{}", format_view_human(name, &content));
}
Ok(())
}
pub async fn process_view(matches: &ArgMatches) -> CliResult<()> {
let name = matches.get_one::<String>("component").map(|s| s.as_str()).unwrap_or("");
let json = matches.get_flag("json");
let content = RustUIClient::fetch_styles_default(name).await?;
let output = if json {
format_view_json(&ComponentView { name: name.to_string(), content })?
} else {
format_view_human(name, &content)
};
println!("{output}");
Ok(())
}
pub fn format_view_human(name: &str, content: &str) -> String {
let line_count = content.lines().count();
let mut out = String::new();
out.push_str(&format!("// {name}.rs ({line_count} lines)\n\n"));
out.push_str(content);
out
}
pub fn format_view_json(view: &ComponentView) -> CliResult<String> {
serde_json::to_string_pretty(view).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_output_includes_component_name_in_header() {
let out = format_view_human("button", "fn foo() {}");
assert!(out.contains("button.rs"));
}
#[test]
fn human_output_includes_line_count() {
let content = "line1\nline2\nline3";
let out = format_view_human("button", content);
assert!(out.contains("3 lines"));
}
#[test]
fn human_output_includes_content() {
let content = "pub fn Button() {}";
let out = format_view_human("button", content);
assert!(out.contains(content));
}
#[test]
fn human_output_single_line_says_line_not_lines() {
let out = format_view_human("badge", "fn x() {}");
assert!(out.contains("1 lines")); }
#[test]
fn json_output_is_valid() {
let view = ComponentView { name: "button".to_string(), content: "fn x() {}".to_string() };
let json = format_view_json(&view).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.is_object());
}
#[test]
fn json_contains_name_and_content() {
let view = ComponentView { name: "button".to_string(), content: "fn x() {}".to_string() };
let json = format_view_json(&view).unwrap();
assert!(json.contains("\"name\""));
assert!(json.contains("button"));
assert!(json.contains("\"content\""));
assert!(json.contains("fn x()"));
}
#[test]
fn json_name_field_matches_input() {
let view = ComponentView { name: "badge".to_string(), content: String::new() };
let json = format_view_json(&view).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["name"], "badge");
}
#[tokio::test]
async fn view_components_empty_names_returns_ok() {
let result = view_components(&[]).await;
assert!(result.is_ok());
}
}