1use anyhow::{Error, Result};
2use clap::{Parser, Subcommand};
3use log::info;
4use ontoenv::api::{OntoEnv, ResolveTarget};
5use ontoenv::config::Config;
6use ontoenv::ontology::{GraphIdentifier, OntologyLocation};
7use ontoenv::options::{Overwrite, RefreshStrategy};
8use ontoenv::util::write_dataset_to_file;
9use ontoenv::ToUriString;
10use oxigraph::io::{JsonLdProfileSet, RdfFormat};
11use oxigraph::model::NamedNode;
12use std::collections::{BTreeMap, BTreeSet};
13use std::env::current_dir;
14use std::ffi::OsString;
15use std::path::PathBuf;
16
17#[derive(Debug, Parser)]
18#[command(name = "ontoenv")]
19#[command(about = "Ontology environment manager")]
20#[command(arg_required_else_help = true)]
21struct Cli {
22 #[command(subcommand)]
23 command: Commands,
24 #[clap(long, short, action, default_value = "false", global = true)]
26 verbose: bool,
27 #[clap(long, action, default_value = "false", global = true)]
29 debug: bool,
30 #[clap(long, short, default_value = "default", global = true)]
32 policy: Option<String>,
33 #[clap(long, short, action, global = true)]
35 temporary: bool,
36 #[clap(long, action, global = true)]
38 require_ontology_names: bool,
39 #[clap(long, action, default_value = "false", global = true)]
41 strict: bool,
42 #[clap(long, short, action, default_value = "false", global = true)]
44 offline: bool,
45 #[clap(long, short, num_args = 1.., global = true)]
47 includes: Vec<String>,
48 #[clap(long, short, num_args = 1.., global = true)]
50 excludes: Vec<String>,
51 #[clap(long = "no-search", short = 'n', action, global = true)]
53 no_search: bool,
54}
55
56#[derive(Debug, Subcommand)]
57enum ConfigCommands {
58 Set {
60 key: String,
62 value: String,
64 },
65 Get {
67 key: String,
69 },
70 Unset {
72 key: String,
74 },
75 Add {
77 key: String,
79 value: String,
81 },
82 Remove {
84 key: String,
86 value: String,
88 },
89 List,
91}
92
93#[derive(Debug, Subcommand)]
94enum ListCommands {
95 Locations,
97 Ontologies,
99 Missing,
101}
102
103#[derive(Debug, Subcommand)]
104enum Commands {
105 Init {
107 #[clap(long, default_value = "false")]
109 overwrite: bool,
110 #[clap(last = true)]
112 locations: Option<Vec<PathBuf>>,
113 },
114 Version,
116 Status {
118 #[clap(long, action, default_value = "false")]
120 json: bool,
121 },
122 Update {
124 #[clap(long, short = 'q', action)]
126 quiet: bool,
127 #[clap(long, short = 'a', action)]
129 all: bool,
130 #[clap(long, action, default_value = "false")]
132 json: bool,
133 },
134 Closure {
136 ontology: String,
138 #[clap(long, action, default_value = "false")]
140 no_rewrite_sh_prefixes: bool,
141 #[clap(long, action, default_value = "false")]
143 keep_owl_imports: bool,
144 destination: Option<String>,
146 #[clap(long, default_value = "-1")]
149 recursion_depth: i32,
150 },
151 Get {
153 ontology: String,
155 #[clap(long, short = 'l')]
157 location: Option<String>,
158 #[clap(long)]
160 output: Option<String>,
161 #[clap(long, short = 'f')]
163 format: Option<String>,
164 },
165 Add {
167 location: String,
169 #[clap(long, action)]
171 no_imports: bool,
172 },
173 List {
176 #[command(subcommand)]
177 list_cmd: ListCommands,
178 #[clap(long, action, default_value = "false")]
180 json: bool,
181 },
182 Dump {
187 contains: Option<String>,
190 },
191 DepGraph {
193 roots: Option<Vec<String>>,
195 #[clap(long, short)]
197 output: Option<String>,
198 },
199 Why {
201 ontologies: Vec<String>,
203 #[clap(long, action, default_value = "false")]
205 json: bool,
206 },
207 Doctor {
209 #[clap(long, action, default_value = "false")]
211 json: bool,
212 },
213 Reset {
215 #[clap(long, short, action = clap::ArgAction::SetTrue, default_value = "false")]
216 force: bool,
217 },
218 #[command(subcommand)]
220 Config(ConfigCommands),
221}
222
223impl ToString for Commands {
224 fn to_string(&self) -> String {
225 match self {
226 Commands::Init { .. } => "Init".to_string(),
227 Commands::Version => "Version".to_string(),
228 Commands::Status { .. } => "Status".to_string(),
229 Commands::Update { .. } => "Update".to_string(),
230 Commands::Closure { .. } => "Closure".to_string(),
231 Commands::Get { .. } => "Get".to_string(),
232 Commands::Add { .. } => "Add".to_string(),
233 Commands::List { .. } => "List".to_string(),
234 Commands::Dump { .. } => "Dump".to_string(),
235 Commands::DepGraph { .. } => "DepGraph".to_string(),
236 Commands::Why { .. } => "Why".to_string(),
237 Commands::Doctor { .. } => "Doctor".to_string(),
238 Commands::Reset { .. } => "Reset".to_string(),
239 Commands::Config { .. } => "Config".to_string(),
240 }
241 }
242}
243
244fn handle_config_command(config_cmd: ConfigCommands, temporary: bool) -> Result<()> {
245 if temporary {
246 return Err(anyhow::anyhow!("Cannot manage config in temporary mode."));
247 }
248 let root = ontoenv::api::find_ontoenv_root()
249 .ok_or_else(|| anyhow::anyhow!("Not in an ontoenv. Use `ontoenv init` to create one."))?;
250 let config_path = root.join(".ontoenv").join("ontoenv.json");
251 if !config_path.exists() {
252 return Err(anyhow::anyhow!(
253 "No ontoenv.json found. Use `ontoenv init`."
254 ));
255 }
256
257 match config_cmd {
258 ConfigCommands::List => {
259 let config_str = std::fs::read_to_string(&config_path)?;
260 let config_json: serde_json::Value = serde_json::from_str(&config_str)?;
261 let pretty_json = serde_json::to_string_pretty(&config_json)?;
262 println!("{}", pretty_json);
263 return Ok(());
264 }
265 ConfigCommands::Get { ref key } => {
266 let config_str = std::fs::read_to_string(&config_path)?;
267 let config_json: serde_json::Value = serde_json::from_str(&config_str)?;
268 let object = config_json
269 .as_object()
270 .ok_or_else(|| anyhow::anyhow!("Invalid config format: not a JSON object."))?;
271
272 if let Some(value) = object.get(key) {
273 if let Some(s) = value.as_str() {
274 println!("{}", s);
275 } else if let Some(arr) = value.as_array() {
276 for item in arr {
277 if let Some(s) = item.as_str() {
278 println!("{}", s);
279 } else {
280 println!("{}", item);
281 }
282 }
283 } else {
284 println!("{}", value);
285 }
286 } else {
287 println!("Configuration key '{}' not set.", key);
288 }
289 return Ok(());
290 }
291 _ => {}
292 }
293
294 let config_str = std::fs::read_to_string(&config_path)?;
296 let mut config_json: serde_json::Value = serde_json::from_str(&config_str)?;
297
298 let object = config_json
299 .as_object_mut()
300 .ok_or_else(|| anyhow::anyhow!("Invalid config format: not a JSON object."))?;
301
302 match config_cmd {
303 ConfigCommands::Set { key, value } => {
304 match key.as_str() {
305 "offline" | "strict" | "require_ontology_names" | "no_search" => {
306 let bool_val = value.parse::<bool>().map_err(|_| {
307 anyhow::anyhow!("Invalid boolean value for {}: {}", key, value)
308 })?;
309 object.insert(key.to_string(), serde_json::Value::Bool(bool_val));
310 }
311 "resolution_policy" => {
312 object.insert(key.to_string(), serde_json::Value::String(value.clone()));
313 }
314 "locations" | "includes" | "excludes" => {
315 return Err(anyhow::anyhow!(
316 "Use `ontoenv config add/remove {} <value>` to modify list values.",
317 key
318 ));
319 }
320 _ => {
321 return Err(anyhow::anyhow!(
322 "Setting configuration for '{}' is not supported.",
323 key
324 ));
325 }
326 }
327 println!("Set {} to {}", key, value);
328 }
329 ConfigCommands::Unset { key } => {
330 if object.remove(&key).is_some() {
331 println!("Unset '{}'.", key);
332 } else {
333 return Err(anyhow::anyhow!("Configuration key '{}' not set.", key));
334 }
335 }
336 ConfigCommands::Add { key, value } => {
337 match key.as_str() {
338 "locations" | "includes" | "excludes" => {
339 let entry = object
340 .entry(key.clone())
341 .or_insert_with(|| serde_json::Value::Array(vec![]));
342 if let Some(arr) = entry.as_array_mut() {
343 let new_val = serde_json::Value::String(value.clone());
344 if !arr.contains(&new_val) {
345 arr.push(new_val);
346 } else {
347 println!("Value '{}' already exists in {}.", value, key);
348 return Ok(());
349 }
350 }
351 }
352 _ => {
353 return Err(anyhow::anyhow!(
354 "Cannot add to configuration key '{}'. It is not a list.",
355 key
356 ));
357 }
358 }
359 println!("Added '{}' to {}", value, key);
360 }
361 ConfigCommands::Remove { key, value } => {
362 match key.as_str() {
363 "locations" | "includes" | "excludes" => {
364 if let Some(entry) = object.get_mut(&key) {
365 if let Some(arr) = entry.as_array_mut() {
366 let val_to_remove = serde_json::Value::String(value.clone());
367 if let Some(pos) = arr.iter().position(|x| *x == val_to_remove) {
368 arr.remove(pos);
369 } else {
370 return Err(anyhow::anyhow!(
371 "Value '{}' not found in {}",
372 value,
373 key
374 ));
375 }
376 }
377 } else {
378 return Err(anyhow::anyhow!("Configuration key '{}' not set.", key));
379 }
380 }
381 _ => {
382 return Err(anyhow::anyhow!(
383 "Cannot remove from configuration key '{}'. It is not a list.",
384 key
385 ));
386 }
387 }
388 println!("Removed '{}' from {}", value, key);
389 }
390 _ => unreachable!(), }
392
393 let new_config_str = serde_json::to_string_pretty(&config_json)?;
394 std::fs::write(config_path, new_config_str)?;
395
396 Ok(())
397}
398
399pub fn run() -> Result<()> {
400 ontoenv::api::init_logging();
401 let cmd = Cli::parse();
402 execute(cmd)
403}
404
405pub fn run_from_args<I, T>(args: I) -> Result<()>
406where
407 I: IntoIterator<Item = T>,
408 T: Into<OsString> + Clone,
409{
410 ontoenv::api::init_logging();
411 let cmd = Cli::try_parse_from(args).map_err(Error::from)?;
412 execute(cmd)
413}
414
415fn execute(cmd: Cli) -> Result<()> {
416 if cmd.debug {
419 std::env::set_var("RUST_LOG", "debug");
420 } else if cmd.verbose {
421 std::env::set_var("RUST_LOG", "info");
422 } else if std::env::var("RUST_LOG").is_err() {
423 std::env::set_var("RUST_LOG", "warn");
425 }
426 let _ = env_logger::try_init();
427
428 let policy = cmd.policy.unwrap_or_else(|| "default".to_string());
429
430 let mut builder = Config::builder()
431 .root(current_dir()?)
432 .require_ontology_names(cmd.require_ontology_names)
433 .strict(cmd.strict)
434 .offline(cmd.offline)
435 .resolution_policy(policy)
436 .temporary(cmd.temporary)
437 .no_search(cmd.no_search);
438
439 if let Commands::Init {
441 locations: Some(locs),
442 ..
443 } = &cmd.command
444 {
445 builder = builder.locations(locs.clone());
446 }
447 if !cmd.includes.is_empty() {
449 builder = builder.includes(&cmd.includes);
450 }
451 if !cmd.excludes.is_empty() {
452 builder = builder.excludes(&cmd.excludes);
453 }
454
455 let config: Config = builder.build()?;
456
457 if cmd.verbose || cmd.debug {
458 config.print();
459 }
460
461 if let Commands::Reset { force } = &cmd.command {
462 if let Some(root) = ontoenv::api::find_ontoenv_root() {
463 let path = root.join(".ontoenv");
464 println!("Removing .ontoenv directory at {}...", path.display());
465 if !*force {
466 let mut input = String::new();
468 println!("Are you sure you want to delete the .ontoenv directory? [y/N] ");
469 std::io::stdin()
470 .read_line(&mut input)
471 .expect("Failed to read line");
472 let input = input.trim();
473 if input != "y" && input != "Y" {
474 println!("Aborting...");
475 return Ok(());
476 }
477 }
478 OntoEnv::reset()?;
479 println!(".ontoenv directory removed.");
480 } else {
481 println!("No .ontoenv directory found. Nothing to do.");
482 }
483 return Ok(());
484 }
485
486 let env_dir_var = std::env::var("ONTOENV_DIR").ok().map(PathBuf::from);
488 let discovered_root = if let Some(dir) = env_dir_var.clone() {
489 if dir.file_name().map(|n| n == ".ontoenv").unwrap_or(false) {
491 dir.parent().map(|p| p.to_path_buf())
492 } else {
493 Some(dir)
494 }
495 } else {
496 ontoenv::api::find_ontoenv_root()
497 };
498 let ontoenv_exists = discovered_root
499 .as_ref()
500 .map(|root| root.join(".ontoenv").join("ontoenv.json").exists())
501 .unwrap_or(false);
502 info!("OntoEnv exists: {ontoenv_exists}");
503
504 let needs_rw = matches!(cmd.command, Commands::Add { .. } | Commands::Update { .. });
509
510 let env: Option<OntoEnv> = if cmd.temporary {
511 let e = OntoEnv::init(config.clone(), false)?;
513 Some(e)
514 } else if cmd.command.to_string() != "Init" && ontoenv_exists {
515 Some(OntoEnv::load_from_directory(
518 discovered_root.unwrap(),
519 !needs_rw,
520 )?)
521 } else {
522 None
523 };
524 info!("OntoEnv loaded: {}", env.is_some());
525
526 match cmd.command {
527 Commands::Init { overwrite, .. } => {
528 if cmd.temporary {
530 return Err(anyhow::anyhow!(
531 "Cannot initialize in temporary mode. Run `ontoenv init` without --temporary."
532 ));
533 }
534
535 let root = current_dir()?;
536 if root.join(".ontoenv").exists() && !overwrite {
537 println!(
538 "An ontology environment already exists in: {}",
539 root.display()
540 );
541 println!("Use --overwrite to re-initialize or `ontoenv update` to update.");
542
543 let env = OntoEnv::load_from_directory(root, false)?;
544 let status = env.status()?;
545 println!("\nCurrent status:");
546 println!("{status}");
547 return Ok(());
548 }
549
550 let _ = OntoEnv::init(config, overwrite)?;
553 }
554 Commands::Get {
555 ontology,
556 location,
557 output,
558 format,
559 } => {
560 let env = require_ontoenv(env)?;
561
562 let graph = if let Some(loc) = location {
564 let oloc = if loc.starts_with("http://") || loc.starts_with("https://") {
565 OntologyLocation::Url(loc)
566 } else {
567 ontoenv::ontology::OntologyLocation::from_str(&loc)
569 .unwrap_or_else(|_| OntologyLocation::File(PathBuf::from(loc)))
570 };
571 oloc.graph()?
573 } else {
574 let iri = NamedNode::new(ontology).map_err(|e| anyhow::anyhow!(e.to_string()))?;
575 let graphid = env
576 .resolve(ResolveTarget::Graph(iri))
577 .ok_or(anyhow::anyhow!("Ontology not found"))?;
578 env.get_graph(&graphid)?
579 };
580
581 let fmt = match format
582 .as_deref()
583 .unwrap_or("turtle")
584 .to_ascii_lowercase()
585 .as_str()
586 {
587 "turtle" | "ttl" => RdfFormat::Turtle,
588 "ntriples" | "nt" => RdfFormat::NTriples,
589 "rdfxml" | "xml" => RdfFormat::RdfXml,
590 "jsonld" | "json-ld" => RdfFormat::JsonLd {
591 profile: JsonLdProfileSet::default(),
592 },
593 other => {
594 return Err(anyhow::anyhow!(
595 "Unsupported format '{}'. Use one of: turtle, ntriples, rdfxml, jsonld",
596 other
597 ))
598 }
599 };
600
601 if let Some(path) = output {
602 let mut file = std::fs::File::create(path)?;
603 let mut serializer =
604 oxigraph::io::RdfSerializer::from_format(fmt).for_writer(&mut file);
605 for t in graph.iter() {
606 serializer.serialize_triple(t)?;
607 }
608 serializer.finish()?;
609 } else {
610 let stdout = std::io::stdout();
611 let mut handle = stdout.lock();
612 let mut serializer =
613 oxigraph::io::RdfSerializer::from_format(fmt).for_writer(&mut handle);
614 for t in graph.iter() {
615 serializer.serialize_triple(t)?;
616 }
617 serializer.finish()?;
618 }
619 }
620 Commands::Version => {
621 println!(
622 "ontoenv {} @ {}",
623 env!("CARGO_PKG_VERSION"),
624 env!("GIT_HASH")
625 );
626 }
627 Commands::Status { json } => {
628 let env = require_ontoenv(env)?;
629 if json {
630 let ontoenv_dir = current_dir()?.join(".ontoenv");
632 let last_updated = if ontoenv_dir.exists() {
633 Some(std::fs::metadata(&ontoenv_dir)?.modified()?)
634 as Option<std::time::SystemTime>
635 } else {
636 None
637 };
638 let size: u64 = if ontoenv_dir.exists() {
639 walkdir::WalkDir::new(&ontoenv_dir)
640 .into_iter()
641 .filter_map(Result::ok)
642 .filter(|e| e.file_type().is_file())
643 .filter_map(|e| e.metadata().ok())
644 .map(|m| m.len())
645 .sum()
646 } else {
647 0
648 };
649 let missing: Vec<String> = env
650 .missing_imports()
651 .into_iter()
652 .map(|n| n.to_uri_string())
653 .collect();
654 let last_str =
655 last_updated.map(|t| chrono::DateTime::<chrono::Utc>::from(t).to_rfc3339());
656 let obj = serde_json::json!({
657 "exists": true,
658 "num_ontologies": env.ontologies().len(),
659 "last_updated": last_str,
660 "store_size_bytes": size,
661 "missing_imports": missing,
662 });
663 println!("{}", serde_json::to_string_pretty(&obj)?);
664 } else {
665 let status = env.status()?;
666 println!("{status}");
667 }
668 }
669 Commands::Update { quiet, all, json } => {
670 let mut env = require_ontoenv(env)?;
671 let updated = env.update_all(all)?;
672 if json {
673 let arr: Vec<String> = updated.iter().map(|id| id.to_uri_string()).collect();
674 println!("{}", serde_json::to_string_pretty(&arr)?);
675 } else if !quiet {
676 for id in updated {
677 if let Some(ont) = env.ontologies().get(&id) {
678 let name = ont.name().to_string();
679 let loc = ont
680 .location()
681 .map(|l| l.to_string())
682 .unwrap_or_else(|| "N/A".to_string());
683 println!("{} @ {}", name, loc);
684 }
685 }
686 }
687 env.save_to_directory()?;
688 }
689 Commands::Closure {
690 ontology,
691 no_rewrite_sh_prefixes,
692 keep_owl_imports,
693 destination,
694 recursion_depth,
695 } => {
696 let iri = NamedNode::new(ontology).map_err(|e| anyhow::anyhow!(e.to_string()))?;
698 let env = require_ontoenv(env)?;
699 let graphid = env
700 .resolve(ResolveTarget::Graph(iri.clone()))
701 .ok_or(anyhow::anyhow!(format!("Ontology {} not found", iri)))?;
702 let closure = env.get_closure(&graphid, recursion_depth)?;
703 let rewrite = !no_rewrite_sh_prefixes;
705 let remove = !keep_owl_imports;
706 let union = env.get_union_graph(&closure, Some(rewrite), Some(remove))?;
707 if let Some(failed_imports) = union.failed_imports {
708 for imp in failed_imports {
709 eprintln!("{imp}");
710 }
711 }
712 let destination = destination.unwrap_or_else(|| "output.ttl".to_string());
714 write_dataset_to_file(&union.dataset, &destination)?;
715 }
716 Commands::Add {
717 location,
718 no_imports,
719 } => {
720 let location = if location.starts_with("http") {
721 OntologyLocation::Url(location)
722 } else {
723 OntologyLocation::File(PathBuf::from(location))
724 };
725 let mut env = require_ontoenv(env)?;
726 if no_imports {
727 let _ =
728 env.add_no_imports(location, Overwrite::Allow, RefreshStrategy::UseCache)?;
729 } else {
730 let _ = env.add(location, Overwrite::Allow, RefreshStrategy::UseCache)?;
731 }
732 }
733 Commands::List { list_cmd, json } => {
734 let env = require_ontoenv(env)?;
735 match list_cmd {
736 ListCommands::Locations => {
737 let mut locations = env.find_files()?;
738 locations.sort_by(|a, b| a.as_str().cmp(b.as_str()));
739 if json {
740 println!("{}", serde_json::to_string_pretty(&locations)?);
741 } else {
742 for loc in locations {
743 println!("{}", loc);
744 }
745 }
746 }
747 ListCommands::Ontologies => {
748 let mut ontologies: Vec<&GraphIdentifier> = env.ontologies().keys().collect();
750 ontologies.sort_by(|a, b| a.name().cmp(&b.name()));
751 ontologies.dedup_by(|a, b| a.name() == b.name());
752 if json {
753 let out: Vec<String> =
754 ontologies.into_iter().map(|o| o.to_uri_string()).collect();
755 println!("{}", serde_json::to_string_pretty(&out)?);
756 } else {
757 for ont in ontologies {
758 println!("{}", ont.to_uri_string());
759 }
760 }
761 }
762 ListCommands::Missing => {
763 let mut missing_imports = env.missing_imports();
764 missing_imports.sort();
765 if json {
766 let out: Vec<String> = missing_imports
767 .into_iter()
768 .map(|n| n.to_uri_string())
769 .collect();
770 println!("{}", serde_json::to_string_pretty(&out)?);
771 } else {
772 for import in missing_imports {
773 println!("{}", import.to_uri_string());
774 }
775 }
776 }
777 }
778 }
779 Commands::Dump { contains } => {
780 let env = require_ontoenv(env)?;
781 env.dump(contains.as_deref());
782 }
783 Commands::DepGraph { roots, output } => {
784 let env = require_ontoenv(env)?;
785 let dot = if let Some(roots) = roots {
786 let roots: Vec<GraphIdentifier> = roots
787 .iter()
788 .map(|iri| {
789 env.resolve(ResolveTarget::Graph(NamedNode::new(iri).unwrap()))
790 .unwrap()
791 .clone()
792 })
793 .collect();
794 env.rooted_dep_graph_to_dot(roots)?
795 } else {
796 env.dep_graph_to_dot()?
797 };
798 let dot_path = current_dir()?.join("dep_graph.dot");
800 std::fs::write(&dot_path, dot)?;
801 let output_path = output.unwrap_or_else(|| "dep_graph.pdf".to_string());
802 let output = std::process::Command::new("dot")
803 .args(["-Tpdf", dot_path.to_str().unwrap(), "-o", &output_path])
804 .output()?;
805 if !output.status.success() {
806 return Err(anyhow::anyhow!(
807 "Failed to generate PDF: {}",
808 String::from_utf8_lossy(&output.stderr)
809 ));
810 }
811 }
812 Commands::Why { ontologies, json } => {
813 let env = require_ontoenv(env)?;
814 if json {
815 let mut all: BTreeMap<String, Vec<Vec<String>>> = BTreeMap::new();
816 for ont in ontologies {
817 let iri = NamedNode::new(ont).map_err(|e| anyhow::anyhow!(e.to_string()))?;
818 let (paths, missing) = match env.explain_import(&iri)? {
819 ontoenv::api::ImportPaths::Present(paths) => (paths, false),
820 ontoenv::api::ImportPaths::Missing { importers } => (importers, true),
821 };
822 let formatted = format_import_paths(&iri, paths, missing);
823 all.insert(iri.to_uri_string(), formatted);
824 }
825 println!("{}", serde_json::to_string_pretty(&all)?);
826 } else {
827 for ont in ontologies {
828 let iri = NamedNode::new(ont).map_err(|e| anyhow::anyhow!(e.to_string()))?;
829 match env.explain_import(&iri)? {
830 ontoenv::api::ImportPaths::Present(paths) => {
831 print_import_paths(&iri, paths, false);
832 }
833 ontoenv::api::ImportPaths::Missing { importers } => {
834 print_import_paths(&iri, importers, true);
835 }
836 }
837 }
838 }
839 }
840 Commands::Doctor { json } => {
841 let env = require_ontoenv(env)?;
842 let problems = env.doctor()?;
843 if json {
844 let out: Vec<serde_json::Value> = problems
845 .into_iter()
846 .map(|p| serde_json::json!({
847 "message": p.message,
848 "locations": p.locations.into_iter().map(|loc| loc.to_string()).collect::<Vec<_>>()
849 }))
850 .collect();
851 println!("{}", serde_json::to_string_pretty(&out)?);
852 } else if problems.is_empty() {
853 println!("No issues found.");
854 } else {
855 println!("Found {} issues:", problems.len());
856 for problem in problems {
857 println!("- {}", problem.message);
858 for location in problem.locations {
859 println!(" - {location}");
860 }
861 }
862 }
863 }
864 Commands::Config(config_cmd) => {
865 handle_config_command(config_cmd, cmd.temporary)?;
866 }
867 Commands::Reset { .. } => {
868 }
870 }
871
872 Ok(())
873}
874
875fn require_ontoenv(env: Option<OntoEnv>) -> Result<OntoEnv> {
876 env.ok_or_else(|| {
877 anyhow::anyhow!("OntoEnv not found. Run `ontoenv init` to create a new OntoEnv or use -t/--temporary to use a temporary environment.")
878 })
879}
880
881fn format_import_paths(
882 target: &NamedNode,
883 paths: Vec<Vec<GraphIdentifier>>,
884 missing: bool,
885) -> Vec<Vec<String>> {
886 let mut unique: BTreeSet<Vec<String>> = BTreeSet::new();
887 if paths.is_empty() {
888 if missing {
889 unique.insert(vec![format!("{} (missing)", target.to_uri_string())]);
890 }
891 return unique.into_iter().collect();
892 }
893 for path in paths {
894 let mut entries: Vec<String> = path.into_iter().map(|id| id.to_uri_string()).collect();
895 if missing {
896 entries.push(format!("{} (missing)", target.to_uri_string()));
897 }
898 unique.insert(entries);
899 }
900 unique.into_iter().collect()
901}
902
903fn print_import_paths(target: &NamedNode, paths: Vec<Vec<GraphIdentifier>>, missing: bool) {
904 if paths.is_empty() {
905 if missing {
906 println!(
907 "Ontology {} is missing but no importers reference it.",
908 target.to_uri_string()
909 );
910 } else {
911 println!("No importers found for {}", target.to_uri_string());
912 }
913 return;
914 }
915
916 println!(
917 "Why {}{}:",
918 target.to_uri_string(),
919 if missing { " (missing)" } else { "" }
920 );
921
922 let mut lines: BTreeSet<String> = BTreeSet::new();
923 for path in paths {
924 let mut segments: Vec<String> = path.into_iter().map(|id| id.to_uri_string()).collect();
925 if missing {
926 segments.push(format!("{} (missing)", target.to_uri_string()));
927 }
928 lines.insert(segments.join(" -> "));
929 }
930
931 for line in lines {
932 println!("{}", line);
933 }
934}