Skip to content

Commit 9e8a38e

Browse files
authored
Merge pull request #35 from softwarepub/improved-types-for-parameters
Implement improved type specification for sc:Parameter placeholders
2 parents 363578a + 8045588 commit 9e8a38e

6 files changed

Lines changed: 216 additions & 114 deletions

File tree

examples/policies/description-parameterizable.ttl

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1515

1616
scex:longDescriptionMinLength a sc:Parameter ;
17-
# The type of this parameter. Could be used for further validation.
18-
sc:parameterType xsd:integer ;
19-
# The string used for lookup in the config file. Could be a "path" in the future.
17+
sc:parameterOuterType sc:Scalar ;
18+
sc:parameterInnerType xsd:integer ;
2019
sc:parameterConfigPath "description_min_length" ;
21-
# The default value for this parameter. Must be of the type given by `sc:parameterType` of this `sc:Parameter`.
2220
sc:parameterDefaultValue 50 ;
2321
.
2422

examples/policies/licenses-parameterizable.ttl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1515

1616
scex:suggestedLicenses a sc:Parameter ;
17-
sc:parameterType rdf:List ;
17+
sc:parameterOuterType rdf:List ;
18+
sc:parameterInnerType xsd:anyURI ;
1819
sc:parameterConfigPath "suggested_licenses" ;
1920
sc:parameterDefaultValue ( "https://spdx.org/licenses/Apache-2.0" "https://spdx.org/licenses/MIT" ) .
2021

src/sc_validate/data_model.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# SPDX-FileCopyrightText: 2025 Helmholtz-Zentrum Dresden - Rossendorf (HZDR)
2+
# SPDX-License-Identifier: Apache-2.0
3+
# SPDX-FileContributor: David Pape
4+
5+
from dataclasses import dataclass
6+
from enum import Enum
7+
from typing import List
8+
9+
from rdflib import Graph, Literal
10+
from rdflib.namespace import SH
11+
from rdflib.term import URIRef
12+
13+
from sc_validate.namespaces import SC
14+
15+
#################################### Software CaRD ####################################
16+
17+
18+
@dataclass
19+
class Parameter:
20+
"""Model of a `sc:Parameter`."""
21+
22+
uri: URIRef
23+
outer_type: URIRef
24+
inner_type: URIRef
25+
default_value: URIRef
26+
config_path: str
27+
28+
@classmethod
29+
def from_graph(cls, uri: URIRef, graph: Graph):
30+
outer_type = graph.value(uri, SC.parameterOuterType, None)
31+
inner_type = graph.value(uri, SC.parameterInnerType, None)
32+
default_value = graph.value(uri, SC.parameterDefaultValue, None)
33+
config_path = str(graph.value(uri, SC.parameterConfigPath, None))
34+
35+
return cls(
36+
uri=uri,
37+
outer_type=outer_type,
38+
inner_type=inner_type,
39+
default_value=default_value,
40+
config_path=config_path,
41+
)
42+
43+
44+
# TODO: This only works for constraints of type NodeShape. Is this enough?
45+
@dataclass
46+
class Policy:
47+
"""Model of a Software CaRD Policy."""
48+
49+
name: str
50+
description: str
51+
52+
@classmethod
53+
def from_graph(cls, reference: URIRef, graph: Graph):
54+
name = graph.value(reference, SH.name, None)
55+
description = graph.value(reference, SH.description, None)
56+
return cls(name=name, description=description)
57+
58+
59+
######################################## SHACL ########################################
60+
61+
62+
class Severity(Enum):
63+
"""Model of `sh:Severity`."""
64+
65+
INFO = 1
66+
WARNING = 2
67+
VIOLATION = 3
68+
OTHER = 4
69+
70+
def __str__(self):
71+
return self.name.title()
72+
73+
@classmethod
74+
def from_graph(cls, reference: URIRef, graph: Graph):
75+
if reference == SH.Info:
76+
return cls.INFO
77+
if reference == SH.Warning:
78+
return cls.WARNING
79+
if reference == SH.Violation:
80+
return cls.VIOLATION
81+
return cls.OTHER
82+
83+
84+
@dataclass
85+
class ValidationResult:
86+
"""Model of a `sh:ValidationResult`."""
87+
88+
severity: Severity
89+
message: str
90+
source_policy: Policy
91+
92+
@classmethod
93+
def from_graph(cls, reference: URIRef, graph: Graph):
94+
severity = graph.value(reference, SH.resultSeverity, None)
95+
message = graph.value(reference, SH.resultMessage, None)
96+
source_policy = graph.value(reference, SH.sourceShape, None)
97+
return cls(
98+
severity=Severity.from_graph(severity, graph),
99+
message=message,
100+
source_policy=Policy.from_graph(source_policy, graph),
101+
)
102+
103+
104+
@dataclass
105+
class ValidationReport:
106+
"""Model of a `sh:ValidationReport`."""
107+
108+
conforms: bool
109+
results: List[ValidationResult]
110+
111+
@classmethod
112+
def from_graph(cls, reference: URIRef, graph: Graph):
113+
conforms = (reference, SH.conforms, Literal(True)) in graph
114+
results = graph.objects(reference, SH.result)
115+
return cls(
116+
conforms=conforms,
117+
results=[ValidationResult.from_graph(result, graph) for result in results],
118+
)

