use super::CliResult;
use super::catalog::{
CatalogSourceFilter, StyleCatalogPage, StyleCatalogRow, paginate_style_catalog_rows,
print_style_catalog_rows, style_catalog_entries, style_row_matches_query,
};
use super::lint::run_lint_style;
use super::style_install::{CliStyleBrowserActions, run_style_add, run_style_remove};
use crate::args::{StyleCatalogFormat, StyleCommands};
use crate::style_browser::{StyleBrowserConfig, run_style_browser};
use citum_schema::{Locale, Style};
use citum_store::{StoreConfig, StoreResolver, platform_data_dir};
use std::error::Error;
use std::io::{self, IsTerminal};
pub(super) fn run_style_list(
source: &str,
format: StyleCatalogFormat,
limit: Option<usize>,
offset: usize,
) -> CliResult {
let source_filter = CatalogSourceFilter::parse(source)?;
let rows = style_catalog_entries(source_filter)?;
let (total, rows) = paginate_style_catalog_rows(rows, StyleCatalogPage { limit, offset });
print_style_catalog_rows(&rows, total, &source_filter.label(), format)
}
pub(super) fn run_style_search(
query: &str,
source: &str,
format: StyleCatalogFormat,
limit: Option<usize>,
offset: usize,
) -> CliResult {
let source_filter = CatalogSourceFilter::parse(source)?;
let rows: Vec<_> = style_catalog_entries(source_filter)?
.into_iter()
.filter(|row| style_row_matches_query(row, query))
.collect();
let (total, rows) = paginate_style_catalog_rows(rows, StyleCatalogPage { limit, offset });
print_style_catalog_rows(&rows, total, &source_filter.label(), format)
}
pub(super) fn run_style_info(name: &str, format: StyleCatalogFormat) -> CliResult {
let rows = style_catalog_entries(CatalogSourceFilter::All)?;
let row = rows
.into_iter()
.find(|row| row.id == name || row.aliases.iter().any(|alias| alias == name))
.ok_or_else(|| format!("style not found: {name}"))?;
let unresolved = load_unresolved_style(&row.id).ok();
let cid_string = unresolved.as_ref().and_then(|style| {
let canonical = serde_yaml::to_string(style).ok()?;
Some(citum_store::cid::compute_style_cid(canonical.as_bytes()))
});
let citum_version = unresolved
.as_ref()
.and_then(|style| style.info.citum_version.clone());
if format == StyleCatalogFormat::Json {
let mut value = serde_json::to_value(&row)?;
if let Some(map) = value.as_object_mut() {
if let Some(cid) = &cid_string {
map.insert("cid".to_string(), serde_json::Value::String(cid.clone()));
}
if let Some(req) = &citum_version {
map.insert(
"citum-version".to_string(),
serde_json::Value::String(req.clone()),
);
}
}
println!("{}", serde_json::to_string_pretty(&value)?);
return Ok(());
}
println!("ID: {}", row.id);
println!("Title: {}", row.title.as_deref().unwrap_or("-"));
println!("Source: {}", row.source);
println!(
"Aliases: {}",
if row.aliases.is_empty() {
"-".to_string()
} else {
row.aliases.join(", ")
}
);
if let Some(description) = row.description {
println!("Summary: {description}");
}
if !row.fields.is_empty() {
println!("Fields: {}", row.fields.join(", "));
}
if let Some(url) = row.url {
println!("URL: {url}");
}
if let Some(req) = &citum_version {
println!("Citum: {req}");
}
if let Some(cid) = &cid_string {
println!("CID: {cid}");
println!("Pin: extends-pin: {cid}");
}
Ok(())
}
pub(super) fn run_style_cid(target: &str, format: StyleCatalogFormat) -> CliResult {
let bytes = read_target_bytes(target)?;
let cid = citum_store::cid::compute_style_cid(&bytes);
if format == StyleCatalogFormat::Json {
let value = serde_json::json!({
"target": target,
"cid": cid,
});
println!("{}", serde_json::to_string_pretty(&value)?);
} else {
println!("{cid}");
}
Ok(())
}
pub(super) fn run_style_pin(
target: &str,
uri_override: Option<&str>,
format: StyleCatalogFormat,
) -> CliResult {
let bytes = read_target_bytes(target)?;
let cid = citum_store::cid::compute_style_cid(&bytes);
let uri = uri_override
.map(str::to_string)
.unwrap_or_else(|| derive_pin_uri(target));
if format == StyleCatalogFormat::Json {
let value = serde_json::json!({
"extends": uri,
"extends-pin": format!("cid:{cid}"),
});
println!("{}", serde_json::to_string_pretty(&value)?);
} else {
println!("extends: {uri}");
println!("extends-pin: cid:{cid}");
}
Ok(())
}
pub(super) fn run_style_validate(target: &str, format: StyleCatalogFormat) -> CliResult {
let style = load_unresolved_style(target)?;
let warnings = style.validate();
let resolved = style
.clone()
.try_into_resolved_with(Some(&build_chain_resolver()?))?;
let canonical = serde_yaml::to_string(&style)?;
let cid = citum_store::cid::compute_style_cid(canonical.as_bytes());
if format == StyleCatalogFormat::Json {
let value = serde_json::json!({
"target": target,
"ok": warnings.is_empty(),
"warnings": warnings.iter().map(ToString::to_string).collect::<Vec<_>>(),
"cid": cid,
"citum-version": resolved.info.citum_version,
});
println!("{}", serde_json::to_string_pretty(&value)?);
} else {
println!("OK {target}");
println!("CID {cid}");
if let Some(ref req) = resolved.info.citum_version {
println!("Citum {req}");
}
for warning in &warnings {
println!("warn {warning}");
}
}
Ok(())
}
fn read_target_bytes(target: &str) -> Result<Vec<u8>, Box<dyn Error>> {
let style = load_unresolved_style(target)?;
Ok(serde_yaml::to_string(&style)?.into_bytes())
}
pub(super) fn load_unresolved_style(target: &str) -> Result<Style, Box<dyn Error>> {
use citum_store::resolver::{ChainResolver, EmbeddedResolver, FileResolver, StyleResolver};
let path = std::path::Path::new(target);
if path.is_file() {
let bytes = std::fs::read(path)?;
return Ok(Style::from_yaml_bytes(&bytes)?);
}
let mut resolvers: Vec<Box<dyn StyleResolver<Style = Style, Locale = Locale>>> =
vec![Box::new(FileResolver)];
if let Some(data_dir) = platform_data_dir()
&& data_dir.exists()
{
let cfg = StoreConfig::load().unwrap_or_default();
resolvers.push(Box::new(StoreResolver::new(data_dir, cfg.store_format())));
}
resolvers.push(Box::new(EmbeddedResolver));
let chain = ChainResolver::new(resolvers);
Ok(chain.resolve_style(target)?)
}
fn build_chain_resolver()
-> Result<impl citum_resolver_api::StyleResolver<Style = Style, Locale = Locale>, Box<dyn Error>> {
use citum_store::resolver::{ChainResolver, EmbeddedResolver, FileResolver, StyleResolver};
let mut resolvers: Vec<Box<dyn StyleResolver<Style = Style, Locale = Locale>>> =
vec![Box::new(FileResolver)];
if let Some(data_dir) = platform_data_dir()
&& data_dir.exists()
{
let cfg = StoreConfig::load().unwrap_or_default();
resolvers.push(Box::new(StoreResolver::new(data_dir, cfg.store_format())));
}
resolvers.push(Box::new(EmbeddedResolver));
Ok(ChainResolver::new(resolvers))
}
fn derive_pin_uri(target: &str) -> String {
let path = std::path::Path::new(target);
if path.is_file() {
let abs = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
url::Url::from_file_path(&abs)
.map(|u| u.to_string())
.unwrap_or_else(|()| format!("file://{}", abs.to_string_lossy()))
} else {
format!("https://hub.citum.org/styles/{target}.yaml")
}
}
pub(super) fn dispatch(command: StyleCommands) -> CliResult {
match command {
StyleCommands::List {
source,
format,
limit,
offset,
} => run_style_list(&source, format, limit, offset),
StyleCommands::Search {
query,
source,
format,
limit,
offset,
} => run_style_search(&query, &source, format, limit, offset),
StyleCommands::Info { name, format } => run_style_info(&name, format),
StyleCommands::Browse { query, source } => run_style_browse(query.as_deref(), &source),
StyleCommands::Add { query, yes } => run_style_add(&query, yes),
StyleCommands::Remove { name, yes } => run_style_remove(&name, yes),
StyleCommands::Lint(args) => run_lint_style(args),
StyleCommands::Cid { target, format } => run_style_cid(&target, format),
StyleCommands::Pin {
target,
uri,
format,
} => run_style_pin(&target, uri.as_deref(), format),
StyleCommands::Validate { target, format } => run_style_validate(&target, format),
}
}
pub(super) fn run_style_browse(query: Option<&str>, source: &str) -> CliResult {
let source_filter = CatalogSourceFilter::parse(source)?;
let all_rows = style_catalog_entries(source_filter)?;
if !io::stdin().is_terminal() {
let rows: Vec<StyleCatalogRow> = all_rows
.into_iter()
.filter(|row| query.is_none_or(|q| style_row_matches_query(row, q)))
.take(20)
.collect();
print_style_catalog_rows(
&rows,
rows.len(),
&source_filter.label(),
StyleCatalogFormat::Text,
)?;
return Ok(());
}
let mut actions = CliStyleBrowserActions;
run_style_browser(
StyleBrowserConfig {
rows: all_rows,
initial_query: query.unwrap_or("").to_string(),
source_label: source_filter.label(),
},
&mut actions,
)
}