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