src/sc_validate/namespaces.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@ class SC(DefinedNamespace):
1111

1212
_NS = Namespace("https://schema.software-metadata.pub/software-card/2025-01-01/#")
1313

14+
#: A "placeholder" parameter to be used in a policy
1415
Parameter: URIRef
1516

16-
parameterType: URIRef
17+
#: Represents the notion that a `sc:Parameter`` is a stand-in for a scalar (i.e.
18+
#: non-List/Seq/Bag/Alt) value
19+
Scalar: URIRef
20+
21+
#: The "outer type" of a parameter. May be `rdf:List/Seq/Bag/Alt` or `sc:Scalar`.
22+
parameterOuterType: URIRef
23+
#: The "inner type" of a parameter. May be `rdfs:Resource` to refer to a complex
24+
#: type. Or, may be one of the following primitive data types:
25+
#: `xsd:integer/float/string/.../anyURI` to refer to a literal value.
26+
parameterInnerType: URIRef
1727
parameterConfigPath: URIRef
1828
parameterDefaultValue: URIRef
1929

src/sc_validate/rdf.py

Lines changed: 81 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,27 @@
66
from typing import Any, Dict, Tuple
77

88
from pyshacl import validate
9-
from rdflib import BNode, Graph, Literal
9+
from rdflib import BNode, Graph, Literal, Node
1010
from rdflib.collection import Collection
11-
from rdflib.namespace import RDF
11+
from rdflib.namespace import RDF, RDFS, XSD
1212

13+
from sc_validate.data_model import Parameter
1314
from sc_validate.namespaces import PREFIXES, SC
1415

16+
# TODO: Add debug messages to all asserts.
17+
18+
19+
_ALLOWED_INNER_TYPES = (
20+
XSD.string,
21+
XSD.boolean,
22+
XSD.integer,
23+
XSD.int,
24+
XSD.decimal,
25+
XSD.float,
26+
XSD.double,
27+
XSD.anyURI,
28+
)
29+
1530

1631
def read_rdf_resource(source: pathlib.Path | str) -> Graph:
1732
graph = Graph()
@@ -21,51 +36,80 @@ def read_rdf_resource(source: pathlib.Path | str) -> Graph:
2136
return graph
2237

2338

39+
def _create_rdf_list_parameter(
40+
parameter: Parameter, graph: Graph, config_parameter: Any
41+
) -> Node:
42+
assert parameter.outer_type == RDF.List
43+
# assert (parameter.default_value, RDF.type, RDF.List) in graph
44+
assert (parameter.default_value, RDF.first, None) in graph
45+
assert (parameter.default_value, RDF.rest, None) in graph
46+
47+
if config_parameter is None:
48+
return Collection(graph, parameter.default_value).uri
49+
50+
# TODO: What happens if `parameter_value` is empty list?
51+
assert isinstance(config_parameter, list)
52+
return Collection(
53+
graph, BNode(), seq=[Literal(value) for value in config_parameter]
54+
).uri
55+
56+
57+
def _create_sc_scalar_parameter(
58+
parameter: Parameter, graph: Graph, config_parameter: Any
59+
) -> Node:
60+
assert parameter.outer_type == SC.Scalar
61+
62+
assert isinstance(config_parameter, (str, int, float, type(None)))
63+
64+
if config_parameter:
65+
return Literal(config_parameter)
66+
67+
return graph.value(parameter.uri, SC.parameterDefaultValue, None)
68+
69+
2470
# TODO: Is it safe to modify the graph while iterating over it?
2571
def parameterize_graph(graph: Graph, config_parameters: Dict[str, Any]) -> Graph:
2672
# iterate over all declared parameters of type `sc:Parameter`
27-
for parameter in graph.subjects(RDF.type, SC.Parameter):
28-
# get config name for the parameter
29-
parameter_name = str(graph.value(parameter, SC.parameterConfigPath, None))
30-
is_list = graph.value(parameter, SC.parameterType, None) == RDF.List
31-
32-
# get default value for the parameter
33-
default_value = graph.value(parameter, SC.parameterDefaultValue, None)
34-
35-
if is_list:
36-
assert (default_value, RDF.first, None) in graph
37-
assert (default_value, RDF.rest, None) in graph
38-
default_value = Collection(graph, default_value)
39-
40-
# load parameter from config by its name
41-
parameter_value = config_parameters.get(parameter_name, [])
42-
assert isinstance(parameter_value, list)
43-
44-
o = default_value.uri
45-
if parameter_value:
46-
parameter_value = Collection(
47-
graph, BNode(), seq=[Literal(value) for value in parameter_value]
48-
)
49-
o = parameter_value.uri
50-
51-
# empty the list of the unused default value while the variable is still
52-
# in scope
53-
default_value.clear()
73+
for parameter_ref in graph.subjects(RDF.type, SC.Parameter):
74+
parameter = Parameter.from_graph(parameter_ref, graph)
75+
76+
inner_type_is_primitive = parameter.inner_type in _ALLOWED_INNER_TYPES
77+
78+
config_parameter = config_parameters.get(parameter.config_path)
79+
80+
if not inner_type_is_primitive and parameter.inner_type != RDFS.Resource:
81+
raise ValueError(
82+
f"Parameter '{parameter.uri}' has unknown inner type "
83+
f"'{parameter.inner_type}'"
84+
)
85+
86+
if parameter.outer_type == RDF.List:
87+
o = _create_rdf_list_parameter(parameter, graph, config_parameter)
88+
89+
elif parameter.outer_type in (RDF.Seq, RDF.Bag, RDF.Alt):
90+
raise NotImplementedError(
91+
f"Parameter '{parameter.uri}' has outer type "
92+
f"'{parameter.outer_type}', the handling of which "
93+
"is currently not implemented"
94+
)
95+
96+
elif parameter.outer_type == SC.Scalar:
97+
o = _create_sc_scalar_parameter(parameter, graph, config_parameter)
5498

