2020import logging
2121import shutil
2222import tempfile
23+ import rdflib
2324from collections .abc import Collection
2425from pathlib import Path
2526from typing import Optional , TypeVar , Union
27+ from urllib .parse import urljoin
2628
2729from rocrate_validator import models , services
2830from rocrate_validator .constants import DEFAULT_PROFILE_IDENTIFIER
3234
3335T = TypeVar ("T" )
3436
37+ SPARQL_PREFIXES = """PREFIX schema: <http://schema.org/>
38+ """
39+
3540
3641def first (c : Collection [T ]) -> T :
3742 return next (iter (c ))
3843
3944
45+ def load_graph_and_preserve_relative_ids (json_data , base = "http://example.org/" ):
46+
47+ rel_ids = set ()
48+
49+ def collect_ids (obj ):
50+ if isinstance (obj , dict ):
51+ if "@id" in obj :
52+ idv = obj ["@id" ]
53+ if isinstance (idv , str ) and (idv .startswith ("./" ) or idv .startswith ("../" ) or idv .startswith ("#" )):
54+ rel_ids .add (idv )
55+ for v in obj .values ():
56+ collect_ids (v )
57+ elif isinstance (obj , list ):
58+ for item in obj :
59+ collect_ids (item )
60+
61+ collect_ids (json_data )
62+
63+ g = rdflib .Graph ()
64+ g .parse (data = json_data , format = "json-ld" , publicID = base )
65+
66+ mapping = {}
67+ for rid in rel_ids :
68+ expanded = urljoin (base , rid )
69+ mapping [expanded ] = rid
70+
71+ def replace_uri_in_graph (graph , old_uri_str , new_uri_str ):
72+ new = rdflib .URIRef (new_uri_str )
73+ triples_to_add = []
74+ triples_to_remove = []
75+ for s , p , o in graph .triples ((None , None , None )):
76+ s2 = new if (isinstance (s , rdflib .URIRef ) and str (s ) == old_uri_str ) else s
77+ o2 = new if (isinstance (o , rdflib .URIRef ) and str (o ) == old_uri_str ) else o
78+ if (s2 , p , o2 ) != (s , p , o ):
79+ triples_to_remove .append ((s , p , o ))
80+ triples_to_add .append ((s2 , p , o2 ))
81+ for t in triples_to_remove :
82+ graph .remove (t )
83+ for t in triples_to_add :
84+ graph .add (t )
85+
86+ for expanded , rel in mapping .items ():
87+ replace_uri_in_graph (g , expanded , rel )
88+
89+ return g
90+
91+
4092def do_entity_test (
4193 rocrate_path : Union [Path , str ],
4294 requirement_severity : models .Severity ,
@@ -46,11 +98,15 @@ def do_entity_test(
4698 abort_on_first : bool = False ,
4799 profile_identifier : str = DEFAULT_PROFILE_IDENTIFIER ,
48100 rocrate_entity_patch : Optional [dict ] = None ,
101+ rocrate_entity_mod_sparql : Optional [str ] = None ,
49102 skip_checks : Optional [list [str ]] = ()
50103):
51104 """
52105 Shared function to test a RO-Crate entity
53106 """
107+ assert not (rocrate_entity_patch and rocrate_entity_mod_sparql ), \
108+ "Cannot use rocrate_entity_patch and rocrate_entity_mod_sparql together"
109+
54110 # declare variables
55111 failed_requirements = None
56112 detected_issues = None
@@ -59,7 +115,7 @@ def do_entity_test(
59115 rocrate_path = Path (rocrate_path )
60116
61117 temp_rocrate_path = None
62- if rocrate_entity_patch is not None and rocrate_path .is_dir ():
118+ if any ([ rocrate_entity_patch , rocrate_entity_mod_sparql ]) and rocrate_path .is_dir ():
63119 # create a temporary copy of the RO-Crate
64120 temp_rocrate_path = Path (tempfile .TemporaryDirectory ().name )
65121 # copy the RO-Crate to the temporary path using shutil
@@ -68,14 +124,30 @@ def do_entity_test(
68124 with open (temp_rocrate_path / "ro-crate-metadata.json" , "r" ) as f :
69125 rocrate = json .load (f )
70126 # update the RO-Crate metadata with the patch
71- for key , value in rocrate_entity_patch .items ():
72- for entity in rocrate ["@graph" ]:
73- if entity ["@id" ] == key :
74- entity .update (value )
75- break
76- # save the updated RO-Crate metadata
77- with open (temp_rocrate_path / "ro-crate-metadata.json" , "w" ) as f :
78- json .dump (rocrate , f )
127+ if rocrate_entity_patch is not None :
128+ for key , value in rocrate_entity_patch .items ():
129+ for entity in rocrate ["@graph" ]:
130+ if entity ["@id" ] == key :
131+ entity .update (value )
132+ break
133+ # save the updated RO-Crate metadata
134+ with open (temp_rocrate_path / "ro-crate-metadata.json" , "w" ) as f :
135+ json .dump (rocrate , f )
136+ # update the RO-Crate metadata using SPARQL, if required
137+ if rocrate_entity_mod_sparql is not None :
138+ rocrate_graph = load_graph_and_preserve_relative_ids (rocrate )
139+
140+ rocrate_graph .update (rocrate_entity_mod_sparql )
141+
142+ # save the updated RO-Crate metadata
143+ context = "https://w3id.org/ro/crate/1.1/context"
144+ rocrate_graph .serialize (
145+ Path (temp_rocrate_path , "ro-crate-metadata.json" ),
146+ format = "json-ld" ,
147+ context = context ,
148+ indent = 2 ,
149+ use_native_types = True ,
150+ )
79151 rocrate_path = temp_rocrate_path
80152
81153 if expected_triggered_requirements is None :
0 commit comments