Skip to content

Commit 1ee4445

Browse files
committed
Validate a SAM resource property using schema models
1 parent d1c26f7 commit 1ee4445

File tree

5 files changed

+150
-3
lines changed

5 files changed

+150
-3
lines changed

requirements/base.txt

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
boto3>=1.19.5,==1.*
22
jsonschema<5,>=3.2 # TODO: evaluate risk of removing jsonschema 3.x support
33
typing_extensions>=4.4,<5 # 3.7 doesn't have Literal
4+
5+
# resource validation & schema generation, requiring features in >=1.10
6+
pydantic~=1.10

requirements/dev.txt

-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,3 @@ mypy~=1.0.0
2929
boto3-stubs[appconfig,serverlessrepo]>=1.19.5,==1.*
3030
types-PyYAML~=5.4
3131
types-jsonschema~=3.2
32-
33-
# schema generation, requiring features in >=1.10
34-
pydantic~=1.10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""A resource validator to help validate resource properties and raise exception when some value is unexpected."""
2+
from typing import Any, Dict, Type
3+
4+
from pydantic import BaseModel
5+
6+
7+
class ResourceModel:
8+
"""
9+
Wrapper class around a SAM schema BaseModel to with a new functional "get" method
10+
"""
11+
12+
def __init__(self, model: BaseModel) -> None:
13+
self.model = model
14+
15+
def get(self, attr: str, default_value: Any = None) -> Any:
16+
attr_value = self.model.__dict__.get(attr, default_value)
17+
if isinstance(attr_value, BaseModel):
18+
if "__root__" in attr_value.__dict__:
19+
return attr_value.__dict__["__root__"]
20+
return ResourceModel(attr_value)
21+
return attr_value
22+
23+
def __getitem__(self, attr):
24+
attr_value = self.model.__dict__[attr]
25+
if isinstance(attr_value, BaseModel):
26+
if "__root__" in attr_value.__dict__:
27+
return attr_value.__dict__["__root__"]
28+
return ResourceModel(attr_value)
29+
return attr_value
30+
31+
32+
def to_resource_model(resource_properties: Dict[Any, Any], cls: Type[BaseModel]) -> ResourceModel:
33+
"""
34+
Given properties of a SAM resource return a typed object from the definitions of SAM schema model
35+
36+
param:
37+
resource_properties: properties from input template
38+
cls: SAM schema models
39+
"""
40+
return ResourceModel(cls(**resource_properties))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
Resources:
2+
NoSourceConnector:
3+
Type: AWS::Serverless::Connector
4+
Properties:
5+
Destination:
6+
Id: MyQueue1
7+
Permissions:
8+
- Write
9+
10+
NoDestConnector:
11+
Type: AWS::Serverless::Connector
12+
Properties:
13+
Source:
14+
Id: MyQueue1
15+
Permissions:
16+
- Write
17+
18+
NoPermissionConnector:
19+
Type: AWS::Serverless::Connector
20+
Properties:
21+
Source:
22+
Id: MyQueue1
23+
Destination:
24+
Id: MyTable
25+
26+
InvalidPermissionConnector:
27+
Type: AWS::Serverless::Connector
28+
Properties:
29+
Source:
30+
Id: MyQueue1
31+
Destination:
32+
Id: MyTable
33+
Permissions:
34+
- Invoke
35+
36+
InvalidPermissionTypeConnector:
37+
Type: AWS::Serverless::Connector
38+
Properties:
39+
Source:
40+
Id: MyQueue1
41+
Destination:
42+
Id: MyTable
43+
Permissions:
44+
Hello: World
45+
46+
InvalidIdTypeConnector:
47+
Type: AWS::Serverless::Connector
48+
Properties:
49+
Source:
50+
Id:
51+
Key: Value
52+
Destination:
53+
Id: MyTable
54+
Permissions:
55+
- Read
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import os.path
2+
from unittest import TestCase
3+
4+
from pydantic.error_wrappers import ValidationError
5+
from samtranslator.validator.resource_validator import to_resource_model
6+
from samtranslator.yaml_helper import yaml_parse
7+
from schema_source.aws_serverless_connector import Properties as ConnectorProperties
8+
9+
BASE_PATH = os.path.dirname(__file__)
10+
CONNECTOR_INPUT_FOLDER = os.path.join(BASE_PATH, "input", "connector")
11+
12+
13+
class TestResourceModel(TestCase):
14+
def setUp(self) -> None:
15+
self.connector_template = {
16+
"Source": {
17+
"Arn": "random-arn",
18+
"Type": "random-type",
19+
},
20+
"Destination": {"Id": "MyTable"},
21+
"Permissions": ["Read"],
22+
}
23+
24+
def test_resource_model_get(self):
25+
connector_model = to_resource_model(
26+
self.connector_template,
27+
ConnectorProperties,
28+
)
29+
self.assertEqual(connector_model.get("Source").get("Arn"), "random-arn")
30+
self.assertEqual(connector_model.get("Source").get("Type"), "random-type")
31+
self.assertEqual(connector_model.get("Source").get("Id"), None)
32+
self.assertEqual(connector_model.get("Destination").get("Id"), "MyTable")
33+
self.assertEqual(connector_model.get("Permissions"), ["Read"])
34+
self.assertEqual(connector_model.get("FakeProperty"), None)
35+
36+
self.assertEqual(connector_model["Source"]["Arn"], "random-arn")
37+
self.assertEqual(connector_model["Source"]["Type"], "random-type")
38+
self.assertEqual(connector_model["Destination"]["Id"], "MyTable")
39+
self.assertEqual(connector_model["Permissions"], ["Read"])
40+
41+
42+
class TestResourceValidatorFailure(TestCase):
43+
def test_to_resource_model_error_connector_template(self):
44+
manifest = yaml_parse(open(os.path.join(CONNECTOR_INPUT_FOLDER, "error_connector.yaml")))
45+
for _, resource in manifest["Resources"].items():
46+
properties = resource["Properties"]
47+
48+
with self.assertRaisesRegex(
49+
ValidationError,
50+
"[1-9] validation error for Properties(.|\n)+(Source|Destination|Permissions)(.|\n)*(field required)|(unexpected value)|(str type expected)|(value is not a valid list).+",
51+
):
52+
to_resource_model(properties, ConnectorProperties)

0 commit comments

Comments
 (0)