5599
else:
56-
# load parameter from config by its name
57-
parameter_value = config_parameters.get(parameter_name)
58-
assert isinstance(parameter_value, (str, int, float, type(None)))
59-
o = Literal(parameter_value) if parameter_value else default_value
100+
raise ValueError(
101+
f"'Parameter {parameter.uri}' has unknown outer type "
102+
f"'{parameter.outer_type}'"
103+
)
60104

61105
# add replacements for all occurences of the parameter
62-
for s, p in graph.subject_predicates(parameter):
106+
for s, p in graph.subject_predicates(parameter.uri):
63107
graph.add((s, p, o))
64108

65109
# remove all references to the parameter from the graph
66-
# TODO: Keep all `(parameter, None, None)` for debugging purposes?
67-
graph.remove((parameter, None, None))
68-
graph.remove((None, None, parameter))
110+
# TODO: Keep all `(parameter.uri, None, None)` for debugging purposes?
111+
graph.remove((parameter.uri, None, None))
112+
graph.remove((None, None, parameter.uri))
69113

70114
return graph
71115

src/sc_validate/report.py

Lines changed: 2 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,11 @@
22
# SPDX-License-Identifier: Apache-2.0
33
# SPDX-FileContributor: David Pape
44

5-
from dataclasses import dataclass
6-
from enum import Enum
7-
from typing import List
8-
95
from jinja2 import Environment, PackageLoader, select_autoescape
10-
from rdflib import Graph, Literal
6+
from rdflib import Graph
117
from rdflib.namespace import RDF, SH
12-
from rdflib.term import URIRef
13-
14-
15-
# TODO: This only works for constraints of type NodeShape. Is this enough?
16-
@dataclass
17-
class Policy:
18-
name: str
19-
description: str
20-
21-
@classmethod
22-
def from_graph(cls, reference: URIRef, graph: Graph):
23-
name = graph.value(reference, SH.name, None)
24-
description = graph.value(reference, SH.description, None)
25-
return cls(name=name, description=description)
26-
27-
28-
class Severity(Enum):
29-
INFO = 1
30-
WARNING = 2
31-
VIOLATION = 3
32-
OTHER = 4
33-
34-
def __str__(self):
35-
return self.name.title()
36-
37-
@classmethod
38-
def from_graph(cls, reference: URIRef, graph: Graph):
39-
if reference == SH.Info:
40-
return cls.INFO
41-
if reference == SH.Warning:
42-
return cls.WARNING
43-
if reference == SH.Violation:
44-
return cls.VIOLATION
45-
return cls.OTHER
46-
47-
48-
@dataclass
49-
class ValidationResult:
50-
severity: Severity
51-
message: str
52-
source_policy: Policy
53-
54-
@classmethod
55-
def from_graph(cls, reference: URIRef, graph: Graph):
56-
severity = graph.value(reference, SH.resultSeverity, None)
57-
message = graph.value(reference, SH.resultMessage, None)
58-
source_policy = graph.value(reference, SH.sourceShape, None)
59-
return cls(
60-
severity=Severity.from_graph(severity, graph),
61-
message=message,
62-
source_policy=Policy.from_graph(source_policy, graph),
63-
)
64-
65-
66-
@dataclass
67-
class ValidationReport:
68-
conforms: bool
69-
results: List[ValidationResult]
708

71-
@classmethod
72-
def from_graph(cls, reference: URIRef, graph: Graph):
73-
conforms = (reference, SH.conforms, Literal(True)) in graph
74-
results = graph.objects(reference, SH.result)
75-
return cls(
76-
conforms=conforms,
77-
results=[ValidationResult.from_graph(result, graph) for result in results],
78-
)
9+
from sc_validate.data_model import ValidationReport
7910

8011

8112
def create_report(validation_graph: Graph, debug=False) -> str:

0 commit comments

Comments
 (0)