use omgbase_surface::read::{ResolvedRef, find_doc_by_id, resolve_ref};
use omgbase_surface::reference_path;
use serde_json::{Value as Json, json};
use crate::cli::argv::{Opt, expand_dash, parse_args};
use crate::cli::context::Cli;
use crate::cli::help::{render_help_for, wants_help};
use crate::cli::output::{CliError, EXIT_OK, Result};
use crate::cli::render::type_glyph;
use super::links::{LinksQuery, doc_links, render_link_groups};
use super::{fmt_value, machine_out, str_of};
enum Card {
Document {
payload: Json,
path: String,
properties: Json,
links: Json,
},
Block {
payload: Json,
history: Option<Json>,
},
}
impl Card {
fn payload(&self) -> &Json {
match self {
Card::Document { payload, .. } | Card::Block { payload, .. } => payload,
}
}
}
fn show_one(cli: &mut Cli, r: &str, include_history: bool) -> Result<Card> {
let repo = cli.repo()?;
let resolved = resolve_ref(cli.store()?.conn(), &repo.repo_id, r)?
.ok_or_else(|| CliError::engine("doc_missing", format!("no node {r}")))?;
match resolved {
ResolvedRef::Document { doc_id } => {
let (path, properties, links) = {
let store = cli.store()?;
let info = find_doc_by_id(store.conn(), &doc_id)?
.ok_or_else(|| CliError::engine("doc_missing", format!("no node {r}")))?;
let links = doc_links(store.conn(), &doc_id, &LinksQuery::default())?;
(
reference_path(&info.path),
store.properties_merged(&doc_id)?,
links,
)
};
let read = cli.call("docs_read", json!({ "id": doc_id }))?;
let mut tagged = serde_json::Map::new();
tagged.insert("kind".to_owned(), json!("document"));
if let Json::Object(m) = read {
tagged.extend(m);
}
let payload = Json::Object(tagged);
Ok(Card::Document {
payload,
path,
properties,
links,
})
}
ResolvedRef::Block { block_id, .. } => {
let node = cli.call(
"blocks_read",
json!({ "id": block_id, "resolution": "full" }),
)?;
let history = if include_history {
Some(cli.call("blocks_history", json!({ "id": block_id, "limit": 5 }))?)
} else {
None
};
let mut payload = node;
if let (Some(h), Some(m)) = (&history, payload.as_object_mut()) {
m.insert("history".to_owned(), h.clone());
}
Ok(Card::Block { payload, history })
}
}
}
fn render_doc_card(cli: &Cli, path: &str, properties: &Json, payload: &Json, links: &Json) {
let style = cli.style;
cli.io.out(&style.wordmark(path));
cli.io.out(&style.rule());
let line = |k: &str, v: &Json| {
cli.io.out(&format!(
" {} {} {}",
style.accent(k),
style.dim("="),
fmt_value(v)
));
};
let authored: Vec<(&String, &Json)> = properties
.as_object()
.map(|m| m.iter().filter(|(k, _)| !k.starts_with('$')).collect())
.unwrap_or_default();
if !authored.is_empty() {
cli.io.out(&format!(" {}", style.dim("properties")));
for (k, v) in authored {
line(k, v);
}
}
let computed = payload
.get("properties")
.and_then(|p| p.get("computed"))
.and_then(Json::as_object)
.filter(|m| !m.is_empty());
if let Some(m) = computed {
cli.io.out(&format!(" {}", style.dim("computed")));
for (k, v) in m {
line(k, v);
}
}
render_link_groups(cli, links, " out links", " backlinks");
}
fn render_block_card(cli: &Cli, node: &Json, history: Option<&Json>) {
let style = cli.style;
let kind = str_of(node, "type");
cli.io.out(&format!(
" {} {} {}",
type_glyph(&style, &kind),
style.bold(&kind),
style.id(&str_of(node, "id"))
));
cli.io.out(&style.rule());
if node.get("ordinal").is_some() {
let parent = node
.get("parent_id")
.and_then(Json::as_str)
.unwrap_or("—")
.to_owned();
cli.io.out(&format!(
" {} parent={} ordinal={} depth={}",
style.dim("placement"),
style.id(&parent),
fmt_value(node.get("ordinal").unwrap_or(&Json::Null)),
fmt_value(node.get("depth").unwrap_or(&Json::Null))
));
}
if let Some(attrs) = node.get("attrs").and_then(Json::as_object) {
if !attrs.is_empty() {
cli.io.out(&format!(" {}", style.dim("attrs")));
for (k, v) in attrs {
cli.io.out(&format!(
" {} {} {}",
style.accent(k),
style.dim("="),
fmt_value(v)
));
}
}
}
if let Some(text) = node.get("text").and_then(Json::as_str) {
if !text.is_empty() {
cli.io.out(&format!(" {} {text}", style.dim("text")));
}
}
if let Some(items) = history.and_then(Json::as_array) {
if !items.is_empty() {
cli.io.out(&format!(" {}", style.dim("history")));
for h in items {
cli.io.out(&format!(
" {} {} {} {}",
style.dim(&format!(
"#{}",
fmt_value(h.get("seq").unwrap_or(&Json::Null))
)),
style.accent(&str_of(h, "kind")),
style.dim(&str_of(h, "origin")),
style.dim(&str_of(h, "ts"))
));
}
}
}
}
pub fn show(cli: &mut Cli, args: &[String]) -> Result<i32> {
if wants_help(args) {
return render_help_for(cli, "show");
}
let a = parse_args(args, &[Opt::value("include")])?;
let refs = expand_dash(&a.positionals);
if refs.is_empty() {
return Err(CliError::usage(
"show requires a <node> (or - to read refs from stdin)",
));
}
let include_history = a
.value("include")
.unwrap_or("")
.split(',')
.any(|s| s.trim() == "history");
let mut cards: Vec<Card> = Vec::with_capacity(refs.len());
for r in &refs {
cards.push(show_one(cli, r, include_history)?);
}
let items: Vec<Json> = cards.iter().map(|c| c.payload().clone()).collect();
let doc = if items.len() == 1 {
items[0].clone()
} else {
Json::Array(items.clone())
};
cli.capture(&doc); if let Some(code) = machine_out(cli, &doc, Some(&items), None) {
return code;
}
for c in &cards {
match c {
Card::Document {
payload,
path,
properties,
links,
} => render_doc_card(cli, path, properties, payload, links),
Card::Block { payload, history } => render_block_card(cli, payload, history.as_ref()),
}
}
Ok(EXIT_OK)
}