diff --git a/rocrate_validator/profiles/five-safes-crate/must/6_workflow_reference.ttl b/rocrate_validator/profiles/five-safes-crate/must/6_workflow_reference.ttl new file mode 100644 index 000000000..647774252 --- /dev/null +++ b/rocrate_validator/profiles/five-safes-crate/must/6_workflow_reference.ttl @@ -0,0 +1,80 @@ +# Copyright (c) 2025 eScience Lab, The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix five-safes-crate: . +@prefix rdf: . +@prefix schema: . +@prefix purl: . +@prefix sh: . +@prefix validator: . +@prefix xsd: . + + + +five-safes-crate:ReferenceToWorkflowCrate + a sh:NodeShape ; + sh:name "RootDataEntity" ; + sh:targetClass ro-crate:RootDataEntity ; + + # RootDataEntity MUST have an mainEntity property + sh:property [ + a sh:PropertyShape ; + sh:name "mainEntity" ; + sh:path schema:mainEntity ; + sh:minCount 1 ; + sh:maxCount 1 ; + sh:nodeKind sh:IRI ; + sh:severity sh:Violation ; + sh:message "The RootDataEntity MUST have exactly one schema:mainEntity property that is an IRI." ; + ] ; + + # The mainEntity of a RootDataEntity MUST be a Dataset + sh:property [ + a sh:PropertyShape ; + sh:name "mainEntity" ; + sh:path schema:mainEntity ; + sh:class schema:Dataset ; + sh:severity sh:Violation ; + sh:message "The mainEntity pointed to by the RootDataEntity MUST be of type schema:Dataset" ; + ] . + +five-safes-crate:mainEntityHasProperConformsTo + a sh:NodeShape ; + sh:name "mainEntity" ; + sh:description "The mainEntity of the RootDataEntity MUST have a conformsTo property with an IRI starting with https://w3id.org/workflowhub/workflow-ro-crate" ; + sh:targetObjectsOf schema:mainEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "conformsTo" ; + sh:path purl:conformsTo ; + sh:minCount 1 ; + sh:maxCount 1 ; + sh:severity sh:Violation ; + sh:message "mainEntity MUST have one and only one `purl:conformsTo` property." ; + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:name "conformsTo" ; + sh:message "conformsTo IRI must start with https://w3id.org/workflowhub/workflow-ro-crate" ; + sh:select """ + PREFIX schema: + PREFIX purl: + SELECT $this WHERE { + $this purl:conformsTo ?iri . + FILTER(!STRSTARTS(STR(?iri), "https://w3id.org/workflowhub/workflow-ro-crate")) + } + """ ; + ] . diff --git a/rocrate_validator/profiles/five-safes-crate/should/6_workflow_reference.ttl b/rocrate_validator/profiles/five-safes-crate/should/6_workflow_reference.ttl new file mode 100644 index 000000000..98c285652 --- /dev/null +++ b/rocrate_validator/profiles/five-safes-crate/should/6_workflow_reference.ttl @@ -0,0 +1,49 @@ +# Copyright (c) 2025 eScience Lab, The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix five-safes-crate: . +@prefix rdf: . +@prefix schema: . +@prefix purl: . +@prefix sh: . +@prefix validator: . +@prefix xsd: . + + +five-safes-crate:DatasetMustHaveDistributionIfURI + a sh:NodeShape ; + sh:name "mainEntity" ; + sh:targetObjectsOf schema:mainEntity ; + sh:description "If mainEntity has an HTTP(S) @id, it SHOULD have a distribution that is an HTTP(S) URL." ; + + sh:sparql [ + a sh:SPARQLConstraint ; + sh:name "distribution" ; + sh:description "If mainEntity has an HTTP(S) @id, it SHOULD have a distribution that is an HTTP(S) URL." ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT $this + WHERE { + FILTER (STRSTARTS(STR($this), "http://") || STRSTARTS(STR($this), "https://")) . + FILTER NOT EXISTS { + $this schema:distribution ?dist . + FILTER (STRSTARTS(STR(?dist), "http://") || STRSTARTS(STR(?dist), "https://")) . + } + } + """ ; + sh:severity sh:Warning ; + sh:message "If mainEntity has an HTTP(S) @id SHOULD have at least one distribution with an HTTP(S) URL." ; + ] . \ No newline at end of file diff --git a/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py b/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py new file mode 100644 index 000000000..145f16c77 --- /dev/null +++ b/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py @@ -0,0 +1,257 @@ +# Copyright (c) 2024-2025 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import ValidROC +from tests.shared import do_entity_test, SPARQL_PREFIXES + +# set up logging +logger = logging.getLogger(__name__) + + +# ----- MUST fails tests + + +def test_5src_root_data_entity_two_main_entities(): + """ + Add a 2nd RootDataEntity's mainEntity so maxCount=1 is violated. + """ + sparql = ( + SPARQL_PREFIXES + + """ + INSERT { + # add an IRI that is NOT typed as schema:Dataset (e.g. a schema:SoftwareSourceCode) + <./> schema:mainEntity . + a schema:Dataset . + } + WHERE { + <./> schema:mainEntity ?m . + } + """ + ) + + do_entity_test( + rocrate_path=ValidROC().five_safes_crate_request, + requirement_severity=Severity.REQUIRED, + expected_validation_result=False, + expected_triggered_requirements=["RootDataEntity"], + expected_triggered_issues=[ + "The RootDataEntity MUST have exactly one schema:mainEntity property that is an IRI." + ], + profile_identifier="five-safes-crate", + rocrate_entity_mod_sparql=sparql, + ) + + +def test_5src_root_data_entity_no_main_entity(): + """ + Remove the RootDataEntity's mainEntity so minCount=1 is violated. + """ + sparql = ( + SPARQL_PREFIXES + + """ + DELETE { + <./> schema:mainEntity ?m . + } + WHERE { + <./> schema:mainEntity ?m . + } + """ + ) + + do_entity_test( + rocrate_path=ValidROC().five_safes_crate_request, + requirement_severity=Severity.REQUIRED, + expected_validation_result=False, + expected_triggered_requirements=["RootDataEntity"], + expected_triggered_issues=[ + "The RootDataEntity MUST have exactly one schema:mainEntity property that is an IRI." + ], + profile_identifier="five-safes-crate", + rocrate_entity_mod_sparql=sparql, + ) + + +def test_5src_root_data_entity_main_entity_not_dataset_iri(): + """ + Test a Five Safes Crate where the RootDataEntity's mainEntity is an IRI but not typed as schema:Dataset. + (We point mainEntity to a new crate-local entity typed as something else.) + """ + sparql = ( + SPARQL_PREFIXES + + """ + DELETE { + <./> schema:mainEntity ?m . + } + INSERT { + # add an IRI that is NOT typed as schema:Dataset (e.g. a schema:SoftwareSourceCode) + <./> schema:mainEntity <./not-a-dataset> . + <./not-a-dataset> a schema:SoftwareSourceCode . + } + WHERE { + <./> schema:mainEntity ?m . + } + """ + ) + + do_entity_test( + rocrate_path=ValidROC().five_safes_crate_request, + requirement_severity=Severity.REQUIRED, + expected_validation_result=False, + expected_triggered_requirements=["RootDataEntity"], + expected_triggered_issues=[ + "The mainEntity pointed to by the RootDataEntity MUST be of type schema:Dataset" + ], + profile_identifier="five-safes-crate", + rocrate_entity_mod_sparql=sparql, + ) + + +def test_5src_main_entity_conformsTo_absent(): + """ + Test a Five Safes Crate where the mainEntity does not have the purl:conformsTo property + (we remove from the graph the triplet 'mainEntity conformsTo ?o'). + """ + sparql = ( + SPARQL_PREFIXES + + """ + PREFIX purl: + DELETE { + ?dataset purl:conformsTo ?o . + } + WHERE { + <./> schema:mainEntity ?dataset . + ?dataset purl:conformsTo ?o . + } + """ + ) + + do_entity_test( + rocrate_path=ValidROC().five_safes_crate_request, + requirement_severity=Severity.REQUIRED, + expected_validation_result=False, + expected_triggered_requirements=["mainEntity"], + expected_triggered_issues=[ + "mainEntity MUST have one and only one `purl:conformsTo` property." + ], + profile_identifier="five-safes-crate", + rocrate_entity_mod_sparql=sparql, + ) + + +def test_5src_main_entity_has_two_conformsto(): + """ + Test a Five Safes Crate where the mainEntity -> purl:conformsTo has two + objects. + (we add second obcject to mainEntity -> purl:conformsTo to violate maxCount 1). + """ + sparql = ( + SPARQL_PREFIXES + + """ + PREFIX purl: + INSERT { + ?main purl:conformsTo . + } + WHERE { + <./> schema:mainEntity ?main . + ?main purl:conformsTo ?o . + } + """ + ) + + do_entity_test( + rocrate_path=ValidROC().five_safes_crate_request, + requirement_severity=Severity.REQUIRED, + expected_validation_result=False, + expected_triggered_requirements=["mainEntity"], + expected_triggered_issues=[ + "mainEntity MUST have one and only one `purl:conformsTo` property." + ], + profile_identifier="five-safes-crate", + rocrate_entity_mod_sparql=sparql, + ) + + +def test_5src_main_entity_conformsTo_invalid(): + """ + Test a Five Safes Crate where the mainEntity's purl:conformsTo IRI does NOT start with + "https://w3id.org/workflowhub/workflow-ro-crate" (violates the SHACL SPARQL constraint). + """ + sparql = ( + SPARQL_PREFIXES + + """ + PREFIX purl: + DELETE { + ?dataset purl:conformsTo ?iri . + } + INSERT { + ?dataset purl:conformsTo . + } + WHERE { + <./> schema:mainEntity ?dataset . + ?dataset purl:conformsTo ?iri . + FILTER(STRSTARTS(STR(?iri), "https://w3id.org/workflowhub/workflow-ro-crate")) + } + """ + ) + + do_entity_test( + rocrate_path=ValidROC().five_safes_crate_request, + requirement_severity=Severity.REQUIRED, + expected_validation_result=False, + expected_triggered_requirements=["mainEntity"], + expected_triggered_issues=[ + "conformsTo IRI must start with https://w3id.org/workflowhub/workflow-ro-crate" + ], + profile_identifier="five-safes-crate", + rocrate_entity_mod_sparql=sparql, + ) + + +# ----- SHOULD fails tests + + +def test_5src_main_entity_missing_distribution_warning(): + """ + Test a Five Safes Crate where a mainEntity has an HTTP(S) IRI but no distribution with an HTTP(S) URL. + This should trigger the SHACL warning about missing or non-HTTP(S) distributions. + """ + sparql = ( + SPARQL_PREFIXES + + """ + DELETE { + ?dataset schema:distribution ?dist . + } + WHERE { + <./> schema:mainEntity ?dataset . + ?dataset schema:distribution ?dist . + FILTER (STRSTARTS(STR(?dataset), "http://") || STRSTARTS(STR(?dataset), "https://")) . + FILTER (STRSTARTS(STR(?dist), "http://") || STRSTARTS(STR(?dist), "https://")) . + } + """ + ) + + do_entity_test( + rocrate_path=ValidROC().five_safes_crate_request, + requirement_severity=Severity.RECOMMENDED, + expected_validation_result=False, + expected_triggered_requirements=["mainEntity"], + expected_triggered_issues=[ + "If mainEntity has an HTTP(S) @id SHOULD have at least one distribution with an HTTP(S) URL." + ], + profile_identifier="five-safes-crate", + rocrate_entity_mod_sparql=sparql, + )