diff --git a/spec_parser/rdf.py b/spec_parser/rdf.py index cc9b030..a8e92c1 100644 --- a/spec_parser/rdf.py +++ b/spec_parser/rdf.py @@ -10,19 +10,31 @@ from rdflib.namespace import DCTERMS, OWL, RDF, RDFS, SH, SKOS, XSD from rdflib.tools.rdf2dot import rdf2dot -URI_BASE = "https://spdx.org/rdf/3.1/terms/" logger = logging.getLogger(__name__) def gen_rdf(model, outpath, cfg): p = outpath - ret = gen_rdf_ontology(model) + base_uri = "https://spdx.org/rdf/3/terms/" + # Determine actual base URI from Core namespace: + # https://spdx.org/rdf//terms/Core/ + # -> https://spdx.org/rdf//terms/ + for ns in model.namespaces: + if ns.name == "Core": + iri = ns.iri + if iri.endswith("/Core/"): + base_uri = iri[: -len("Core/")] + elif iri.endswith("/Core"): + base_uri = iri[: -len("Core")] + break + + ret = gen_rdf_ontology(model, base_uri) for ext in ["hext", "json-ld", "longturtle", "n3", "nt", "pretty-xml", "trig", "ttl", "xml"]: f = p / ("spdx-model." + ext) ret.serialize(f, format=ext, encoding="utf-8") - ctx = jsonld_context(ret) + ctx = jsonld_context(ret, base_uri) fn = p / "spdx-context.jsonld" with fn.open("w") as f: json.dump(ctx, f, sort_keys=True, indent=2) @@ -40,15 +52,30 @@ def xsd_range(rng, propname): return None -def gen_rdf_ontology(model): +def gen_rdf_ontology(model, base_uri: str): g = Graph() - g.bind("spdx", Namespace(URI_BASE)) + g.bind("spdx", Namespace(base_uri)) OMG_ANN = Namespace("https://www.omg.org/spec/Commons/AnnotationVocabulary/") g.bind("omg-ann", OMG_ANN) - node = URIRef(URI_BASE) + node = URIRef(base_uri) g.add((node, RDF.type, OWL.Ontology)) - g.add((node, OWL.versionIRI, node)) + + # owl:versionInfo / owl:versionIRI from namespace metadata. + # Ontology IRI is stable across minor releases (major ver. only in path). + # versionIRI identifies the specific release by replacing the major version + # segment with the full version: https://spdx.org/rdf/3/terms/ (ontology) + # -> https://spdx.org/rdf/3.1/terms/ (versionIRI for 3.1 release). + # W3C OWL 2 Section 3.1: + # https://www.w3.org/TR/owl2-syntax/#Ontology_IRI_and_Version_IRI + for ns in model.namespaces: + v = ns.metadata.get("version") or ns.metadata.get("Version") + if v: + g.add((node, OWL.versionInfo, Literal(v))) + major = v.split(".")[0] + versioned_iri = URIRef(base_uri.replace(f"/{major}/", f"/{v}/", 1)) + g.add((node, OWL.versionIRI, versioned_iri)) + break g.add((node, RDFS.label, Literal("System Package Data Exchange™ (SPDX®) Ontology", lang="en"))) g.add( ( @@ -71,7 +98,7 @@ def gen_rdf_ontology(model): gen_rdf_properties(model, g) # gen_rdf_datatypes(model, g) gen_rdf_vocabularies(model, g) - gen_rdf_individuals(model, g) + gen_rdf_individuals(model, g, base_uri) return g @@ -233,12 +260,12 @@ def gen_rdf_vocabularies(model, g): g.add((enode, RDFS.comment, Literal(d, lang="en"))) -def gen_rdf_individuals(model, g): +def gen_rdf_individuals(model, g, base_uri: str): def ci_ref(s): - return URIRef(URI_BASE + "Core/" + s) + return URIRef(base_uri + "Core/" + s) for i in model.individuals.values(): - ci_node = URIRef("https://spdx.org/rdf/3.1/creationInfo_" + i.name) + ci_node = URIRef("https://spdx.org/rdf/3/creationInfo_" + i.name) g.add((ci_node, RDF.type, ci_ref("CreationInfo"))) g.add((ci_node, RDFS.comment, Literal("This individual element was defined by the spec.", lang="en"))) g.add((ci_node, ci_ref("created"), Literal("2026-01-23T03:01:00Z", datatype=XSD.dateTimeStamp))) @@ -259,7 +286,7 @@ def ci_ref(s): g.add((node, OWL.sameAs, URIRef(custom_iri))) -def jsonld_context(g): +def jsonld_context(g, base_uri: str): terms = dict() def get_subject_term(subject): @@ -308,7 +335,7 @@ def get_subject_term(subject): except ValueError: continue - if base != URI_BASE.rstrip("/"): + if base != base_uri.rstrip("/"): continue key = name if ns == "Core" else ns.lower() + "_" + name @@ -320,7 +347,7 @@ def get_subject_term(subject): terms[key] = get_subject_term(subject) - terms["spdx"] = URI_BASE + terms["spdx"] = base_uri terms["spdxId"] = "@id" terms["type"] = "@type"