use std::path::{Path, PathBuf};
use dbmd_core::graph::{self, ContextSlice, Direction};
use dbmd_core::store::Layer;
use dbmd_core::Store;
use crate::cli::{GraphArgs, GraphCommand, GraphTargetArgs, NeighborhoodArgs, OrphansArgs};
use crate::context::Context;
use crate::error::{CliError, CliResult};
pub fn run(ctx: &Context, args: &GraphArgs) -> CliResult {
match &args.command {
GraphCommand::Backlinks(a) => run_backlinks(ctx, a),
GraphCommand::Forwardlinks(a) => run_forwardlinks(ctx, a),
GraphCommand::Neighborhood(a) => run_neighborhood(ctx, a),
GraphCommand::Orphans(a) => run_orphans(ctx, a),
}
}
pub fn run_backlinks(ctx: &Context, args: &GraphTargetArgs) -> CliResult {
let store = Store::open(Path::new(&args.dir)).map_err(dbmd_core::Error::from)?;
let layer = parse_layer(args.r#in.as_deref())?;
let types: Vec<String> = args.r#type.clone().into_iter().collect();
let mut hits = graph::backlinks_filtered(&store, Path::new(&args.path), &types, layer)
.map_err(dbmd_core::Error::from)?;
apply_limit(&mut hits, args.limit);
emit_paths(ctx, &hits);
Ok(())
}
pub fn run_forwardlinks(ctx: &Context, args: &GraphTargetArgs) -> CliResult {
let store = Store::open(Path::new(&args.dir)).map_err(dbmd_core::Error::from)?;
let layer = parse_layer(args.r#in.as_deref())?;
let mut hits =
graph::forwardlinks(&store, Path::new(&args.path)).map_err(dbmd_core::Error::from)?;
if let Some(layer) = layer {
hits.retain(|t| node_in_layer(t, layer));
}
if let Some(type_) = &args.r#type {
let typed: std::collections::BTreeSet<PathBuf> = store
.find_by_type(type_)
.map_err(dbmd_core::Error::from)?
.into_iter()
.map(|r| bare_target(&r.path))
.collect();
hits.retain(|t| typed.contains(&bare_target(t)));
}
apply_limit(&mut hits, args.limit);
emit_paths(ctx, &hits);
Ok(())
}
pub fn run_neighborhood(ctx: &Context, args: &NeighborhoodArgs) -> CliResult {
let store = Store::open(Path::new(&args.dir)).map_err(dbmd_core::Error::from)?;
let layer = parse_layer(args.r#in.as_deref())?;
let types: Vec<String> = args.r#type.clone().into_iter().collect();
let slice = graph::neighborhood(
&store,
Path::new(&args.seed),
args.hops as u32,
&types,
Direction::Both,
)
.map_err(dbmd_core::Error::from)?;
let nodes: Vec<&dbmd_core::graph::ContextNode> = slice
.nodes
.iter()
.filter(|n| layer.map(|l| node_in_layer(&n.path, l)).unwrap_or(true))
.take(args.limit.unwrap_or(usize::MAX))
.collect();
emit_neighborhood(ctx, &slice, &nodes);
Ok(())
}
pub fn run_orphans(ctx: &Context, args: &OrphansArgs) -> CliResult {
let store = Store::open(Path::new(&args.dir)).map_err(dbmd_core::Error::from)?;
let layer = parse_layer(args.r#in.as_deref())?;
let mut hits = graph::orphans(&store, layer).map_err(dbmd_core::Error::from)?;
apply_limit(&mut hits, args.limit);
emit_paths(ctx, &hits);
Ok(())
}
fn apply_limit<T>(items: &mut Vec<T>, limit: Option<usize>) {
if let Some(n) = limit {
items.truncate(n);
}
}
fn emit_paths(ctx: &Context, paths: &[PathBuf]) {
if ctx.json {
let arr: Vec<String> = paths.iter().map(|p| path_str(p)).collect();
println!("{}", serde_json::to_string(&arr).expect("serialize paths"));
} else {
for p in paths {
println!("{}", path_str(p));
}
}
}
fn emit_neighborhood(
ctx: &Context,
slice: &ContextSlice,
nodes: &[&dbmd_core::graph::ContextNode],
) {
if ctx.json {
let nodes_json: Vec<serde_json::Value> = nodes
.iter()
.map(|n| {
let (via_path, via_dir) = match &n.via {
Some((p, d)) => (Some(path_str(p)), Some(direction_str(*d))),
None => (None, None),
};
serde_json::json!({
"path": path_str(&n.path),
"summary": n.summary,
"type": n.type_,
"hops": n.hops,
"via": via_path,
"direction": via_dir,
})
})
.collect();
let out = serde_json::json!({
"seed": path_str(&slice.seed),
"nodes": nodes_json,
});
println!(
"{}",
serde_json::to_string(&out).expect("serialize neighborhood")
);
} else {
for n in nodes {
println!("{}\t{}\t{}", path_str(&n.path), n.hops, n.summary);
}
}
}
fn path_str(p: &Path) -> String {
p.to_string_lossy().replace('\\', "/")
}
fn bare_target(p: &Path) -> PathBuf {
let unix = path_str(p);
PathBuf::from(unix.strip_suffix(".md").unwrap_or(&unix))
}
fn direction_str(dir: Direction) -> &'static str {
match dir {
Direction::Incoming => "incoming",
Direction::Outgoing => "outgoing",
Direction::Both => "both",
}
}
fn node_in_layer(rel: &Path, layer: Layer) -> bool {
rel.components()
.next()
.and_then(|c| c.as_os_str().to_str())
.map(|first| first == layer.dir_name())
.unwrap_or(false)
}
fn parse_layer(value: Option<&str>) -> Result<Option<Layer>, CliError> {
match value {
None => Ok(None),
Some(name) => Layer::from_dir_name(name).map(Some).ok_or_else(|| {
CliError::new(
crate::error::ExitCode::Runtime,
"BAD_LAYER",
format!("unknown layer `{name}` (expected sources, records, or wiki)"),
)
}),
}
}