Skip to content

Commit c22c4ca

Browse files
committed
add get information and docs
1 parent 0a86b65 commit c22c4ca

File tree

3 files changed

+222
-7
lines changed

3 files changed

+222
-7
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Examples:
9595
- Discovery: Commands (except `init`) discover an environment by walking up parent directories from the current working directory, looking for `.ontoenv/`.
9696
- Override: Set `ONTOENV_DIR` to point to a specific environment; if it points at a `.ontoenv` directory the parent of that directory is used as the root.
9797
- Creation: Only `ontoenv init` creates an environment on disk. Other commands will error if no environment is found.
98+
- Positional search directories: Only `ontoenv init` accepts positional search directories (LOCATIONS). Other commands ignore trailing positionals.
9899
- Temporary mode: Pass `--temporary` to run with an in‑memory environment (no `.ontoenv/`).
99100

100101
#### update
@@ -117,6 +118,23 @@ Examples:
117118
- `ontoenv closure http://example.org/ont/MyOntology --no-rewrite-sh-prefixes`
118119
- `ontoenv closure http://example.org/ont/MyOntology --keep-owl-imports`
119120

121+
#### get
122+
123+
Retrieve a single ontology graph from the environment and write it to STDOUT or a file in a chosen serialization format.
124+
125+
Examples:
126+
- `ontoenv get http://example.org/ont/MyOntology` — prints Turtle to STDOUT
127+
- `ontoenv get http://example.org/ont/MyOntology --format jsonld` — prints JSON‑LD to STDOUT
128+
- `ontoenv get http://example.org/ont/MyOntology --output my.ttl` — writes Turtle to `my.ttl`
129+
- Disambiguate when multiple copies share the same IRI (different locations):
130+
- `ontoenv get http://example.org/ont/MyOntology --location ./ontologies/MyOntology-1.4.ttl`
131+
- `ontoenv get http://example.org/ont/MyOntology -l https://example.org/MyOntology-1.3.ttl`
132+
133+
Notes:
134+
- Supported formats: `turtle` (default), `ntriples`, `rdfxml`, `jsonld`.
135+
- `--output` writes to a file; omit to print to STDOUT.
136+
- `--location` accepts a file path or URL and is only needed to disambiguate when multiple sources exist for the same IRI.
137+
120138
#### Other commands
121139

122140
- `ontoenv dump` — show ontologies, imports, sizes, and metadata

cli/src/main.rs

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use ontoenv::ontology::{GraphIdentifier, OntologyLocation};
77
use ontoenv::util::write_dataset_to_file;
88
use ontoenv::ToUriString;
99
use oxigraph::model::NamedNode;
10+
use oxigraph::io::{RdfFormat, JsonLdProfileSet};
1011
use serde_json;
1112
use std::env::current_dir;
1213
use std::path::PathBuf;
@@ -49,9 +50,6 @@ struct Cli {
4950
/// Do not search for ontologies in the search directories
5051
#[clap(long = "no-search", short = 'n', action, global = true)]
5152
no_search: bool,
52-
/// Directories to search for ontologies. If not provided, the current directory is used.
53-
#[clap(global = true, last = true)]
54-
locations: Option<Vec<PathBuf>>,
5553
}
5654

