Skip to content
31 changes: 25 additions & 6 deletions samtranslator/model/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,40 @@ class InvalidResourcePropertyTypeException(InvalidResourceException):
def __init__(
self,
logical_id: str,
property_identifier: str,
key_path: str,
expected_type: Optional[ExpectedType],
message: Optional[str] = None,
) -> None:
message = message or self._default_message(property_identifier, expected_type)
message = message or self._default_message(key_path, expected_type)
super().__init__(logical_id, message)

self.property_identifier = property_identifier
self.key_path = key_path

@staticmethod
def _default_message(property_identifier: str, expected_type: Optional[ExpectedType]) -> str:
def _default_message(key_path: str, expected_type: Optional[ExpectedType]) -> str:
if expected_type:
type_description, _ = expected_type.value
return f"Property '{property_identifier}' should be a {type_description}."
return f"Type of property '{property_identifier}' is invalid."
return f"Property '{key_path}' should be a {type_description}."
return f"Type of property '{key_path}' is invalid."


class InvalidResourceAttributeTypeException(InvalidResourceException):
def __init__(
self,
logical_id: str,
key_path: str,
expected_type: Optional[ExpectedType],
message: Optional[str] = None,
) -> None:
message = message or self._default_message(logical_id, key_path, expected_type)
super().__init__(logical_id, message)

@staticmethod
def _default_message(logical_id: str, key_path: str, expected_type: Optional[ExpectedType]) -> str:
if expected_type:
type_description, _ = expected_type.value
return f"Attribute '{key_path}' should be a {type_description}."
return f"Type of attribute '{key_path}' is invalid."


class InvalidEventException(ExceptionWithMessage):
Expand Down
23 changes: 12 additions & 11 deletions samtranslator/parser/parser.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import logging

from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException, InvalidResourceException
from samtranslator.model.exceptions import (
InvalidDocumentException,
InvalidTemplateException,
InvalidResourceAttributeTypeException,
)
from samtranslator.validator.validator import SamTemplateValidator
from samtranslator.plugins import LifeCycleEvents
from samtranslator.public.sdk.template import SamTemplate
from samtranslator.validator.value_validator import sam_expect

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -41,16 +46,12 @@ def validate_datatypes(sam_template): # type: ignore[no-untyped-def]
# NOTE: Properties isn't required for SimpleTable, so we can't check
# `not isinstance(sam_resources.get("Properties"), dict)` as this would be a breaking change.
# sam_resource.properties defaults to {} in SamTemplate init
if not isinstance(sam_resource.properties, dict):
raise InvalidDocumentException(
[
InvalidResourceException(
resource_logical_id,
"All 'Resources' must be Objects and have a 'Properties' Object. If "
"you're using YAML, this may be an indentation issue.",
)
]
)
try:
sam_expect(
sam_resource.properties, resource_logical_id, "Properties", is_resource_attribute=True
).to_be_a_map()
except InvalidResourceAttributeTypeException as e:
raise InvalidDocumentException([e]) from e

# private methods
def _validate(self, sam_template, parameter_values): # type: ignore[no-untyped-def]
Expand Down
2 changes: 1 addition & 1 deletion samtranslator/sdk/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def valid(self) -> bool:
"""
# As long as the type is valid and type string.
# validate the condition should be string

# TODO Refactor this file so that it has logical id, can use sam_expect here after that
if self.condition:

if not IS_STR(self.condition, should_raise=False):
Expand Down
30 changes: 21 additions & 9 deletions samtranslator/validator/value_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
InvalidEventException,
InvalidResourceException,
InvalidResourcePropertyTypeException,
InvalidResourceAttributeTypeException,
)

T = TypeVar("T")
Expand All @@ -14,16 +15,23 @@
class _ResourcePropertyValueValidator(Generic[T]):
value: Optional[T]
resource_id: str
property_identifier: str
key_path: str
is_sam_event: bool
is_resource_attribute: bool

def __init__(
self, value: Optional[T], resource_id: str, property_identifier: str, is_sam_event: bool = False
self,
value: Optional[T],
resource_id: str,
key_path: str,
is_sam_event: bool = False,
is_resource_attribute: bool = False,
) -> None:
self.value = value
self.resource_id = resource_id
self.property_identifier = property_identifier
self.key_path = key_path
self.is_sam_event = is_sam_event
self.is_resource_attribute = is_resource_attribute

@property
def resource_logical_id(self) -> Optional[str]:
Expand All @@ -43,11 +51,15 @@ def to_be_a(self, expected_type: ExpectedType, message: Optional[str] = "") -> T
if not isinstance(self.value, type_class):
if self.event_id:
raise InvalidEventException(
self.event_id, message or f"Property '{self.property_identifier}' should be a {type_description}."
self.event_id, message or f"Property '{self.key_path}' should be a {type_description}."
)
if self.resource_logical_id:
if self.is_resource_attribute:
raise InvalidResourceAttributeTypeException(
self.resource_logical_id, self.key_path, expected_type, message
)
raise InvalidResourcePropertyTypeException(
self.resource_logical_id, self.property_identifier, expected_type, message
self.resource_logical_id, self.key_path, expected_type, message
)
raise RuntimeError("event_id and resource_logical_id are both None")
# mypy is not smart to derive class from expected_type.value[1], ignore types:
Expand All @@ -61,7 +73,7 @@ def to_not_be_none(self, message: Optional[str] = "") -> T:
"""
if self.value is None:
if not message:
message = f"Property '{self.property_identifier}' is required."
message = f"Property '{self.key_path}' is required."
if self.event_id:
raise InvalidEventException(self.event_id, message)
if self.resource_logical_id:
Expand Down Expand Up @@ -89,9 +101,9 @@ def to_be_a_list_of(self, expected_type: ExpectedType, message: Optional[str] =
"""
value = self.to_be_a(ExpectedType.LIST, message)
for index, item in enumerate(value): # type: ignore
sam_expect(
item, self.resource_id, f"{self.property_identifier}[{index}]", is_sam_event=self.is_sam_event
).to_be_a(expected_type, message)
sam_expect(item, self.resource_id, f"{self.key_path}[{index}]", is_sam_event=self.is_sam_event).to_be_a(
expected_type, message
)
return value

def to_be_a_string(self, message: Optional[str] = "") -> str:
Expand Down
4 changes: 2 additions & 2 deletions tests/translator/output/error_invalid_properties.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Function] is invalid. All 'Resources' must be Objects and have a 'Properties' Object. If you're using YAML, this may be an indentation issue.",
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Function] is invalid. Attribute 'Properties' should be a map.",
"errors": [
{
"errorMessage": "Resource with id [Function] is invalid. All 'Resources' must be Objects and have a 'Properties' Object. If you're using YAML, this may be an indentation issue."
"errorMessage": "Resource with id [Function] is invalid. Attribute 'Properties' should be a map."
}
]
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Function] is invalid. All 'Resources' must be Objects and have a 'Properties' Object. If you're using YAML, this may be an indentation issue.",
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Function] is invalid. Attribute 'Properties' should be a map.",
"errors": [
{
"errorMessage": "Resource with id [Function] is invalid. All 'Resources' must be Objects and have a 'Properties' Object. If you're using YAML, this may be an indentation issue."
"errorMessage": "Resource with id [Function] is invalid. Attribute 'Properties' should be a map."
}
]
}