Skip to content

Commit

Permalink
Changes for new SPDX 3 Model
Browse files Browse the repository at this point in the history
The new SPDX 3 model has exposed some problems with the way the ontology
is parsed.
  • Loading branch information
JPEWdev committed Feb 29, 2024
1 parent 0e71cfb commit 79d9bc9
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 65 deletions.
8 changes: 8 additions & 0 deletions src/shacl2code/lang/templates/jsonschema.j2
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@
"$ref": "#/$defs/SemVer"
{%- elif prop.datatype == "https://spdx.org/rdf/v3/Core/Extension" %}
"$ref": "#/$defs/Extension"
{%- elif prop.datatype == "https://rdf.spdx.org/v3/Core/DateTime" %}
"$ref": "#/$defs/DateTime"
{%- elif prop.datatype == "https://rdf.spdx.org/v3/Core/MediaType" %}
"$ref": "#/$defs/MediaType"
{%- elif prop.datatype == "https://rdf.spdx.org/v3/Core/SemVer" %}
"$ref": "#/$defs/SemVer"
{%- elif prop.datatype == "https://rdf.spdx.org/v3/Core/Extension" %}
"$ref": "#/$defs/Extension"
{%- elif prop.datatype == "http://www.w3.org/2001/XMLSchema#positiveInteger" %}
"type": "integer",
"minimum": 0
Expand Down
4 changes: 4 additions & 0 deletions src/shacl2code/lang/templates/python.j2
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,10 @@ DATATYPE_CLASSES = {
"https://spdx.org/rdf/v3/Core/MediaType": "MediaTypeProp",
"https://spdx.org/rdf/v3/Core/SemVer": "SemVerProp",
"https://spdx.org/rdf/v3/Core/Extension": "ExtensionProp",
"https://rdf.spdx.org/v3/Core/DateTime": "DateTimeProp",
"https://rdf.spdx.org/v3/Core/MediaType": "MediaTypeProp",
"https://rdf.spdx.org/v3/Core/SemVer": "SemVerProp",
"https://rdf.spdx.org/v3/Core/Extension": "ExtensionProp",
"http://www.w3.org/2001/XMLSchema#positiveInteger": "PositiveIntegerProp",
"http://www.w3.org/2001/XMLSchema#nonNegativeInteger": "NonNegativeIntegerProp",
"http://www.w3.org/2001/XMLSchema#boolean": "BooleanProp",
Expand Down
179 changes: 122 additions & 57 deletions src/shacl2code/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@ def to_var_name(name):
return name


def is_type(obj, *typ):
if "@type" not in obj:
return False

if isinstance(obj["@type"], list):
return all(t in obj["@type"] for t in typ)

return all(obj["@type"] == t for t in typ)


def common_prefix(*s):
if not s:
return ""

if len(s) == 1:
return s[0]

p1 = common_prefix(*s[: len(s) // 2])
p2 = common_prefix(*s[len(s) // 2 :])
for idx in range(len(p1)):
if idx >= len(p2):
return p2

if p1[idx] != p2[idx]:
return p2[:idx]

return p1


@dataclass
class EnumValue:
_id: str
Expand Down Expand Up @@ -70,50 +99,64 @@ class Model(object):
def __init__(self, model_data, context=None):
self.model = jsonld.expand(model_data)
self.context = context
self.compact = {}
self.compact_ids = {}
self.objects = {}
self.enums = []
self.classes = []
classes = []
enums = []
model_context = {}

context = model_data.get("@context", {})
if isinstance(model_data, dict) and "@context" in model_data:
model_context = model_data["@context"]

for obj in self.model:
self.compact[obj["@id"]] = jsonld.compact(obj, context)
del self.compact[obj["@id"]]["@context"]
if model_context:
self.compact_ids[obj["@id"]] = jsonld.compact(obj, model_context)["@id"]

self.objects[obj["@id"]] = obj

if "http://www.w3.org/2002/07/owl#oneOf" in obj:
enums.append(obj)
elif "@type" in obj:
if "http://www.w3.org/2002/07/owl#Class" in obj["@type"]:
classes.append(obj)

self.class_ids = set(c["@id"] for c in classes)
self.enum_ids = set(e["@id"] for e in enums)
if is_type(obj, "http://www.w3.org/2002/07/owl#Class"):
classes.append(obj)

for c in classes:
enum_values = []

for _id, obj in self.objects.items():
if not is_type(
obj,
"http://www.w3.org/2002/07/owl#NamedIndividual",
c["@id"],
):
continue

v = EnumValue(
_id=_id,
varname=get_prop(
obj,
"http://www.w3.org/2000/01/rdf-schema#label",
"@value",
to_var_name(_id.split("/")[-1]),
),
comment=self.get_comment(obj),
)
enum_values.append(v)

for obj in enums:
e = Enum(
_id=obj["@id"],
clsname=self.get_class_name(obj),
comment=self.get_comment(obj),
values=[],
)
if enum_values:
enum_values.sort(key=lambda v: v._id)

for v in get_prop(obj, "http://www.w3.org/2002/07/owl#oneOf", "@list"):
e.values.append(
EnumValue(
_id=v["@id"],
varname=to_var_name(v["@id"].split("/")[-1]),
comment=self.get_comment(self.objects[v["@id"]]),
)
e = Enum(
_id=c["@id"],
clsname=self.get_class_name(c),
comment=self.get_comment(c),
values=enum_values,
)
self.enums.append(e)

e.values.sort(key=lambda v: v._id)
self.enum_ids = set(e._id for e in self.enums)

self.enums.append(e)
classes = [c for c in classes if not self.is_enum(c["@id"])]

self.class_ids = set(c["@id"] for c in classes)

for obj in classes:
c = Class(
Expand All @@ -131,47 +174,69 @@ def __init__(self, model_data, context=None):
)

for prop_id in obj.get("http://www.w3.org/ns/shacl#property", []):
prop = self.objects[prop_id["@id"]]
name = prop["http://www.w3.org/ns/shacl#name"][0]["@value"]
prop_path = get_prop(
prop,
"http://www.w3.org/ns/shacl#path",
"@id",
)
class_prop = self.objects[prop_id["@id"]]
prop = self.objects[
get_prop(
class_prop,
"http://www.w3.org/ns/shacl#path",
"@id",
)
]
name = get_prop(class_prop, "http://www.w3.org/ns/shacl#name", "@value")
if name is None:
prefix = common_prefix(prop["@id"], obj["@id"])
name = prop["@id"][len(prefix) :]

p = Property(
varname=to_var_name(name),
path=prop_path,
comment=self.get_comment(self.objects[prop_path]),
path=prop["@id"],
comment=self.get_comment(prop),
max_count=get_prop(
prop,
class_prop,
"http://www.w3.org/ns/shacl#maxCount",
"@value",
None,
),
min_count=get_prop(
prop,
class_prop,
"http://www.w3.org/ns/shacl#minCount",
"@value",
None,
),
)

prop_cls_id = get_prop(prop, "http://www.w3.org/ns/shacl#class", "@id")
if prop_cls_id:
if self.is_enum(prop_cls_id):
p.enum_id = prop_cls_id
range_id = get_prop(
prop,
"http://www.w3.org/2000/01/rdf-schema#range",
"@id",
)
if range_id is not None:
if self.is_enum(range_id):
p.enum_id = range_id

elif self.is_class(range_id):
p.class_id = range_id

elif self.is_class(prop_cls_id):
p.class_id = prop_cls_id
else:
raise ModelException(f"Unknown type '{prop_cls_id}'")
p.datatype = range_id
else:
p.datatype = get_prop(
prop,
"http://www.w3.org/ns/shacl#datatype",
"@id",
prop_cls_id = get_prop(
class_prop, "http://www.w3.org/ns/shacl#class", "@id"
)
if prop_cls_id:
if self.is_enum(prop_cls_id):
p.enum_id = prop_cls_id

elif self.is_class(prop_cls_id):
p.class_id = prop_cls_id
else:
raise ModelException(f"Unknown type '{prop_cls_id}'")
else:
p.datatype = get_prop(
class_prop,
"http://www.w3.org/ns/shacl#datatype",
"@id",
)

c.properties.append(p)

Expand Down Expand Up @@ -203,21 +268,21 @@ def is_enum(self, _id):
def is_class(self, _id):
return _id in self.class_ids

def get_compact(self, obj, *path):
def get_compact_id(self, _id):
"""
Returns the "compacted" name of an object, that is the name of the
object with the context applied
"""
v = self.compact[obj["@id"]]
for p in path:
v = v[p]
return v
if _id not in self.compact_ids:
self.compact_ids[_id] = self.context.compact(_id)

return self.compact_ids[_id]

def get_class_name(self, c):
"""
Returns the name for a class that should be used in Code
"""
return self.get_compact(c, "@id").replace(":", "_")
return to_var_name(self.get_compact_id(c["@id"]).replace(":", "_"))

def get_comment(self, obj, indent=0):
"""
Expand Down
3 changes: 0 additions & 3 deletions tests/data/bad-reference.jsonld
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@
"rdfs:comment": "CreationInfo provides information about the creation of the Element.",
"rdfs:domain": {
"@id": "core:Element"
},
"rdfs:range": {
"@id": "core:CreationInfo"
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion tests/expect/jsonschema/spdx3-context.json
Original file line number Diff line number Diff line change
Expand Up @@ -2152,7 +2152,7 @@
"oneOf": [
{ "$ref": "#/$defs/idRef" },
{
"$ref": "#/$defs/expandedlicensing_ExtendableLicense",
"$ref": "#/$defs/expandedlicensing_License",
"unevaluatedProperties": false
}
]
Expand Down
2 changes: 1 addition & 1 deletion tests/expect/jsonschema/spdx3.json
Original file line number Diff line number Diff line change
Expand Up @@ -2148,7 +2148,7 @@
"oneOf": [
{ "$ref": "#/$defs/idRef" },
{
"$ref": "#/$defs/expandedlicensing_ExtendableLicense",
"$ref": "#/$defs/expandedlicensing_License",
"unevaluatedProperties": false
}
]
Expand Down
2 changes: 1 addition & 1 deletion tests/expect/python/spdx3-context.py
Original file line number Diff line number Diff line change
Expand Up @@ -3689,7 +3689,7 @@ def __init__(self, **kwargs):
# (OrLaterOperator) or a 'with additional text' effect (WithAdditionOperator).
self._add_property(
"subjectLicense",
ObjectProp(expandedlicensing_ExtendableLicense, True),
ObjectProp(expandedlicensing_License, True),
json_name="https://spdx.org/rdf/v3/ExpandedLicensing/subjectLicense",
min_count=1,
vocab="https://spdx.org/rdf/v3/ExpandedLicensing/subjectLicense",
Expand Down
2 changes: 1 addition & 1 deletion tests/expect/python/spdx3.py
Original file line number Diff line number Diff line change
Expand Up @@ -2936,7 +2936,7 @@ def __init__(self, **kwargs):
# (OrLaterOperator) or a 'with additional text' effect (WithAdditionOperator).
self._add_property(
"subjectLicense",
ObjectProp(expandedlicensing_ExtendableLicense, True),
ObjectProp(expandedlicensing_License, True),
json_name="https://spdx.org/rdf/v3/ExpandedLicensing/subjectLicense",
min_count=1,
vocab="https://spdx.org/rdf/v3/ExpandedLicensing/subjectLicense",
Expand Down
Loading

0 comments on commit 79d9bc9

Please sign in to comment.