5755
#[derive(Debug, Subcommand)]
@@ -108,6 +106,9 @@ enum Commands {
108106
/// Overwrite the environment if it already exists
109107
#[clap(long, default_value = "false")]
110108
overwrite: bool,
109+
/// Directories to search for ontologies. If not provided, the current directory is used.
110+
#[clap(last = true)]
111+
locations: Option<Vec<PathBuf>>,
111112
},
112113
/// Prints the version of the ontoenv binary
113114
Version,
@@ -146,6 +147,20 @@ enum Commands {
146147
#[clap(long, default_value = "-1")]
147148
recursion_depth: i32,
148149
},
150+
/// Retrieve a single graph from the environment and write it to STDOUT or a file
151+
Get {
152+
/// Ontology IRI (name)
153+
ontology: String,
154+
/// Optional source location (file path or URL) to disambiguate
155+
#[clap(long, short = 'l')]
156+
location: Option<String>,
157+
/// Output file path; if omitted, writes to STDOUT
158+
#[clap(long)]
159+
output: Option<String>,
160+
/// Serialization format: one of [turtle, ntriples, rdfxml, jsonld] (default: turtle)
161+
#[clap(long, short = 'f')]
162+
format: Option<String>,
163+
},
149164
/// Add an ontology to the environment
150165
Add {
151166
/// The location of the ontology to add (file path or URL)
@@ -212,6 +227,7 @@ impl ToString for Commands {
212227
Commands::Status { .. } => "Status".to_string(),
213228
Commands::Update { .. } => "Update".to_string(),
214229
Commands::Closure { .. } => "Closure".to_string(),
230+
Commands::Get { .. } => "Get".to_string(),
215231
Commands::Add { .. } => "Add".to_string(),
216232
Commands::List { .. } => "List".to_string(),
217233
Commands::Dump { .. } => "Dump".to_string(),
@@ -406,8 +422,9 @@ fn main() -> Result<()> {
406422
.temporary(cmd.temporary)
407423
.no_search(cmd.no_search);
408424

409-
if let Some(locations) = cmd.locations {
410-
builder = builder.locations(locations);
425+
// Locations only apply to `init`; other commands ignore positional LOCATIONS
426+
if let Commands::Init { locations: Some(locs), .. } = &cmd.command {
427+
builder = builder.locations(locs.clone());
411428
}
412429
// only set includes if they are provided on the command line, otherwise use builder defaults
413430
if !cmd.includes.is_empty() {
@@ -469,20 +486,30 @@ fn main() -> Result<()> {
469486
// create the env object to use in the subcommand.
470487
// - if temporary is true, create a new env object each time
471488
// - if temporary is false, load the env from the .ontoenv directory if it exists
489+
// Determine if this command needs write access to the store
490+
let needs_rw = matches!(
491+
cmd.command,
492+
Commands::Add { .. } | Commands::Update { .. }
493+
);
494+
472495
let env: Option<OntoEnv> = if cmd.temporary {
473496
// Create a new OntoEnv object in temporary mode
474497
let e = OntoEnv::init(config.clone(), false)?;
475498
Some(e)
476499
} else if cmd.command.to_string() != "Init" && ontoenv_exists {
477500
// if .ontoenv exists, load it from discovered root
478-
Some(OntoEnv::load_from_directory(discovered_root.unwrap(), false)?) // no read-only
501+
// Open read-only unless the command requires write access
502+
Some(OntoEnv::load_from_directory(
503+
discovered_root.unwrap(),
504+
!needs_rw,
505+
)?)
479506
} else {
480507
None
481508
};
482509
info!("OntoEnv loaded: {}", env.is_some());
483510

484511
match cmd.command {
485-
Commands::Init { overwrite } => {
512+
Commands::Init { overwrite, .. } => {
486513
// if temporary, raise an error
487514
if cmd.temporary {
488515
return Err(anyhow::anyhow!(
@@ -509,6 +536,68 @@ fn main() -> Result<()> {
509536
// `update` will also save it to the directory.
510537
let _ = OntoEnv::init(config, overwrite)?;
511538
}
539+
Commands::Get {
540+
ontology,
541+
location,
542+
output,
543+
format,
544+
} => {
545+
let env = require_ontoenv(env)?;
546+
547+
// If a location is provided, resolve by location. Otherwise resolve by name (IRI).
548+
let graph = if let Some(loc) = location {
549+
let oloc = if loc.starts_with("http://") || loc.starts_with("https://") {
550+
OntologyLocation::Url(loc)
551+
} else {
552+
// Normalize to absolute path
553+
ontoenv::ontology::OntologyLocation::from_str(&loc).unwrap_or_else(|_| OntologyLocation::File(PathBuf::from(loc)))
554+
};
555+
// Read directly from the specified location to disambiguate
556+
oloc.graph()?
557+
} else {
558+
let iri = NamedNode::new(ontology)
559+
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
560+
let graphid = env
561+
.resolve(ResolveTarget::Graph(iri))
562+
.ok_or(anyhow::anyhow!("Ontology not found"))?;
563+
env.get_graph(&graphid)?
564+
};
565+
566+
let fmt = match format
567+
.as_deref()
568+
.unwrap_or("turtle")
569+
.to_ascii_lowercase()
570+
.as_str()
571+
{
572+
"turtle" | "ttl" => RdfFormat::Turtle,
573+
"ntriples" | "nt" => RdfFormat::NTriples,
574+
"rdfxml" | "xml" => RdfFormat::RdfXml,
575+
"jsonld" | "json-ld" => RdfFormat::JsonLd { profile: JsonLdProfileSet::default() },
576+
other => {
577+
return Err(anyhow::anyhow!(
578+
"Unsupported format '{}'. Use one of: turtle, ntriples, rdfxml, jsonld",
579+
other
580+
))
581+
}
582+
};
583+
584+
if let Some(path) = output {
585+
let mut file = std::fs::File::create(path)?;
586+
let mut serializer = oxigraph::io::RdfSerializer::from_format(fmt).for_writer(&mut file);
587+
for t in graph.iter() {
588+
serializer.serialize_triple(t)?;
589+
}
590+
serializer.finish()?;
591+
} else {
592+
let stdout = std::io::stdout();
593+
let mut handle = stdout.lock();
594+
let mut serializer = oxigraph::io::RdfSerializer::from_format(fmt).for_writer(&mut handle);
595+
for t in graph.iter() {
596+
serializer.serialize_triple(t)?;
597+
}
598+
serializer.finish()?;
599+
}
600+
}
512601
Commands::Version => {
513602
println!(
514603
"ontoenv {} @ {}",

cli/tests/cli_integration.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,111 @@ fn why_lists_importers_paths() {
133133
assert!(stdout.contains(&format!("{} -> {} -> {}", c_uri, a_uri, b_uri)));
134134
}
135135

136+
// Get command: default Turtle to STDOUT by IRI
137+
#[test]
138+
fn get_stdout_turtle() {
139+
let exe = ontoenv_bin();
140+
let root = tmp_dir("get_turtle");
141+
let iri = "http://example.org/ont/Only";
142+
let path = root.join("only.ttl");
143+
write_ttl(&path, iri, "");
144+
145+
// init
146+
let out = Command::new(&exe)
147+
.current_dir(&root)
148+
.arg("init")
149+
.output()
150+
.expect("run init");
151+
assert!(out.status.success(), "init failed: {}", String::from_utf8_lossy(&out.stderr));
152+
153+
// get to stdout
154+
let out = Command::new(&exe)
155+
.current_dir(&root)
156+
.arg("get")
157+
.arg(iri)
158+
.output()
159+
.expect("run get");
160+
assert!(out.status.success(), "get failed: {}", String::from_utf8_lossy(&out.stderr));
161+
let stdout = String::from_utf8_lossy(&out.stdout);
162+
// Expect to see the ontology triple in some form
163+
assert!(stdout.contains(iri), "stdout did not contain IRI: {}", stdout);
164+
}
165+
166+
// Get command: JSON-LD output
167+
#[test]
168+
fn get_jsonld_output() {
169+
let exe = ontoenv_bin();
170+
let root = tmp_dir("get_jsonld");
171+
let iri = "http://example.org/ont/JL";
172+
let path = root.join("jl.ttl");
173+
write_ttl(&path, iri, "");
174+
175+
// init
176+
let out = Command::new(&exe)
177+
.current_dir(&root)
178+
.arg("init")
179+
.output()
180+
.expect("run init");
181+
assert!(out.status.success());
182+
183+
// get jsonld to stdout
184+
let out = Command::new(&exe)
185+
.current_dir(&root)
186+
.arg("get")
187+
.arg(iri)
188+
.arg("--format")
189+
.arg("jsonld")
190+
.output()
191+
.expect("run get jsonld");
192+
assert!(out.status.success(), "get jsonld failed: {}", String::from_utf8_lossy(&out.stderr));
193+
let stdout = String::from_utf8_lossy(&out.stdout);
194+
assert!(stdout.contains(iri), "jsonld output missing iri; got: {}", stdout);
195+
assert!(stdout.trim_start().starts_with("{") || stdout.trim_start().starts_with("["), "not JSON-LD? {}", stdout);
196+
}
197+
198+
// Get command: disambiguate with --location when same IRI at two locations
199+
#[test]
200+
fn get_with_location_disambiguates() {
201+
let exe = ontoenv_bin();
202+
let root = tmp_dir("get_loc");
203+
let iri = "http://example.org/ont/Dup";
204+
let p1 = root.join("dup_v1.ttl");
205+
let p2 = root.join("dup_v2.ttl");
206+
// add distinguishing triples
207+
write_ttl(&p1, iri, "<http://example.org/x> <http://example.org/p> \"v1\" .");
208+
write_ttl(&p2, iri, "<http://example.org/x> <http://example.org/p> \"v2\" .");
209+
210+
// init
211+
let out = Command::new(&exe)
212+
.current_dir(&root)
213+
.arg("init")
214+
.output()
215+
.expect("run init");
216+
assert!(out.status.success());
217+
218+
// get with location pointing to v1
219+
let out = Command::new(&exe)
220+
.current_dir(&root)
221+
.arg("get")
222+
.arg(iri)
223+
.arg("--location")
224+
.arg(p1.to_str().unwrap())
225+
.output()
226+
.expect("run get v1");
227+
assert!(out.status.success(), "get v1 failed: {}", String::from_utf8_lossy(&out.stderr));
228+
let s1 = String::from_utf8_lossy(&out.stdout);
229+
assert!(s1.contains("\"v1\""), "expected v1 triple, got: {}", s1);
230+
231+
// get with location pointing to v2
232+
let out = Command::new(&exe)
233+
.current_dir(&root)
234+
.arg("get")
235+
.arg(iri)
236+
.arg("-l")
237+
.arg(p2.to_str().unwrap())
238+
.output()
239+
.expect("run get v2");
240+
assert!(out.status.success(), "get v2 failed: {}", String::from_utf8_lossy(&out.stderr));
241+
let s2 = String::from_utf8_lossy(&out.stdout);
242+
assert!(s2.contains("\"v2\""), "expected v2 triple, got: {}", s2);
243+
}

0 commit comments

Comments
 (0)