import rdflib from pyshacl import validate import click from pathlib import Path import requests import re prefix_mapping = {} def replace_prefixes(value): if isinstance(value, str): for full_uri, prefix in prefix_mapping.items(): value = value.replace(full_uri, prefix) return value def print_validation_results(report, results, indent=0): violation_count = 0 for result in results: severity = replace_prefixes(report.value(result, rdflib.SH["resultSeverity"])) source_shape = replace_prefixes(report.value(result, rdflib.SH["sourceShape"])) focus_node = replace_prefixes(report.value(result, rdflib.SH["focusNode"])) value_node = replace_prefixes(report.value(result, rdflib.SH["value"])) result_path = replace_prefixes(report.value(result, rdflib.SH["resultPath"])) message = replace_prefixes(report.value(result, rdflib.SH["resultMessage"])) violation_count += 1 click.secho(f"-" * 100) if indent > 0: click.secho(f"{(indent - 2) * ' '}Details:") click.secho(f"{indent * ' '}Severity: {severity}") click.secho(f"{indent * ' '}Source Shape: {source_shape}") click.secho(f"{indent * ' '}Focus Node: {focus_node}") click.secho(f"{indent * ' '}Value Node: {value_node}") click.secho(f"{indent * ' '}Result Path: {result_path}") click.secho(f"{indent * ' '}Message: {message}") details = [*report.objects(result, rdflib.SH["detail"])] if details: print_validation_results(report, details, indent + 4) return violation_count def validate_shacl(model_graph, shapes_graph, data_graph, inference): conforms, report, message = validate( data_graph=data_graph, shacl_graph=shapes_graph, ont_graph=model_graph, inference=inference, ) if isinstance(report, rdflib.Graph): results = report.subjects(rdflib.RDF.type, rdflib.SH["ValidationResult"]) if conforms: click.secho("No SHACL violations found!", fg="green") else: violation_count = print_validation_results(report, results) click.secho(f"-" * 100) click.secho(f"Found {violation_count} SHACL violations!", fg="red") else: click.secho("SHACL validation failed!", fg="red") click.secho(f"Error message: {message}", fg="yellow") @click.command() @click.option( "--model", "-m", help="URL to SPDX model", default="https://spdx.github.io/spdx-spec/v3.0.1/rdf/spdx-model.ttl", ) @click.option( "--shapes", "-s", type=click.Path(exists=True), help="Path to SHACL shapes", default="shapes.ttl", ) @click.option( "--data", "-d", type=click.Path(exists=True), help="Path to test data", default="data.ttl", ) @click.option( "--prefixes", "-p", type=click.Path(exists=True), help="Path to prefixes", default="prefixes.ttl", ) @click.option( "--inference", "-i", type=click.Choice(["none", "rdfs", "owlrl", "both"]), help="Inference type {none,rdfs,owlrl,both}", default="none", ) def main(model, shapes, data, prefixes, inference): prefixes_content = Path(prefixes).read_text() model_graph = rdflib.Graph() if re.match(r"^https?://", model): model_content = requests.get(model).text else: model_content = Path(model).read_text() model_graph.parse(data=model_content + prefixes_content, format="turtle") shapes_graph = rdflib.Graph() shapes_content = Path(shapes).read_text() shapes_graph.parse( data=model_content + shapes_content + prefixes_content, format="turtle" ) data_graph = rdflib.Graph() data_content = Path(data).read_text() data_graph.parse(data=data_content + prefixes_content, format="turtle") global prefix_mapping prefix_mapping = {str(ns): prefix + ":" for prefix, ns in data_graph.namespaces()} if inference != "none": s = f"# # # Will perform pre-inferencing of type: {inference} # # #" click.secho("#" * len(s), fg="yellow") click.secho(s, fg="yellow") click.secho("#" * len(s), fg="yellow") validate_shacl(shapes_graph, shapes_graph, data_graph, inference) if __name__ == "__main__": main()