Repository: spdx/tools-python

Issue:
Use document SPDX version as default for validation
An optional `version` parameter can be given to the document validator to overwrite the document version. In case of a discrepancy, a validation error will be logged.

Discussion:

Patch:
diff --git a/src/validation/creation_info_validator.py b/src/validation/creation_info_validator.py
index 5f7a427..50446f8 100644
--- a/src/validation/creation_info_validator.py
+++ b/src/validation/creation_info_validator.py
@@ -24,14 +24,6 @@ def validate_creation_info(creation_info: CreationInfo) -> List[ValidationMessag
 
     context = ValidationContext(spdx_id=creation_info.spdx_id, element_type=SpdxElementType.DOCUMENT)
 
-    if not re.match(r"^SPDX-\d+.\d+$", creation_info.spdx_version):
-        validation_messages.append(
-            ValidationMessage(
-                f'spdx_version must be of the form "SPDX-[major].[minor]" but is: {creation_info.spdx_version}',
-                context
-            )
-        )
-
     if creation_info.spdx_id != "SPDXRef-DOCUMENT":
         validation_messages.append(
             ValidationMessage(
diff --git a/src/validation/document_validator.py b/src/validation/document_validator.py
index bf2c114..cab7b09 100644
--- a/src/validation/document_validator.py
+++ b/src/validation/document_validator.py
@@ -8,7 +8,7 @@
 #  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 re
 from typing import List
 
 from src.model.document import Document
@@ -24,9 +24,34 @@ from src.validation.snippet_validator import validate_snippets
 from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType
 
 
-def validate_full_spdx_document(document: Document, spdx_version: str) -> List[ValidationMessage]:
+def validate_full_spdx_document(document: Document, spdx_version: str = None) -> List[ValidationMessage]:
     validation_messages: List[ValidationMessage] = []
 
+    # SPDX version validation has to happen here because subsequent validators rely on it
+    document_version: str = document.creation_info.spdx_version
+    context = ValidationContext(spdx_id=document.creation_info.spdx_id, element_type=SpdxElementType.DOCUMENT)
+    if not spdx_version:
+        spdx_version = document_version
+
+    if not re.match(r"^SPDX-\d+.\d+$", document_version):
+        validation_messages.append(
+            ValidationMessage(
+                f'the document\'s spdx_version must be of the form "SPDX-[major].[minor]" but is: {document_version}',
+                context
+            )
+        )
+    elif spdx_version != document_version:
+        validation_messages.append(
+            ValidationMessage(f"provided SPDX version {spdx_version} does not match "
+                              f"the document's SPDX version {document_version}", context)
+        )
+
+    if validation_messages:
+        validation_messages.append(ValidationMessage("There are issues concerning the SPDX version of the document. "
+                                                     "As subsequent validation relies on the correct version, "
+                                                     "the validation process has been cancelled.", context))
+        return validation_messages
+
     validation_messages.extend(validate_creation_info(document.creation_info))
     validation_messages.extend(validate_packages(document.packages, document))
     validation_messages.extend(validate_files(document.files, document))
diff --git a/src/validation/relationship_validator.py b/src/validation/relationship_validator.py
index 87da7f9..bace552 100644
--- a/src/validation/relationship_validator.py
+++ b/src/validation/relationship_validator.py
@@ -43,9 +43,9 @@ def validate_relationship(relationship: Relationship, document: Document, spdx_v
         for message in messages:
             validation_messages.append(ValidationMessage(message, context))
 
-    if spdx_version != "2.3":
+    if spdx_version != "SPDX-2.3":
         if relationship_type == RelationshipType.SPECIFICATION_FOR or relationship_type == RelationshipType.REQUIREMENT_DESCRIPTION_FOR:
             validation_messages.append(
-                ValidationMessage(f"{relationship_type} is not supported for SPDX versions below 2.3", context))
+                ValidationMessage(f"{relationship_type} is not supported for SPDX versions below SPDX-2.3", context))
 
     return validation_messages
diff --git a/src/writer/json/json_writer.py b/src/writer/json/json_writer.py
index 5083c8c..fbde9ad 100644
--- a/src/writer/json/json_writer.py
+++ b/src/writer/json/json_writer.py
@@ -24,8 +24,7 @@ def write_document(document: Document, file_name: str, validate: bool = True, co
     a new one is created.
     """
     if validate:
-        validation_messages: List[ValidationMessage] = validate_full_spdx_document(document,
-                                                                                   document.creation_info.spdx_version)
+        validation_messages: List[ValidationMessage] = validate_full_spdx_document(document)
         if validation_messages:
             raise ValueError(f"Document is not valid. The following errors were detected: {validation_messages}")
     if converter is None:


Test patch:
diff --git a/tests/validation/test_creation_info_validator.py b/tests/validation/test_creation_info_validator.py
index 5de0416..8f0d661 100644
--- a/tests/validation/test_creation_info_validator.py
+++ b/tests/validation/test_creation_info_validator.py
@@ -27,9 +27,7 @@ def test_valid_creation_info():
 
 @pytest.mark.parametrize \
         ("creation_info_input, spdx_id, expected_message",
-         [(creation_info_fixture(spdx_version="version-2.3"), "SPDXRef-DOCUMENT",
-           'spdx_version must be of the form "SPDX-[major].[minor]" but is: version-2.3'),
-          (creation_info_fixture(spdx_id="SPDXRef-doc"), "SPDXRef-doc",
+         [(creation_info_fixture(spdx_id="SPDXRef-doc"), "SPDXRef-doc",
            'spdx_id must be "SPDXRef-DOCUMENT", but is: SPDXRef-doc'),
           (creation_info_fixture(data_license="MIT"), "SPDXRef-DOCUMENT",
            'data_license must be "CC0-1.0", but is: MIT'),
diff --git a/tests/validation/test_document_validator.py b/tests/validation/test_document_validator.py
index 637c6ad..a7f2d7a 100644
--- a/tests/validation/test_document_validator.py
+++ b/tests/validation/test_document_validator.py
@@ -9,17 +9,50 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from typing import List
+from typing import List, Optional
 
+import pytest
+
+from src.model.document import Document, CreationInfo
 from src.validation.document_validator import validate_full_spdx_document
-from src.validation.validation_message import ValidationMessage
-from tests.fixtures import document_fixture
+from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType
+from tests.fixtures import document_fixture, creation_info_fixture
 
 
 def test_valid_document():
     document = document_fixture()
-    validation_messages: List[ValidationMessage] = validate_full_spdx_document(document, "2.3")
+    validation_messages: List[ValidationMessage] = validate_full_spdx_document(document)
 
     assert validation_messages == []
 
-# TODO: https://github.com/spdx/tools-python/issues/375
+
+@pytest.mark.parametrize("creation_info, version_input, expected_message",
+                         [(creation_info_fixture(spdx_version="SPDX-2.3"), "SPDX-2.3", None),
+                          (creation_info_fixture(spdx_version="SPDX-2.3"), None, None),
+                          (creation_info_fixture(spdx_version="SPDX-2.3"), "SPDX-2.2",
+                           "provided SPDX version SPDX-2.2 does not match the document's SPDX version SPDX-2.3"),
+                          (creation_info_fixture(spdx_version="SPDX-2.3"), "SPDX2.3",
+                           "provided SPDX version SPDX2.3 does not match the document's SPDX version SPDX-2.3"),
+                          (creation_info_fixture(spdx_version="SPDX2.3"), "SPDX-2.3",
+                           'the document\'s spdx_version must be of the form "SPDX-[major].[minor]" but is: SPDX2.3'),
+                          (creation_info_fixture(spdx_version="SPDX2.3"), None,
+                           'the document\'s spdx_version must be of the form "SPDX-[major].[minor]" but is: SPDX2.3'),
+                          (creation_info_fixture(spdx_version="SPDX2.3"), "SPDX2.3",
+                           'the document\'s spdx_version must be of the form "SPDX-[major].[minor]" but is: SPDX2.3'),
+                          ])
+def test_spdx_version_handling(creation_info: CreationInfo, version_input: str, expected_message: Optional[str]):
+    document: Document = document_fixture(creation_info=creation_info)
+    validation_messages: List[ValidationMessage] = validate_full_spdx_document(document, version_input)
+
+    context = ValidationContext(spdx_id=creation_info.spdx_id, element_type=SpdxElementType.DOCUMENT)
+    expected: List[ValidationMessage] = []
+
+    if expected_message:
+        expected.append(ValidationMessage(expected_message, context))
+        expected.append(ValidationMessage("There are issues concerning the SPDX version of the document. "
+                                          "As subsequent validation relies on the correct version, "
+                                          "the validation process has been cancelled.", context))
+
+    assert validation_messages == expected
+
+    # TODO: https://github.com/spdx/tools-python/issues/375
diff --git a/tests/validation/test_relationship_validator.py b/tests/validation/test_relationship_validator.py
index 7066555..1d164e2 100644
--- a/tests/validation/test_relationship_validator.py
+++ b/tests/validation/test_relationship_validator.py
@@ -40,7 +40,7 @@ def test_valid_relationship(related_spdx_element):
 def test_unknown_spdx_id(spdx_element_id, related_spdx_element_id, expected_message):
     relationship: Relationship = relationship_fixture(spdx_element_id=spdx_element_id,
                                                       related_spdx_element_id=related_spdx_element_id)
-    validation_messages: List[ValidationMessage] = validate_relationship(relationship, document_fixture(), "2.3")
+    validation_messages: List[ValidationMessage] = validate_relationship(relationship, document_fixture(), "SPDX-2.3")
 
     expected = ValidationMessage(expected_message,
                                  ValidationContext(element_type=SpdxElementType.RELATIONSHIP,
@@ -51,14 +51,14 @@ def test_unknown_spdx_id(spdx_element_id, related_spdx_element_id, expected_mess
 
 @pytest.mark.parametrize("relationship, expected_message",
                          [(Relationship("SPDXRef-DOCUMENT", RelationshipType.SPECIFICATION_FOR, "SPDXRef-Package"),
-                           "RelationshipType.SPECIFICATION_FOR is not supported for SPDX versions below 2.3"),
+                           "RelationshipType.SPECIFICATION_FOR is not supported for SPDX versions below SPDX-2.3"),
                           (Relationship("SPDXRef-DOCUMENT", RelationshipType.REQUIREMENT_DESCRIPTION_FOR,
                                         "SPDXRef-Package"),
-                           "RelationshipType.REQUIREMENT_DESCRIPTION_FOR is not supported for SPDX versions below 2.3")])
+                           "RelationshipType.REQUIREMENT_DESCRIPTION_FOR is not supported for SPDX versions below SPDX-2.3")])
 def test_v2_3_only_types(relationship, expected_message):
     document: Document = document_fixture()
 
-    validation_message: List[ValidationMessage] = validate_relationship(relationship, document, "2.2")
+    validation_message: List[ValidationMessage] = validate_relationship(relationship, document, "SPDX-2.2")
 
     expected = [ValidationMessage(expected_message,
                                   ValidationContext(element_type=SpdxElementType.RELATIONSHIP,