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 = "remote-cache-ttl-secs", value_parser, global = true)]
60 remote_cache_ttl_secs: Option<u64>,
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(value_name = "LOCATION", num_args = 0.., value_parser)]
119 locations: 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" => {
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 "remote_cache_ttl_secs" => {
322 let ttl = value
323 .parse::<u64>()
324 .map_err(|_| anyhow::anyhow!("Invalid u64 value for {}: {}", key, value))?;
325 object.insert(key.to_string(), serde_json::Value::Number(ttl.into()));
326 }
327 "locations" | "includes" | "excludes" => {
328 return Err(anyhow::anyhow!(
329 "Use `ontoenv config add/remove {} <value>` to modify list values.",
330 key
331 ));
332 }
333 _ => {
334 return Err(anyhow::anyhow!(
335 "Setting configuration for '{}' is not supported.",
336 key
337 ));
338 }
339 }
340 println!("Set {} to {}", key, value);
341 }
342 ConfigCommands::Unset { key } => {
343 if object.remove(&key).is_some() {
344 println!("Unset '{}'.", key);
345 } else {
346 return Err(anyhow::anyhow!("Configuration key '{}' not set.", key));
347 }
348 }
349 ConfigCommands::Add { key, value } => {
350 match key.as_str() {
351 "locations" | "includes" | "excludes" => {
352 let entry = object
353 .entry(key.clone())
354 .or_insert_with(|| serde_json::Value::Array(vec![]));
355 if let Some(arr) = entry.as_array_mut() {
356 let new_val = serde_json::Value::String(value.clone());
357 if !arr.contains(&new_val) {
358 arr.push(new_val);
359 } else {
360 println!("Value '{}' already exists in {}.", value, key);
361 return Ok(());
362 }
363 }
364 }
365 _ => {
366 return Err(anyhow::anyhow!(
367 "Cannot add to configuration key '{}'. It is not a list.",
368 key
369 ));
370 }
371 }
372 println!("Added '{}' to {}", value, key);
373 }
374 ConfigCommands::Remove { key, value } => {
375 match key.as_str() {
376 "locations" | "includes" | "excludes" => {
377 if let Some(entry) = object.get_mut(&key) {
378 if let Some(arr) = entry.as_array_mut() {
379 let val_to_remove = serde_json::Value::String(value.clone());
380 if let Some(pos) = arr.iter().position(|x| *x == val_to_remove) {
381 arr.remove(pos);
382 } else {
383 return Err(anyhow::anyhow!(
384 "Value '{}' not found in {}",
385 value,
386 key
387 ));
388 }
389 }
390 } else {
391 return Err(anyhow::anyhow!("Configuration key '{}' not set.", key));
392 }
393 }
394 _ => {
395 return Err(anyhow::anyhow!(
396 "Cannot remove from configuration key '{}'. It is not a list.",
397 key
398 ));
399 }
400 }
401 println!("Removed '{}' from {}", value, key);
402 }
403 _ => unreachable!(), }
405
406 let new_config_str = serde_json::to_string_pretty(&config_json)?;
407 std::fs::write(config_path, new_config_str)?;
408
409 Ok(())
410}
411
412pub fn run() -> Result<()> {
413 ontoenv::api::init_logging();
414 let cmd = Cli::parse();
415 execute(cmd)
416}
417
418pub fn run_from_args<I, T>(args: I) -> Result<()>
419where
420 I: IntoIterator<Item = T>,
421 T: Into<OsString> + Clone,
422{
423 ontoenv::api::init_logging();
424 let cmd = Cli::try_parse_from(args).map_err(Error::from)?;
425 execute(cmd)
426}
427
428fn execute(cmd: Cli) -> Result<()> {
429 if cmd.debug {
432 std::env::set_var("RUST_LOG", "debug");
433 } else if cmd.verbose {
434 std::env::set_var("RUST_LOG", "info");
435 } else if std::env::var("RUST_LOG").is_err() {
436 std::env::set_var("RUST_LOG", "warn");
438 }
439 let _ = env_logger::try_init();
440
441 let policy = cmd.policy.unwrap_or_else(|| "default".to_string());
442
443 let cwd = current_dir()?;
444 let mut builder = Config::builder()
445 .root(cwd.clone())
446 .require_ontology_names(cmd.require_ontology_names)
447 .strict(cmd.strict)
448 .offline(cmd.offline)
449 .resolution_policy(policy)
450 .temporary(cmd.temporary);
451
452 if let Commands::Init { locations, .. } = &cmd.command {
454 builder = builder.locations(locations.clone());
455 }
456 if !cmd.includes.is_empty() {
458 builder = builder.includes(&cmd.includes);
459 }
460 if !cmd.excludes.is_empty() {
461 builder = builder.excludes(&cmd.excludes);
462 }
463 if !cmd.include_ontologies.is_empty() {
464 builder = builder.include_ontologies(&cmd.include_ontologies);
465 }
466 if !cmd.exclude_ontologies.is_empty() {
467 builder = builder.exclude_ontologies(&cmd.exclude_ontologies);
468 }
469 if let Some(ttl) = cmd.remote_cache_ttl_secs {
470 builder = builder.remote_cache_ttl_secs(ttl);
471 }
472
473 let config: Config = builder.build()?;
474
475 if cmd.verbose || cmd.debug {
476 config.print();
477 }
478
479 if let Commands::Reset { force } = &cmd.command {
480 if let Some(root) = ontoenv::api::find_ontoenv_root() {
481 let path = root.join(".ontoenv");
482 println!("Removing .ontoenv directory at {}...", path.display());
483 if !*force {
484 let mut input = String::new();
486 println!("Are you sure you want to delete the .ontoenv directory? [y/N] ");
487 std::io::stdin()
488 .read_line(&mut input)
489 .expect("Failed to read line");
490 let input = input.trim();
491 if input != "y" && input != "Y" {
492 println!("Aborting...");
493 return Ok(());
494 }
495 }
496 OntoEnv::reset()?;
497 println!(".ontoenv directory removed.");
498 } else {
499 println!("No .ontoenv directory found. Nothing to do.");
500 }
501 return Ok(());
502 }
503
504 let env_dir_var = std::env::var("ONTOENV_DIR").ok().map(PathBuf::from);
506 let discovered_root = if let Some(dir) = env_dir_var.clone() {
507 if dir.file_name().map(|n| n == ".ontoenv").unwrap_or(false) {
509 dir.parent().map(|p| p.to_path_buf())
510 } else {
511 Some(dir)
512 }
513 } else {
514 ontoenv::api::find_ontoenv_root()
515 };
516 let ontoenv_exists = discovered_root
517 .as_ref()
518 .map(|root| root.join(".ontoenv").join("ontoenv.json").exists())
519 .unwrap_or(false);
520 info!("OntoEnv exists: {ontoenv_exists}");
521
522 let needs_rw = matches!(cmd.command, Commands::Add { .. } | Commands::Update { .. });
527
528 let env: Option<OntoEnv> = if cmd.temporary {
529 let e = OntoEnv::init(config.clone(), false)?;
531 Some(e)
532 } else if cmd.command.to_string() != "Init" && ontoenv_exists {
533 Some(OntoEnv::load_from_directory(
536 discovered_root.unwrap(),
537 !needs_rw,
538 )?)
539 } else {
540 None
541 };
542 info!("OntoEnv loaded: {}", env.is_some());
543
544 match cmd.command {
545 Commands::Init { overwrite, .. } => {
546 if cmd.temporary {
548 return Err(anyhow::anyhow!(
549 "Cannot initialize in temporary mode. Run `ontoenv init` without --temporary."
550 ));
551 }
552
553 let root = current_dir()?;
554 if root.join(".ontoenv").exists() && !overwrite {
555 println!(
556 "An ontology environment already exists in: {}",
557 root.display()
558 );
559 println!("Use --overwrite to re-initialize or `ontoenv update` to update.");
560
561 let env = OntoEnv::load_from_directory(root, false)?;
562 let status = env.status()?;
563 println!("\nCurrent status:");
564 println!("{status}");
565 return Ok(());
566 }
567
568 let _ = OntoEnv::init(config, overwrite)?;
571 }
572 Commands::Get {
573 ontology,
574 location,
575 output,
576 format,
577 } => {
578 let env = require_ontoenv(env)?;
579
580 let graph = if let Some(loc) = location {
582 let oloc = if loc.starts_with("http://") || loc.starts_with("https://") {
583 OntologyLocation::Url(loc)
584 } else {
585 ontoenv::ontology::OntologyLocation::from_str(&loc)
587 .unwrap_or_else(|_| OntologyLocation::File(PathBuf::from(loc)))
588 };
589 oloc.graph()?
591 } else {
592 let iri = NamedNode::new(ontology).map_err(|e| anyhow::anyhow!(e.to_string()))?;
593 let graphid = env
594 .resolve(ResolveTarget::Graph(iri))
595 .ok_or(anyhow::anyhow!("Ontology not found"))?;
596 env.get_graph(&graphid)?
597 };
598
599 let fmt = match format
600 .as_deref()
601 .unwrap_or("turtle")
602 .to_ascii_lowercase()
603 .as_str()
604 {
605 "turtle" | "ttl" => RdfFormat::Turtle,
606 "ntriples" | "nt" => RdfFormat::NTriples,
607 "rdfxml" | "xml" => RdfFormat::RdfXml,
608 "jsonld" | "json-ld" => RdfFormat::JsonLd {
609 profile: JsonLdProfileSet::default(),
610 },
611 other => {
612 return Err(anyhow::anyhow!(
613 "Unsupported format '{}'. Use one of: turtle, ntriples, rdfxml, jsonld",
614 other
615 ))
616 }
617 };
618
619 if let Some(path) = output {
620 let mut file = std::fs::File::create(path)?;
621 let mut serializer =
622 oxigraph::io::RdfSerializer::from_format(fmt).for_writer(&mut file);
623 for t in graph.iter() {
624 serializer.serialize_triple(t)?;
625 }
626 serializer.finish()?;
627 } else {
628 let stdout = std::io::stdout();
629 let mut handle = stdout.lock();
630 let mut serializer =
631 oxigraph::io::RdfSerializer::from_format(fmt).for_writer(&mut handle);
632 for t in graph.iter() {
633 serializer.serialize_triple(t)?;
634 }
635 serializer.finish()?;
636 }
637 }
638 Commands::Version => {
639 println!(
640 "ontoenv {} @ {}",
641 env!("CARGO_PKG_VERSION"),
642 env!("GIT_HASH")
643 );
644 }
645 Commands::Status { json } => {
646 let env = require_ontoenv(env)?;
647 if json {
648 let ontoenv_dir = current_dir()?.join(".ontoenv");
650 let last_updated = if ontoenv_dir.exists() {
651 Some(std::fs::metadata(&ontoenv_dir)?.modified()?)
652 as Option<std::time::SystemTime>
653 } else {
654 None
655 };
656 let size: u64 = if ontoenv_dir.exists() {
657 walkdir::WalkDir::new(&ontoenv_dir)
658 .into_iter()
659 .filter_map(Result::ok)
660 .filter(|e| e.file_type().is_file())
661 .filter_map(|e| e.metadata().ok())
662 .map(|m| m.len())
663 .sum()
664 } else {
665 0
666 };
667 let missing: Vec<String> = env
668 .missing_imports()
669 .into_iter()
670 .map(|n| n.to_uri_string())
671 .collect();
672 let last_str =
673 last_updated.map(|t| chrono::DateTime::<chrono::Utc>::from(t).to_rfc3339());
674 let obj = serde_json::json!({
675 "exists": true,
676 "num_ontologies": env.ontologies().len(),
677 "last_updated": last_str,
678 "store_size_bytes": size,
679 "missing_imports": missing,
680 });
681 println!("{}", serde_json::to_string_pretty(&obj)?);
682 } else {
683 let status = env.status()?;
684 println!("{status}");
685 }
686 }
687 Commands::Update { quiet, all, json } => {
688 let mut env = require_ontoenv(env)?;
689 let updated = env.update_all(all)?;
690 if json {
691 let arr: Vec<String> = updated.iter().map(|id| id.to_uri_string()).collect();
692 println!("{}", serde_json::to_string_pretty(&arr)?);
693 } else if !quiet {
694 for id in updated {
695 if let Some(ont) = env.ontologies().get(&id) {
696 let name = ont.name().to_string();
697 let loc = ont
698 .location()
699 .map(|l| l.to_string())
700 .unwrap_or_else(|| "N/A".to_string());
701 println!("{} @ {}", name, loc);
702 }
703 }
704 }
705 env.save_to_directory()?;
706 }
707 Commands::Closure {
708 ontology,
709 no_rewrite_sh_prefixes,
710 keep_owl_imports,
711 destination,
712 recursion_depth,
713 } => {
714 let iri = NamedNode::new(ontology).map_err(|e| anyhow::anyhow!(e.to_string()))?;
716 let env = require_ontoenv(env)?;
717 let graphid = env
718 .resolve(ResolveTarget::Graph(iri.clone()))
719 .ok_or(anyhow::anyhow!(format!("Ontology {} not found", iri)))?;
720 let closure = env.get_closure(&graphid, recursion_depth)?;
721 let rewrite = !no_rewrite_sh_prefixes;
723 let remove = !keep_owl_imports;
724 let union = env.get_union_graph(&closure, Some(rewrite), Some(remove))?;
725 if let Some(failed_imports) = union.failed_imports {
726 for imp in failed_imports {
727 eprintln!("{imp}");
728 }
729 }
730 let destination = destination.unwrap_or_else(|| "output.ttl".to_string());
732 write_dataset_to_file(&union.dataset, &destination)?;
733 }
734 Commands::Add {
735 location,
736 no_imports,
737 } => {
738 let location = if location.starts_with("http") {
739 OntologyLocation::Url(location)
740 } else {
741 OntologyLocation::File(PathBuf::from(location))
742 };
743 let mut env = require_ontoenv(env)?;
744 if no_imports {
745 let _ =
746 env.add_no_imports(location, Overwrite::Allow, RefreshStrategy::UseCache)?;
747 } else {
748 let _ = env.add(location, Overwrite::Allow, RefreshStrategy::UseCache)?;
749 }
750 }
751 Commands::List { list_cmd, json } => {
752 let env = require_ontoenv(env)?;
753 match list_cmd {
754 ListCommands::Locations => {
755 let mut locations = env.find_files()?;
756 locations.sort_by(|a, b| a.as_str().cmp(b.as_str()));
757 if json {
758 println!("{}", serde_json::to_string_pretty(&locations)?);
759 } else {
760 for loc in locations {
761 println!("{}", loc);
762 }
763 }
764 }
765 ListCommands::Ontologies => {
766 let mut ontologies: Vec<&GraphIdentifier> = env.ontologies().keys().collect();
768 ontologies.sort_by(|a, b| a.name().cmp(&b.name()));
769 ontologies.dedup_by(|a, b| a.name() == b.name());
770 if json {
771 let out: Vec<String> =
772 ontologies.into_iter().map(|o| o.to_uri_string()).collect();
773 println!("{}", serde_json::to_string_pretty(&out)?);
774 } else {
775 for ont in ontologies {
776 println!("{}", ont.to_uri_string());
777 }
778 }
779 }
780 ListCommands::Missing => {
781 let mut missing_imports = env.missing_imports();
782 missing_imports.sort();
783 if json {
784 let out: Vec<String> = missing_imports
785 .into_iter()
786 .map(|n| n.to_uri_string())
787 .collect();
788 println!("{}", serde_json::to_string_pretty(&out)?);
789 } else {
790 for import in missing_imports {
791 println!("{}", import.to_uri_string());
792 }
793 }
794 }
795 }
796 }
797 Commands::Dump { contains } => {
798 let env = require_ontoenv(env)?;
799 env.dump(contains.as_deref());
800 }
801 Commands::DepGraph { roots, output } => {
802 let env = require_ontoenv(env)?;
803 let dot = if let Some(roots) = roots {
804 let roots: Vec<GraphIdentifier> = roots
805 .iter()
806 .map(|iri| {
807 env.resolve(ResolveTarget::Graph(NamedNode::new(iri).unwrap()))
808 .unwrap()
809 .clone()
810 })
811 .collect();
812 env.rooted_dep_graph_to_dot(roots)?
813 } else {
814 env.dep_graph_to_dot()?
815 };
816 let dot_path = current_dir()?.join("dep_graph.dot");
818 std::fs::write(&dot_path, dot)?;
819 let output_path = output.unwrap_or_else(|| "dep_graph.pdf".to_string());
820 let output = std::process::Command::new("dot")
821 .args(["-Tpdf", dot_path.to_str().unwrap(), "-o", &output_path])
822 .output()?;
823 if !output.status.success() {
824 return Err(anyhow::anyhow!(
825 "Failed to generate PDF: {}",
826 String::from_utf8_lossy(&output.stderr)
827 ));
828 }
829 }
830 Commands::Why { ontologies, json } => {
831 let env = require_ontoenv(env)?;
832 if json {
833 let mut all: BTreeMap<String, Vec<Vec<String>>> = BTreeMap::new();
834 for ont in ontologies {
835 let iri = NamedNode::new(ont).map_err(|e| anyhow::anyhow!(e.to_string()))?;
836 let (paths, missing) = match env.explain_import(&iri)? {
837 ontoenv::api::ImportPaths::Present(paths) => (paths, false),
838 ontoenv::api::ImportPaths::Missing { importers } => (importers, true),
839 };
840 let formatted = format_import_paths(&iri, paths, missing);
841 all.insert(iri.to_uri_string(), formatted);
842 }
843 println!("{}", serde_json::to_string_pretty(&all)?);
844 } else {
845 for ont in ontologies {
846 let iri = NamedNode::new(ont).map_err(|e| anyhow::anyhow!(e.to_string()))?;
847 match env.explain_import(&iri)? {
848 ontoenv::api::ImportPaths::Present(paths) => {
849 print_import_paths(&iri, paths, false);
850 }
851 ontoenv::api::ImportPaths::Missing { importers } => {
852 print_import_paths(&iri, importers, true);
853 }
854 }
855 }
856 }
857 }
858 Commands::Doctor { json } => {
859 let env = require_ontoenv(env)?;
860 let problems = env.doctor()?;
861 if json {
862 let out: Vec<serde_json::Value> = problems
863 .into_iter()
864 .map(|p| serde_json::json!({
865 "message": p.message,
866 "locations": p.locations.into_iter().map(|loc| loc.to_string()).collect::<Vec<_>>()
867 }))
868 .collect();
869 println!("{}", serde_json::to_string_pretty(&out)?);
870 } else if problems.is_empty() {
871 println!("No issues found.");
872 } else {
873 println!("Found {} issues:", problems.len());
874 for problem in problems {
875 println!("- {}", problem.message);
876 for location in problem.locations {
877 println!(" - {location}");
878 }
879 }
880 }
881 }
882 Commands::Config(config_cmd) => {
883 handle_config_command(config_cmd, cmd.temporary)?;
884 }
885 Commands::Reset { .. } => {
886 }
888 }
889
890 Ok(())
891}
892
893fn require_ontoenv(env: Option<OntoEnv>) -> Result<OntoEnv> {
894 env.ok_or_else(|| {
895 anyhow::anyhow!("OntoEnv not found. Run `ontoenv init` to create a new OntoEnv or use -t/--temporary to use a temporary environment.")
896 })
897}
898
899fn format_import_paths(
900 target: &NamedNode,
901 paths: Vec<Vec<GraphIdentifier>>,
902 missing: bool,
903) -> Vec<Vec<String>> {
904 let mut unique: BTreeSet<Vec<String>> = BTreeSet::new();
905 if paths.is_empty() {
906 if missing {
907 unique.insert(vec![format!("{} (missing)", target.to_uri_string())]);
908 }
909 return unique.into_iter().collect();
910 }
911 for path in paths {
912 let mut entries: Vec<String> = path.into_iter().map(|id| id.to_uri_string()).collect();
913 if missing {
914 entries.push(format!("{} (missing)", target.to_uri_string()));
915 }
916 unique.insert(entries);
917 }
918 unique.into_iter().collect()
919}
920
921fn print_import_paths(target: &NamedNode, paths: Vec<Vec<GraphIdentifier>>, missing: bool) {
922 if paths.is_empty() {
923 if missing {
924 println!(
925 "Ontology {} is missing but no importers reference it.",
926 target.to_uri_string()
927 );
928 } else {
929 println!("No importers found for {}", target.to_uri_string());
930 }
931 return;
932 }
933
934 println!(
935 "Why {}{}:",
936 target.to_uri_string(),
937 if missing { " (missing)" } else { "" }
938 );
939
940 let mut lines: BTreeSet<String> = BTreeSet::new();
941 for path in paths {
942 let mut segments: Vec<String> = path.into_iter().map(|id| id.to_uri_string()).collect();
943 if missing {
944 segments.push(format!("{} (missing)", target.to_uri_string()));
945 }
946 lines.insert(segments.join(" -> "));
947 }
948
949 for line in lines {
950 println!("{}", line);
951 }
952}