diff --git a/samtranslator/model/connector/connector.py b/samtranslator/model/connector/connector.py index fbde61f31..329e0c4a9 100644 --- a/samtranslator/model/connector/connector.py +++ b/samtranslator/model/connector/connector.py @@ -20,6 +20,8 @@ ], ) +UNSUPPORTED_CONNECTOR_PROFILE_TYPE = "UNSUPPORTED_CONNECTOR_PROFILE_TYPE" + class ConnectorResourceError(Exception): """ diff --git a/samtranslator/model/exceptions.py b/samtranslator/model/exceptions.py index a0d563e7e..2d9b1cbf8 100644 --- a/samtranslator/model/exceptions.py +++ b/samtranslator/model/exceptions.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod +from collections import defaultdict from enum import Enum -from typing import List, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence, Union class ExpectedType(Enum): @@ -16,12 +17,17 @@ class ExceptionWithMessage(ABC, Exception): def message(self) -> str: """Return the exception message.""" + @property + def metadata(self) -> Optional[Dict[str, Any]]: + """Return the exception metadata.""" + class InvalidDocumentException(ExceptionWithMessage): """Exception raised when the given document is invalid and cannot be transformed. Attributes: message -- explanation of the error + metadata -- a dictionary of metadata (key, value pair) causes -- list of errors which caused this document to be invalid """ @@ -37,6 +43,17 @@ def message(self) -> str: len(self.causes) ) + @property + def metadata(self) -> Dict[str, List[Any]]: + # Merge metadata in each exception to one single metadata dictionary + metadata_dict = defaultdict(list) + for cause in self.causes: + if not cause.metadata: + continue + for k, v in cause.metadata.items(): + metadata_dict[k].append(v) + return metadata_dict + @property def causes(self) -> Sequence[ExceptionWithMessage]: return self._causes @@ -86,9 +103,12 @@ class InvalidResourceException(ExceptionWithMessage): message -- explanation of the error """ - def __init__(self, logical_id: Union[str, List[str]], message: str) -> None: + def __init__( + self, logical_id: Union[str, List[str]], message: str, metadata: Optional[Dict[str, Any]] = None + ) -> None: self._logical_id = logical_id self._message = message + self._metadata = metadata def __lt__(self, other): # type: ignore[no-untyped-def] return self._logical_id < other._logical_id @@ -97,6 +117,10 @@ def __lt__(self, other): # type: ignore[no-untyped-def] def message(self) -> str: return "Resource with id [{}] is invalid. {}".format(self._logical_id, self._message) + @property + def metadata(self) -> Optional[Dict[str, Any]]: + return self._metadata + class InvalidResourcePropertyTypeException(InvalidResourceException): def __init__( diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index d87230912..ef7e0d8b1 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -32,6 +32,7 @@ from samtranslator.model.architecture import ARM64, X86_64 from samtranslator.model.cloudformation import NestedStack from samtranslator.model.connector.connector import ( + UNSUPPORTED_CONNECTOR_PROFILE_TYPE, ConnectorResourceError, ConnectorResourceReference, add_depends_on, @@ -1861,6 +1862,7 @@ def generate_resources( raise InvalidResourceException( self.logical_id, f"Unable to create connector from {source.resource_type} to {destination.resource_type}; it's not supported or the template is invalid.", + {UNSUPPORTED_CONNECTOR_PROFILE_TYPE: {source.resource_type: destination.resource_type}}, ) # removing duplicate permissions diff --git a/tests/model/test_exceptions.py b/tests/model/test_exceptions.py new file mode 100644 index 000000000..acf6e0baa --- /dev/null +++ b/tests/model/test_exceptions.py @@ -0,0 +1,64 @@ +from unittest import TestCase + +from samtranslator.model.exceptions import ( + DuplicateLogicalIdException, + InvalidResourceException, + InvalidDocumentException, + InvalidTemplateException, + InvalidEventException, +) + + +class TestExceptions(TestCase): + def setUp(self) -> None: + self.invalid_template = InvalidTemplateException("foo") + self.duplicate_id = DuplicateLogicalIdException("foo", "bar", "type") + self.invalid_resource = InvalidResourceException("foo", "bar") + self.invalid_event = InvalidEventException("foo", "bar") + self.invalid_resource_with_metadata = InvalidResourceException("foo-bar", "foo", {"hello": "world"}) + + def test_invalid_template(self): + self.assertEqual(self.invalid_template.metadata, None) + self.assertIn("Structure of the SAM template is invalid", self.invalid_template.message) + + def test_duplicate_id(self): + self.assertEqual(self.duplicate_id.metadata, None) + self.assertIn("Transforming resource with id", self.duplicate_id.message) + + def test_invalid_resource_without_metadata(self): + self.assertEqual(self.invalid_resource.metadata, None) + self.assertIn("Resource with id [foo] is invalid", self.invalid_resource.message) + + def test_invalid_resource_with_metadata(self): + self.assertEqual(self.invalid_resource_with_metadata.metadata, {"hello": "world"}) + self.assertIn("Resource with id [foo-bar] is invalid", self.invalid_resource_with_metadata.message) + + def test_invalid_event(self): + self.assertEqual(self.invalid_event.metadata, None) + self.assertIn("Event with id [foo] is invalid.", self.invalid_event.message) + + def test_invalid_document_exceptions(self): + unsupported_connector_profile = InvalidResourceException("hello", "world", {"KEY": {"C": "D"}}) + unsupported_connector_profile2 = InvalidResourceException("foobar", "bar", {"KEY": {"A": "B"}}) + self.assertEqual(unsupported_connector_profile.metadata, {"KEY": {"C": "D"}}) + self.assertEqual(unsupported_connector_profile2.metadata, {"KEY": {"A": "B"}}) + + invalid_document_exception = InvalidDocumentException( + [ + self.invalid_template, + self.duplicate_id, + self.invalid_resource, + self.invalid_event, + self.invalid_resource_with_metadata, + unsupported_connector_profile, + unsupported_connector_profile2, + ] + ) + self.assertEqual( + "Invalid Serverless Application Specification document. Number of errors found: 7.", + invalid_document_exception.message, + ) + self.assertEqual( + invalid_document_exception.metadata, + {"hello": ["world"], "KEY": [{"C": "D"}, {"A": "B"}]}, + )