11use anyhow:: Result ;
22use ontoenv:: api:: { OntoEnv , ResolveTarget } ;
33use ontoenv:: config:: Config ;
4+ use ontoenv:: consts:: IMPORTS ;
45use ontoenv:: ontology:: OntologyLocation ;
56use ontoenv:: options:: { CacheMode , Overwrite , RefreshStrategy } ;
67use oxigraph:: model:: NamedNodeRef ;
8+ use oxigraph:: model:: NamedOrBlankNodeRef ;
9+ use oxigraph:: model:: TermRef ;
710use std:: fs;
811use std:: path:: PathBuf ;
912use std:: thread;
@@ -178,11 +181,20 @@ ex:b ex:p ex:o .
178181 "Merged graph missing B data"
179182 ) ;
180183
181- // No owl:imports should remain
182- assert_eq ! (
183- merged. triples_for_predicate( IMPORTS ) . count( ) ,
184- 0 ,
185- "Merged graph should not contain owl:imports"
184+ // owl:imports should be rewritten onto the root (base) ontology
185+ let imports: Vec < _ > = merged
186+ . triples_for_predicate ( IMPORTS )
187+ . filter ( |t| t. subject == NamedOrBlankNodeRef :: NamedNode ( a_id. name ( ) ) )
188+ . collect ( ) ;
189+ assert ! (
190+ !imports. is_empty( ) ,
191+ "Merged graph should contain rewritten imports on the root"
192+ ) ;
193+ assert ! (
194+ imports
195+ . iter( )
196+ . all( |t| t. subject == NamedOrBlankNodeRef :: NamedNode ( a_id. name( ) ) ) ,
197+ "All imports should be on the root ontology"
186198 ) ;
187199
188200 // Only one owl:Ontology declaration (root) should remain
@@ -199,6 +211,178 @@ ex:b ex:p ex:o .
199211 Ok ( ( ) )
200212}
201213
214+ #[ test]
215+ fn import_graph_handles_cycles ( ) -> Result < ( ) > {
216+ use ontoenv:: consts:: { IMPORTS , ONTOLOGY , TYPE } ;
217+
218+ let dir = TempDir :: new ( "ontoenv-import-cycle" ) ?;
219+
220+ let a_path = dir. path ( ) . join ( "A.ttl" ) ;
221+ let b_path = dir. path ( ) . join ( "B.ttl" ) ;
222+ let a_iri = format ! ( "file://{}" , a_path. display( ) ) ;
223+ let b_iri = format ! ( "file://{}" , b_path. display( ) ) ;
224+
225+ fs:: write (
226+ & a_path,
227+ format ! (
228+ "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n @prefix ex: <http://example.com/A#> .\n <{a}> a owl:Ontology ; owl:imports <{b}> .\n ex:A a owl:Class .\n " ,
229+ a = a_iri,
230+ b = b_iri
231+ ) ,
232+ ) ?;
233+ fs:: write (
234+ & b_path,
235+ format ! (
236+ "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n @prefix ex: <http://example.com/B#> .\n <{b}> a owl:Ontology ; owl:imports <{a}> .\n ex:B a owl:Class .\n " ,
237+ a = a_iri,
238+ b = b_iri
239+ ) ,
240+ ) ?;
241+
242+ let cfg = default_config ( & dir) ;
243+ let mut env = OntoEnv :: init ( cfg, false ) ?;
244+ env. add (
245+ OntologyLocation :: File ( a_path) ,
246+ Overwrite :: Allow ,
247+ RefreshStrategy :: UseCache ,
248+ ) ?;
249+ env. add (
250+ OntologyLocation :: File ( b_path) ,
251+ Overwrite :: Allow ,
252+ RefreshStrategy :: UseCache ,
253+ ) ?;
254+
255+ let a_id = env
256+ . resolve ( ResolveTarget :: Location ( OntologyLocation :: from_str ( & a_iri) ?) )
257+ . unwrap ( ) ;
258+ let merged = env. import_graph ( & a_id, -1 ) ?;
259+
260+ // Single root ontology
261+ let ontology_decls = merged
262+ . triples_for_object ( ONTOLOGY )
263+ . filter ( |t| t. predicate == TYPE )
264+ . count ( ) ;
265+ assert_eq ! ( ontology_decls, 1 ) ;
266+
267+ // Imports rewritten onto root with no self-loop
268+ let imports: Vec < _ > = merged
269+ . triples_for_predicate ( IMPORTS )
270+ . filter ( |t| t. subject == NamedOrBlankNodeRef :: NamedNode ( a_id. name ( ) ) )
271+ . collect ( ) ;
272+ assert_eq ! ( imports. len( ) , 1 ) ;
273+ if let TermRef :: NamedNode ( obj) = imports[ 0 ] . object {
274+ assert_eq ! ( obj. as_str( ) , b_iri) ;
275+ } else {
276+ panic ! ( "Import object was not a NamedNode" ) ;
277+ }
278+
279+ // No imports hanging off B
280+ assert_eq ! (
281+ merged
282+ . triples_for_predicate( IMPORTS )
283+ . filter( |t| {
284+ t. subject == NamedOrBlankNodeRef :: NamedNode ( NamedNodeRef :: new_unchecked( & b_iri) )
285+ } )
286+ . count( ) ,
287+ 0
288+ ) ;
289+
290+ // Data from both ontologies present
291+ assert ! ( merged
292+ . iter( )
293+ . any( |t| format!( "{:?}" , t. subject) . contains( "#A" ) ) ) ;
294+ assert ! ( merged
295+ . iter( )
296+ . any( |t| format!( "{:?}" , t. subject) . contains( "#B" ) ) ) ;
297+
298+ teardown ( dir) ;
299+ Ok ( ( ) )
300+ }
301+
302+ #[ test]
303+ fn import_graph_respects_recursion_depth ( ) -> Result < ( ) > {
304+ let dir = TempDir :: new ( "ontoenv-import-depth" ) ?;
305+
306+ let a_path = dir. path ( ) . join ( "A.ttl" ) ;
307+ let b_path = dir. path ( ) . join ( "B.ttl" ) ;
308+ let c_path = dir. path ( ) . join ( "C.ttl" ) ;
309+
310+ let a_iri = format ! ( "file://{}" , a_path. display( ) ) ;
311+ let b_iri = format ! ( "file://{}" , b_path. display( ) ) ;
312+ let c_iri = format ! ( "file://{}" , c_path. display( ) ) ;
313+
314+ fs:: write (
315+ & a_path,
316+ format ! (
317+ "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n <{a}> a owl:Ontology ; owl:imports <{b}> ." ,
318+ a = a_iri, b = b_iri
319+ ) ,
320+ ) ?;
321+ fs:: write (
322+ & b_path,
323+ format ! (
324+ "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n <{b}> a owl:Ontology ; owl:imports <{c}> ." ,
325+ b = b_iri, c = c_iri
326+ ) ,
327+ ) ?;
328+ fs:: write (
329+ & c_path,
330+ format ! (
331+ "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n <{c}> a owl:Ontology ." ,
332+ c = c_iri
333+ ) ,
334+ ) ?;
335+
336+ let cfg = default_config ( & dir) ;
337+ let mut env = OntoEnv :: init ( cfg, false ) ?;
338+ env. add (
339+ OntologyLocation :: File ( a_path) ,
340+ Overwrite :: Allow ,
341+ RefreshStrategy :: UseCache ,
342+ ) ?;
343+ env. add (
344+ OntologyLocation :: File ( b_path) ,
345+ Overwrite :: Allow ,
346+ RefreshStrategy :: UseCache ,
347+ ) ?;
348+ env. add (
349+ OntologyLocation :: File ( c_path) ,
350+ Overwrite :: Allow ,
351+ RefreshStrategy :: UseCache ,
352+ ) ?;
353+
354+ let a_id = env
355+ . resolve ( ResolveTarget :: Location ( OntologyLocation :: from_str ( & a_iri) ?) )
356+ . unwrap ( ) ;
357+
358+ // depth 0: only A (no imports attached)
359+ let g0 = env. import_graph ( & a_id, 0 ) ?;
360+ let imports0 = g0
361+ . triples_for_predicate ( IMPORTS )
362+ . filter ( |t| t. subject == NamedOrBlankNodeRef :: NamedNode ( a_id. name ( ) ) )
363+ . count ( ) ;
364+ assert_eq ! ( imports0, 0 , "depth 0 should not carry imports on root" ) ;
365+
366+ // depth 1: A imports B
367+ let g1 = env. import_graph ( & a_id, 1 ) ?;
368+ let imports_b: Vec < _ > = g1
369+ . triples_for_predicate ( IMPORTS )
370+ . filter ( |t| t. subject == NamedOrBlankNodeRef :: NamedNode ( a_id. name ( ) ) )
371+ . collect ( ) ;
372+ assert_eq ! ( imports_b. len( ) , 1 ) ;
373+
374+ // depth -1: full closure, includes C
375+ let gfull = env. import_graph ( & a_id, -1 ) ?;
376+ let imports_full: Vec < _ > = gfull
377+ . triples_for_predicate ( IMPORTS )
378+ . filter ( |t| t. subject == NamedOrBlankNodeRef :: NamedNode ( a_id. name ( ) ) )
379+ . collect ( ) ;
380+ assert_eq ! ( imports_full. len( ) , 2 ) ;
381+
382+ teardown ( dir) ;
383+ Ok ( ( ) )
384+ }
385+
202386#[ cfg( unix) ]
203387mod unix_permission_tests {
204388 use super :: * ;
0 commit comments