Repository: spdx/tools-python

Issue:
relationship validation must allow NONE and NOASSERTION
The spec explicitly allows the keywords `NOASSERTION` or `NONE` on the right hand side of a relationship (see [here](https://spdx.github.io/spdx-spec/v2.3/relationships-between-SPDX-elements/)). When trying to parse the example file `SPDXJSONExample-v2.3.spdx.json` the validation returns the following messages:
```
ValidationMessage(validation_message='spdx_id must only contain letters, numbers, "." and "-" and must begin with "SPDXRef-", but is: NOASSERTION', context=ValidationContext(spdx_id=None, parent_id=None, element_type=<SpdxElementType.RELATIONSHIP: 13>, full_element=Relationship(spdx_element_id='SPDXRef-CommonsLangSrc', relationship_type=<RelationshipType.GENERATED_FROM: 25>, related_spdx_element_id='NOASSERTION', comment=None))), 
ValidationMessage(validation_message='did not find the referenced spdx_id NOASSERTION in the SPDX document', context=ValidationContext(spdx_id=None, parent_id=None, element_type=<SpdxElementType.RELATIONSHIP: 13>, full_element=Relationship(spdx_element_id='SPDXRef-CommonsLangSrc', relationship_type=<RelationshipType.GENERATED_FROM: 25>, related_spdx_element_id='NOASSERTION', comment=None)))
```


Discussion:

Patch:
diff --git a/src/model/relationship.py b/src/model/relationship.py
index 1b34fe0..88026c9 100644
--- a/src/model/relationship.py
+++ b/src/model/relationship.py
@@ -9,8 +9,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 from enum import auto, Enum
-from typing import Optional
+from typing import Optional, Union
 
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.model.spdx_none import SpdxNone
 from src.model.typing.dataclass_with_properties import dataclass_with_properties
 from src.model.typing.type_checks import check_types_and_set_values
 
@@ -67,9 +69,9 @@ class RelationshipType(Enum):
 class Relationship:
     spdx_element_id: str
     relationship_type: RelationshipType
-    related_spdx_element_id: str
+    related_spdx_element_id: Union[str, SpdxNone, SpdxNoAssertion]
     comment: Optional[str] = None
 
-    def __init__(self, spdx_element_id: str, relationship_type: RelationshipType, related_spdx_element_id: str,
-                 comment: Optional[str] = None):
+    def __init__(self, spdx_element_id: str, relationship_type: RelationshipType,
+                 related_spdx_element_id: Union[str, SpdxNone, SpdxNoAssertion], comment: Optional[str] = None):
         check_types_and_set_values(self, locals())
diff --git a/src/parser/json/relationship_parser.py b/src/parser/json/relationship_parser.py
index defecd3..e2c910e 100644
--- a/src/parser/json/relationship_parser.py
+++ b/src/parser/json/relationship_parser.py
@@ -15,7 +15,7 @@ from src.model.typing.constructor_type_errors import ConstructorTypeErrors
 from src.parser.error import SPDXParsingError
 from src.parser.json.dict_parsing_functions import raise_parsing_error_if_logger_has_messages, json_str_to_enum_name, \
     construct_or_raise_parsing_error, \
-    parse_field_or_log_error, parse_list_of_elements
+    parse_field_or_log_error, parse_field_or_no_assertion_or_none
 from src.parser.logger import Logger
 
 
@@ -58,7 +58,7 @@ class RelationshipParser:
     def parse_relationship(self, relationship_dict: Dict) -> Relationship:
         logger = Logger()
         spdx_element_id: Optional[str] = relationship_dict.get("spdxElementId")
-        related_spdx_element: Optional[str] = relationship_dict.get("relatedSpdxElement")
+        related_spdx_element: Optional[str] = parse_field_or_no_assertion_or_none(relationship_dict.get("relatedSpdxElement"))
         relationship_type: Optional[RelationshipType] = parse_field_or_log_error(logger, relationship_dict.get(
             "relationshipType"), self.parse_relationship_type)
         relationship_comment: Optional[str] = relationship_dict.get("comment")
diff --git a/src/validation/relationship_validator.py b/src/validation/relationship_validator.py
index a929225..edf8754 100644
--- a/src/validation/relationship_validator.py
+++ b/src/validation/relationship_validator.py
@@ -9,10 +9,12 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from typing import List
+from typing import List, Union
 
 from src.model.document import Document
 from src.model.relationship import Relationship, RelationshipType
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.model.spdx_none import SpdxNone
 from src.validation.spdx_id_validators import validate_spdx_id
 from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType
 
@@ -30,12 +32,14 @@ def validate_relationship(relationship: Relationship, document: Document, spdx_v
     context = ValidationContext(element_type=SpdxElementType.RELATIONSHIP,
                                 full_element=relationship)
 
-    first_id: str = relationship.spdx_element_id
-    second_id: str = relationship.related_spdx_element_id
     relationship_type: RelationshipType = relationship.relationship_type
 
-    for spdx_id in [first_id, second_id]:
-        messages: List[str] = validate_spdx_id(spdx_id, document, check_document=True)
+    messages: List[str] = validate_spdx_id(relationship.spdx_element_id, document, check_document=True)
+    for message in messages:
+        validation_messages.append(ValidationMessage(message, context))
+
+    if relationship.related_spdx_element_id not in [SpdxNone(), SpdxNoAssertion()]:
+        messages: List[str] = validate_spdx_id(relationship.related_spdx_element_id, document, check_document=True)
         for message in messages:
             validation_messages.append(ValidationMessage(message, context))
 


Test patch:
diff --git a/tests/model/test_relationship.py b/tests/model/test_relationship.py
index 4d0a0a6..c3b99a6 100644
--- a/tests/model/test_relationship.py
+++ b/tests/model/test_relationship.py
@@ -1,19 +1,20 @@
 import pytest
 
 from src.model.relationship import Relationship, RelationshipType
+from src.model.spdx_no_assertion import SpdxNoAssertion
 
 
 def test_correct_initialization():
-    relationship = Relationship("id", RelationshipType.OTHER, "other_id", "comment")
+    relationship = Relationship("id", RelationshipType.OTHER, SpdxNoAssertion(), "comment")
     assert relationship.spdx_element_id == "id"
     assert relationship.relationship_type == RelationshipType.OTHER
-    assert relationship.related_spdx_element_id == "other_id"
+    assert relationship.related_spdx_element_id == SpdxNoAssertion()
     assert relationship.comment == "comment"
 
 
 def test_wrong_type_in_spdx_element_id():
     with pytest.raises(TypeError):
-        Relationship(42, RelationshipType.OTHER, "other_id")
+        Relationship(SpdxNoAssertion(), RelationshipType.OTHER, "other_id")
 
 
 def test_wrong_type_in_relationship_type():
diff --git a/tests/parser/test_relationship_parser.py b/tests/parser/test_relationship_parser.py
index d1afa64..27f2c4f 100644
--- a/tests/parser/test_relationship_parser.py
+++ b/tests/parser/test_relationship_parser.py
@@ -13,6 +13,7 @@ from unittest import TestCase
 import pytest
 
 from src.model.relationship import RelationshipType, Relationship
+from src.model.spdx_no_assertion import SpdxNoAssertion
 from src.parser.error import SPDXParsingError
 from src.parser.json.relationship_parser import RelationshipParser
 
@@ -23,7 +24,7 @@ def test_parse_relationship():
     relationship_dict = {
         "spdxElementId": "SPDXRef-DOCUMENT",
         "relationshipType": "CONTAINS",
-        "relatedSpdxElement": "SPDXRef-Package",
+        "relatedSpdxElement": "NOASSERTION",
         "comment": "Comment."
     }
 
@@ -31,7 +32,7 @@ def test_parse_relationship():
 
     assert relationship.relationship_type == RelationshipType.CONTAINS
     assert relationship.spdx_element_id == "SPDXRef-DOCUMENT"
-    assert relationship.related_spdx_element_id == "SPDXRef-Package"
+    assert relationship.related_spdx_element_id == SpdxNoAssertion()
     assert relationship.comment == "Comment."
 
 
diff --git a/tests/validation/test_relationship_validator.py b/tests/validation/test_relationship_validator.py
index 1d56bd6..2e2a08b 100644
--- a/tests/validation/test_relationship_validator.py
+++ b/tests/validation/test_relationship_validator.py
@@ -15,15 +15,19 @@ import pytest
 
 from src.model.document import Document
 from src.model.relationship import Relationship, RelationshipType
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.model.spdx_none import SpdxNone
 from src.validation.relationship_validator import validate_relationship
 from src.validation.validation_message import ValidationMessage, SpdxElementType, ValidationContext
 from tests.valid_defaults import get_document, get_package, get_relationship, get_file
 
 
-def test_valid_relationship():
+@pytest.mark.parametrize("related_spdx_element",
+                         ["SPDXRef-Package", SpdxNoAssertion(), SpdxNone()])
+def test_valid_relationship(related_spdx_element):
     document: Document = get_document(packages=[get_package(spdx_id="SPDXRef-Package")])
 
-    relationship = Relationship("SPDXRef-DOCUMENT", RelationshipType.AMENDS, "SPDXRef-Package", comment="comment")
+    relationship = Relationship("SPDXRef-DOCUMENT", RelationshipType.AMENDS, related_spdx_element, comment="comment")
     validation_messages: List[ValidationMessage] = validate_relationship(relationship, document, "2.3")
 
     assert validation_messages == []