use anyhow::{Error, Result};
use clap::{Parser, Subcommand};
use log::info;
use ontoenv::api::{OntoEnv, ResolveTarget};
use ontoenv::config::Config;
use ontoenv::ontology::{GraphIdentifier, OntologyLocation};
use ontoenv::options::{Overwrite, RefreshStrategy};
use ontoenv::util::write_dataset_to_file;
use ontoenv::ToUriString;
use oxigraph::io::{JsonLdProfileSet, RdfFormat};
use oxigraph::model::NamedNode;
use std::collections::{BTreeMap, BTreeSet};
use std::env::current_dir;
use std::ffi::OsString;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(name = "ontoenv")]
#[command(about = "Ontology environment manager")]
#[command(arg_required_else_help = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[clap(long, short, action, default_value = "false", global = true)]
verbose: bool,
#[clap(long, action, default_value = "false", global = true)]
debug: bool,
#[clap(long, short, default_value = "default", global = true)]
policy: Option<String>,
#[clap(long, short, action, global = true)]
temporary: bool,
#[clap(long, action, global = true)]
require_ontology_names: bool,
#[clap(long, action, default_value = "false", global = true)]
strict: bool,
#[clap(long, short, action, default_value = "false", global = true)]
offline: bool,
#[clap(long, short, num_args = 1.., global = true)]
includes: Vec<String>,
#[clap(long, short, num_args = 1.., global = true)]
excludes: Vec<String>,
#[clap(long = "include-ontology", alias = "io", num_args = 1.., global = true)]
include_ontologies: Vec<String>,
#[clap(long = "exclude-ontology", alias = "eo", num_args = 1.., global = true)]
exclude_ontologies: Vec<String>,
#[clap(long = "remote-cache-ttl-secs", value_parser, global = true)]
remote_cache_ttl_secs: Option<u64>,
}
#[derive(Debug, Subcommand)]
enum ConfigCommands {
Set {
key: String,
value: String,
},
Get {
key: String,
},
Unset {
key: String,
},
Add {
key: String,
value: String,
},
Remove {
key: String,
value: String,
},
List,
}
#[derive(Debug, Subcommand)]
enum ListCommands {
Locations,
Ontologies,
Missing,
}
#[derive(Debug, Subcommand)]
enum Commands {
Init {
#[clap(long, default_value = "false")]
overwrite: bool,
#[clap(value_name = "LOCATION", num_args = 0.., value_parser)]
locations: Vec<PathBuf>,
},
Version,
Status {
#[clap(long, action, default_value = "false")]
json: bool,
},
Update {
#[clap(long, short = 'q', action)]
quiet: bool,
#[clap(long, short = 'a', action)]
all: bool,
#[clap(long, action, default_value = "false")]
json: bool,
},
Closure {
ontology: String,
#[clap(long, action, default_value = "false")]
no_rewrite_sh_prefixes: bool,
#[clap(long, action, default_value = "false")]
keep_owl_imports: bool,
destination: Option<String>,
#[clap(long, default_value = "-1")]
recursion_depth: i32,
},
Get {
ontology: String,
#[clap(long, short = 'l')]
location: Option<String>,
#[clap(long)]
output: Option<String>,
#[clap(long, short = 'f')]
format: Option<String>,
},
Add {
location: String,
#[clap(long, action)]
no_imports: bool,
},
List {
#[command(subcommand)]
list_cmd: ListCommands,
#[clap(long, action, default_value = "false")]
json: bool,
},
Dump {
contains: Option<String>,
},
DepGraph {
roots: Option<Vec<String>>,
#[clap(long, short)]
output: Option<String>,
},
Why {
ontologies: Vec<String>,
#[clap(long, action, default_value = "false")]
json: bool,
},
Doctor {
#[clap(long, action, default_value = "false")]
json: bool,
},
Reset {
#[clap(long, short, action = clap::ArgAction::SetTrue, default_value = "false")]
force: bool,
},
Namespaces {
ontology: Option<String>,
#[clap(long, action, default_value = "false")]
closure: bool,
#[clap(long, action, default_value = "false")]
json: bool,
},
#[command(subcommand)]
Config(ConfigCommands),
}
impl std::fmt::Display for Commands {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Commands::Init { .. } => "Init",
Commands::Version => "Version",
Commands::Status { .. } => "Status",
Commands::Update { .. } => "Update",
Commands::Closure { .. } => "Closure",
Commands::Get { .. } => "Get",
Commands::Add { .. } => "Add",
Commands::List { .. } => "List",
Commands::Dump { .. } => "Dump",
Commands::DepGraph { .. } => "DepGraph",
Commands::Why { .. } => "Why",
Commands::Doctor { .. } => "Doctor",
Commands::Reset { .. } => "Reset",
Commands::Namespaces { .. } => "Namespaces",
Commands::Config { .. } => "Config",
};
f.write_str(name)
}
}
fn handle_config_command(config_cmd: ConfigCommands, temporary: bool) -> Result<()> {
if temporary {
return Err(anyhow::anyhow!("Cannot manage config in temporary mode."));
}
let root = ontoenv::api::find_ontoenv_root()
.ok_or_else(|| anyhow::anyhow!("Not in an ontoenv. Use `ontoenv init` to create one."))?;
let config_path = root.join(".ontoenv").join("ontoenv.json");
if !config_path.exists() {
return Err(anyhow::anyhow!(
"No ontoenv.json found. Use `ontoenv init`."
));
}
match config_cmd {
ConfigCommands::List => {
let config_str = std::fs::read_to_string(&config_path)?;
let config_json: serde_json::Value = serde_json::from_str(&config_str)?;
let pretty_json = serde_json::to_string_pretty(&config_json)?;
println!("{}", pretty_json);
return Ok(());
}
ConfigCommands::Get { ref key } => {
let config_str = std::fs::read_to_string(&config_path)?;
let config_json: serde_json::Value = serde_json::from_str(&config_str)?;
let object = config_json
.as_object()
.ok_or_else(|| anyhow::anyhow!("Invalid config format: not a JSON object."))?;
if let Some(value) = object.get(key) {
if let Some(s) = value.as_str() {
println!("{}", s);
} else if let Some(arr) = value.as_array() {
for item in arr {
if let Some(s) = item.as_str() {
println!("{}", s);
} else {
println!("{}", item);
}
}
} else {
println!("{}", value);
}
} else {
println!("Configuration key '{}' not set.", key);
}
return Ok(());
}
_ => {}
}
let config_str = std::fs::read_to_string(&config_path)?;
let mut config_json: serde_json::Value = serde_json::from_str(&config_str)?;
let object = config_json
.as_object_mut()
.ok_or_else(|| anyhow::anyhow!("Invalid config format: not a JSON object."))?;
match config_cmd {
ConfigCommands::Set { key, value } => {
match key.as_str() {
"offline" | "strict" | "require_ontology_names" => {
let bool_val = value.parse::<bool>().map_err(|_| {
anyhow::anyhow!("Invalid boolean value for {}: {}", key, value)
})?;
object.insert(key.to_string(), serde_json::Value::Bool(bool_val));
}
"resolution_policy" => {
object.insert(key.to_string(), serde_json::Value::String(value.clone()));
}
"remote_cache_ttl_secs" => {
let ttl = value
.parse::<u64>()
.map_err(|_| anyhow::anyhow!("Invalid u64 value for {}: {}", key, value))?;
object.insert(key.to_string(), serde_json::Value::Number(ttl.into()));
}
"locations" | "includes" | "excludes" => {
return Err(anyhow::anyhow!(
"Use `ontoenv config add/remove {} <value>` to modify list values.",
key
));
}
_ => {
return Err(anyhow::anyhow!(
"Setting configuration for '{}' is not supported.",
key
));
}
}
println!("Set {} to {}", key, value);
}
ConfigCommands::Unset { key } => {
if object.remove(&key).is_some() {
println!("Unset '{}'.", key);
} else {
return Err(anyhow::anyhow!("Configuration key '{}' not set.", key));
}
}
ConfigCommands::Add { key, value } => {
match key.as_str() {
"locations" | "includes" | "excludes" => {
let entry = object
.entry(key.clone())
.or_insert_with(|| serde_json::Value::Array(vec![]));
if let Some(arr) = entry.as_array_mut() {
let new_val = serde_json::Value::String(value.clone());
if !arr.contains(&new_val) {
arr.push(new_val);
} else {
println!("Value '{}' already exists in {}.", value, key);
return Ok(());
}
}
}
_ => {
return Err(anyhow::anyhow!(
"Cannot add to configuration key '{}'. It is not a list.",
key
));
}
}
println!("Added '{}' to {}", value, key);
}
ConfigCommands::Remove { key, value } => {
match key.as_str() {
"locations" | "includes" | "excludes" => {
if let Some(entry) = object.get_mut(&key) {
if let Some(arr) = entry.as_array_mut() {
let val_to_remove = serde_json::Value::String(value.clone());
if let Some(pos) = arr.iter().position(|x| *x == val_to_remove) {
arr.remove(pos);
} else {
return Err(anyhow::anyhow!(
"Value '{}' not found in {}",
value,
key
));
}
}
} else {
return Err(anyhow::anyhow!("Configuration key '{}' not set.", key));
}
}
_ => {
return Err(anyhow::anyhow!(
"Cannot remove from configuration key '{}'. It is not a list.",
key
));
}
}
println!("Removed '{}' from {}", value, key);
}
_ => unreachable!(), }
let new_config_str = serde_json::to_string_pretty(&config_json)?;
std::fs::write(config_path, new_config_str)?;
Ok(())
}
pub fn run() -> Result<()> {
ontoenv::api::init_logging();
let cmd = Cli::parse();
execute(cmd)
}
pub fn run_from_args<I, T>(args: I) -> Result<()>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
ontoenv::api::init_logging();
let cmd = Cli::try_parse_from(args).map_err(Error::from)?;
execute(cmd)
}
fn execute(cmd: Cli) -> Result<()> {
if cmd.debug {
std::env::set_var("RUST_LOG", "debug");
} else if cmd.verbose {
std::env::set_var("RUST_LOG", "info");
} else if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "warn");
}
let _ = env_logger::try_init();
let policy = cmd.policy.unwrap_or_else(|| "default".to_string());
let cwd = current_dir()?;
let mut builder = Config::builder()
.root(cwd.clone())
.require_ontology_names(cmd.require_ontology_names)
.strict(cmd.strict)
.offline(cmd.offline)
.resolution_policy(policy)
.temporary(cmd.temporary);
if let Commands::Init { locations, .. } = &cmd.command {
builder = builder.locations(locations.clone());
}
if !cmd.includes.is_empty() {
builder = builder.includes(&cmd.includes);
}
if !cmd.excludes.is_empty() {
builder = builder.excludes(&cmd.excludes);
}
if !cmd.include_ontologies.is_empty() {
builder = builder.include_ontologies(&cmd.include_ontologies);
}
if !cmd.exclude_ontologies.is_empty() {
builder = builder.exclude_ontologies(&cmd.exclude_ontologies);
}
if let Some(ttl) = cmd.remote_cache_ttl_secs {
builder = builder.remote_cache_ttl_secs(ttl);
}
let config: Config = builder.build()?;
if cmd.verbose || cmd.debug {
config.print();
}
if let Commands::Reset { force } = &cmd.command {
if let Some(root) = ontoenv::api::find_ontoenv_root() {
let path = root.join(".ontoenv");
println!("Removing .ontoenv directory at {}...", path.display());
if !*force {
let mut input = String::new();
println!("Are you sure you want to delete the .ontoenv directory? [y/N] ");
std::io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
let input = input.trim();
if input != "y" && input != "Y" {
println!("Aborting...");
return Ok(());
}
}
OntoEnv::reset()?;
println!(".ontoenv directory removed.");
} else {
println!("No .ontoenv directory found. Nothing to do.");
}
return Ok(());
}
let env_dir_var = std::env::var("ONTOENV_DIR").ok().map(PathBuf::from);
let discovered_root = if let Some(dir) = env_dir_var.clone() {
if dir.file_name().map(|n| n == ".ontoenv").unwrap_or(false) {
dir.parent().map(|p| p.to_path_buf())
} else {
Some(dir)
}
} else {
ontoenv::api::find_ontoenv_root()
};
let ontoenv_exists = discovered_root
.as_ref()
.map(|root| root.join(".ontoenv").join("ontoenv.json").exists())
.unwrap_or(false);
info!("OntoEnv exists: {ontoenv_exists}");
let needs_rw = matches!(cmd.command, Commands::Add { .. } | Commands::Update { .. });
let env: Option<OntoEnv> = if cmd.temporary {
let e = OntoEnv::init(config.clone(), false)?;
Some(e)
} else if cmd.command.to_string() != "Init" && ontoenv_exists {
Some(OntoEnv::load_from_directory(
discovered_root.unwrap(),
!needs_rw,
)?)
} else {
None
};
info!("OntoEnv loaded: {}", env.is_some());
match cmd.command {
Commands::Init { overwrite, .. } => {
if cmd.temporary {
return Err(anyhow::anyhow!(
"Cannot initialize in temporary mode. Run `ontoenv init` without --temporary."
));
}
let root = current_dir()?;
if root.join(".ontoenv").exists() && !overwrite {
println!(
"An ontology environment already exists in: {}",
root.display()
);
println!("Use --overwrite to re-initialize or `ontoenv update` to update.");
let env = OntoEnv::load_from_directory(root, false)?;
let status = env.status()?;
println!("\nCurrent status:");
println!("{status}");
return Ok(());
}
let _ = OntoEnv::init(config, overwrite)?;
}
Commands::Get {
ontology,
location,
output,
format,
} => {
let env = require_ontoenv(env)?;
let graph = if let Some(loc) = location {
let oloc = if loc.starts_with("http://") || loc.starts_with("https://") {
OntologyLocation::Url(loc)
} else {
ontoenv::ontology::OntologyLocation::from_str(&loc)
.unwrap_or_else(|_| OntologyLocation::File(PathBuf::from(loc)))
};
oloc.graph()?
} else {
let iri = NamedNode::new(ontology).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let graphid = env
.resolve(ResolveTarget::Graph(iri))
.ok_or(anyhow::anyhow!("Ontology not found"))?;
env.get_graph(&graphid)?
};
let fmt = match format
.as_deref()
.unwrap_or("turtle")
.to_ascii_lowercase()
.as_str()
{
"turtle" | "ttl" => RdfFormat::Turtle,
"ntriples" | "nt" => RdfFormat::NTriples,
"rdfxml" | "xml" => RdfFormat::RdfXml,
"jsonld" | "json-ld" => RdfFormat::JsonLd {
profile: JsonLdProfileSet::default(),
},
other => {
return Err(anyhow::anyhow!(
"Unsupported format '{}'. Use one of: turtle, ntriples, rdfxml, jsonld",
other
))
}
};
if let Some(path) = output {
let mut file = std::fs::File::create(path)?;
let mut serializer =
oxigraph::io::RdfSerializer::from_format(fmt).for_writer(&mut file);
for t in graph.iter() {
serializer.serialize_triple(t)?;
}
serializer.finish()?;
} else {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
let mut serializer =
oxigraph::io::RdfSerializer::from_format(fmt).for_writer(&mut handle);
for t in graph.iter() {
serializer.serialize_triple(t)?;
}
serializer.finish()?;
}
}
Commands::Version => {
println!(
"ontoenv {} @ {}",
env!("CARGO_PKG_VERSION"),
env!("GIT_HASH")
);
}
Commands::Status { json } => {
let env = require_ontoenv(env)?;
if json {
let status = env.status()?;
let missing: Vec<String> = status
.missing_imports()
.iter()
.map(|n| n.to_uri_string())
.collect();
let last_str = status.last_updated().map(|t| t.to_rfc3339());
let ontoenv_path = status.ontoenv_path().map(|path| path.display().to_string());
let obj = serde_json::json!({
"exists": status.exists(),
"ontoenv_path": ontoenv_path,
"num_ontologies": status.num_ontologies(),
"last_updated": last_str,
"store_size_bytes": status.store_size(),
"missing_imports": missing,
});
println!("{}", serde_json::to_string_pretty(&obj)?);
} else {
let status = env.status()?;
println!("{status}");
}
}
Commands::Update { quiet, all, json } => {
let mut env = require_ontoenv(env)?;
let updated = env.update_all(all)?;
if json {
let arr: Vec<String> = updated.iter().map(|id| id.to_uri_string()).collect();
println!("{}", serde_json::to_string_pretty(&arr)?);
} else if !quiet {
for id in updated {
if let Some(ont) = env.ontologies().get(&id) {
let name = ont.name().to_string();
let loc = ont
.location()
.map(|l| l.to_string())
.unwrap_or_else(|| "N/A".to_string());
println!("{} @ {}", name, loc);
}
}
}
env.save_to_directory()?;
}
Commands::Closure {
ontology,
no_rewrite_sh_prefixes,
keep_owl_imports,
destination,
recursion_depth,
} => {
let iri = NamedNode::new(ontology).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let env = require_ontoenv(env)?;
let graphid = env
.resolve(ResolveTarget::Graph(iri.clone()))
.ok_or(anyhow::anyhow!(format!("Ontology {} not found", iri)))?;
let closure = env.get_closure(&graphid, recursion_depth)?;
let rewrite = !no_rewrite_sh_prefixes;
let remove = !keep_owl_imports;
let root = closure[0].name();
let union = env.get_union_graph(&closure, root, Some(rewrite), Some(remove))?;
if let Some(failed_imports) = union.failed_imports {
for imp in failed_imports {
eprintln!("{imp}");
}
}
let destination = destination.unwrap_or_else(|| "output.ttl".to_string());
write_dataset_to_file(&union.dataset, &destination)?;
}
Commands::Add {
location,
no_imports,
} => {
let location = if location.starts_with("http") {
OntologyLocation::Url(location)
} else {
OntologyLocation::File(PathBuf::from(location))
};
let mut env = require_ontoenv(env)?;
if no_imports {
let _ =
env.add_no_imports(location, Overwrite::Allow, RefreshStrategy::UseCache)?;
} else {
let _ = env.add(location, Overwrite::Allow, RefreshStrategy::UseCache)?;
}
}
Commands::List { list_cmd, json } => {
let env = require_ontoenv(env)?;
match list_cmd {
ListCommands::Locations => {
let mut locations = env.find_files()?;
locations.sort_by(|a, b| a.as_str().cmp(b.as_str()));
if json {
println!("{}", serde_json::to_string_pretty(&locations)?);
} else {
for loc in locations {
println!("{}", loc);
}
}
}
ListCommands::Ontologies => {
let mut ontologies: Vec<&GraphIdentifier> = env.ontologies().keys().collect();
ontologies.sort_by(|a, b| a.name().cmp(&b.name()));
ontologies.dedup_by(|a, b| a.name() == b.name());
if json {
let out: Vec<String> =
ontologies.into_iter().map(|o| o.to_uri_string()).collect();
println!("{}", serde_json::to_string_pretty(&out)?);
} else {
for ont in ontologies {
println!("{}", ont.to_uri_string());
}
}
}
ListCommands::Missing => {
let mut missing_imports = env.missing_imports();
missing_imports.sort();
if json {
let out: Vec<String> = missing_imports
.into_iter()
.map(|n| n.to_uri_string())
.collect();
println!("{}", serde_json::to_string_pretty(&out)?);
} else {
for import in missing_imports {
println!("{}", import.to_uri_string());
}
}
}
}
}
Commands::Dump { contains } => {
let env = require_ontoenv(env)?;
env.dump(contains.as_deref());
}
Commands::DepGraph { roots, output } => {
let env = require_ontoenv(env)?;
let dot = if let Some(roots) = roots {
let roots: Vec<GraphIdentifier> = roots
.iter()
.map(|iri| {
env.resolve(ResolveTarget::Graph(NamedNode::new(iri).unwrap()))
.unwrap()
.clone()
})
.collect();
env.rooted_dep_graph_to_dot(roots)?
} else {
env.dep_graph_to_dot()?
};
let dot_path = current_dir()?.join("dep_graph.dot");
std::fs::write(&dot_path, dot)?;
let output_path = output.unwrap_or_else(|| "dep_graph.pdf".to_string());
let output = std::process::Command::new("dot")
.args(["-Tpdf", dot_path.to_str().unwrap(), "-o", &output_path])
.output()?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"Failed to generate PDF: {}",
String::from_utf8_lossy(&output.stderr)
));
}
}
Commands::Why { ontologies, json } => {
let env = require_ontoenv(env)?;
if json {
let mut all: BTreeMap<String, Vec<Vec<String>>> = BTreeMap::new();
for ont in ontologies {
let iri = NamedNode::new(ont).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let (paths, missing) = match env.explain_import(&iri)? {
ontoenv::api::ImportPaths::Present(paths) => (paths, false),
ontoenv::api::ImportPaths::Missing { importers } => (importers, true),
};
let formatted = format_import_paths(&iri, paths, missing);
all.insert(iri.to_uri_string(), formatted);
}
println!("{}", serde_json::to_string_pretty(&all)?);
} else {
for ont in ontologies {
let iri = NamedNode::new(ont).map_err(|e| anyhow::anyhow!(e.to_string()))?;
match env.explain_import(&iri)? {
ontoenv::api::ImportPaths::Present(paths) => {
print_import_paths(&iri, paths, false);
}
ontoenv::api::ImportPaths::Missing { importers } => {
print_import_paths(&iri, importers, true);
}
}
}
}
}
Commands::Doctor { json } => {
let env = require_ontoenv(env)?;
let problems = env.doctor()?;
if json {
let out: Vec<serde_json::Value> = problems
.into_iter()
.map(|p| serde_json::json!({
"message": p.message,
"locations": p.locations.into_iter().map(|loc| loc.to_string()).collect::<Vec<_>>()
}))
.collect();
println!("{}", serde_json::to_string_pretty(&out)?);
} else if problems.is_empty() {
println!("No issues found.");
} else {
println!("Found {} issues:", problems.len());
for problem in problems {
println!("- {}", problem.message);
for location in problem.locations {
println!(" - {location}");
}
}
}
}
Commands::Namespaces {
ontology,
closure,
json,
} => {
let env = require_ontoenv(env)?;
let namespaces = if let Some(ontology) = ontology {
let iri = NamedNode::new(ontology).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let graphid = env
.resolve(ResolveTarget::Graph(iri.clone()))
.ok_or(anyhow::anyhow!(format!("Ontology {} not found", iri)))?;
env.get_namespaces(&graphid, closure)?
} else {
env.get_all_namespaces()
};
if json {
let map: BTreeMap<&str, &str> = namespaces
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
println!("{}", serde_json::to_string_pretty(&map)?);
} else {
let mut sorted: Vec<(&str, &str)> = namespaces
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
sorted.sort();
for (prefix, namespace) in sorted {
println!("{}: {}", prefix, namespace);
}
}
}
Commands::Config(config_cmd) => {
handle_config_command(config_cmd, cmd.temporary)?;
}
Commands::Reset { .. } => {
}
}
Ok(())
}
fn require_ontoenv(env: Option<OntoEnv>) -> Result<OntoEnv> {
env.ok_or_else(|| {
anyhow::anyhow!("OntoEnv not found. Run `ontoenv init` to create a new OntoEnv or use -t/--temporary to use a temporary environment.")
})
}
fn format_import_paths(
target: &NamedNode,
paths: Vec<Vec<GraphIdentifier>>,
missing: bool,
) -> Vec<Vec<String>> {
let mut unique: BTreeSet<Vec<String>> = BTreeSet::new();
if paths.is_empty() {
if missing {
unique.insert(vec![format!("{} (missing)", target.to_uri_string())]);
}
return unique.into_iter().collect();
}
for path in paths {
let mut entries: Vec<String> = path.into_iter().map(|id| id.to_uri_string()).collect();
if missing {
entries.push(format!("{} (missing)", target.to_uri_string()));
}
unique.insert(entries);
}
unique.into_iter().collect()
}
fn print_import_paths(target: &NamedNode, paths: Vec<Vec<GraphIdentifier>>, missing: bool) {
if paths.is_empty() {
if missing {
println!(
"Ontology {} is missing but no importers reference it.",
target.to_uri_string()
);
} else {
println!("No importers found for {}", target.to_uri_string());
}
return;
}
println!(
"Why {}{}:",
target.to_uri_string(),
if missing { " (missing)" } else { "" }
);
let mut lines: BTreeSet<String> = BTreeSet::new();
for path in paths {
let mut segments: Vec<String> = path.into_iter().map(|id| id.to_uri_string()).collect();
if missing {
segments.push(format!("{} (missing)", target.to_uri_string()));
}
lines.insert(segments.join(" -> "));
}
for line in lines {
println!("{}", line);
}
}