Skip to content

Commit ced5a6f

Browse files
authored
Better comparison of json schema types for cfn usage (#3373)
1 parent 6b17797 commit ced5a6f

File tree

4 files changed

+137
-6
lines changed

4 files changed

+137
-6
lines changed

src/cfnlint/helpers.py

+49-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import os
2020
import sys
2121
from io import BytesIO
22-
from typing import Any
22+
from typing import Any, Sequence
2323
from urllib.request import Request, urlopen, urlretrieve
2424

2525
import regex as re
@@ -537,6 +537,54 @@ def is_function(instance: Any) -> tuple[str | None, Any]:
537537
return None, None
538538

539539

540+
def _translate_types(types: Sequence[str]) -> list[str]:
541+
"""
542+
Return compatible types. This is an adventitious result
543+
meaning a string could be an integer.
544+
545+
Args:
546+
types (Sequence[str]): The types
547+
548+
Returns:
549+
bool: If any type of source is compatible with any type in the destination
550+
"""
551+
compatible_types = []
552+
for t in types:
553+
if t == "string":
554+
compatible_types.extend([t, "number", "boolean", "integer"])
555+
if t == "integer":
556+
compatible_types.extend([t, "number", "string"])
557+
if t == "boolean":
558+
compatible_types.extend([t, "string"])
559+
if t == "number":
560+
compatible_types.extend([t, "string"])
561+
else:
562+
compatible_types.append(t)
563+
return compatible_types
564+
565+
566+
def is_types_compatible(
567+
source_types: str | Sequence[str], destination_types: str | Sequence[str]
568+
) -> bool:
569+
"""
570+
Validate if desination types are compatible with source types.
571+
572+
Args:
573+
source_types (str | Sequence[str]): The source types
574+
destination_types (str | Sequence[str]): The destination types
575+
576+
Returns:
577+
bool: If any type of source is compatible with any type in the destination
578+
"""
579+
source_types = _translate_types(ensure_list(source_types))
580+
destination_types = ensure_list(destination_types)
581+
582+
if any(schema_type in source_types for schema_type in destination_types):
583+
return True
584+
585+
return False
586+
587+
540588
def format_json_string(json_string):
541589
"""Format the given JSON string"""
542590

src/cfnlint/rules/functions/GetAtt.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import regex as re
1212

13-
from cfnlint.helpers import ensure_list
13+
from cfnlint.helpers import ensure_list, is_types_compatible
1414
from cfnlint.jsonschema import ValidationError, ValidationResult, Validator
1515
from cfnlint.rules.functions._BaseFn import BaseFn, all_types
1616
from cfnlint.schema import PROVIDER_SCHEMA_MANAGER
@@ -132,7 +132,7 @@ def _resolve_getatt(
132132

133133
types = ensure_list(s.get("type"))
134134

135-
if any(schema_type in types for schema_type in schema_types):
135+
if is_types_compatible(types, schema_types):
136136
continue
137137

138138
reprs = ", ".join(repr(type) for type in types)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
import pytest
7+
8+
from cfnlint.helpers import is_types_compatible
9+
10+
11+
@pytest.mark.parametrize(
12+
"name,source,destination,expected",
13+
[
14+
(
15+
"Types are directly working together",
16+
"string",
17+
["string"],
18+
True,
19+
),
20+
(
21+
"Types can be converted from source to destiniation",
22+
"string",
23+
["integer"],
24+
True,
25+
),
26+
(
27+
"Types can be converted from destination to source",
28+
"integer",
29+
["string"],
30+
True,
31+
),
32+
(
33+
"Types of integer against array are not compatible",
34+
"integer",
35+
"array",
36+
False,
37+
),
38+
(
39+
"Types of integer can be compatible",
40+
"integer",
41+
"number",
42+
True,
43+
),
44+
(
45+
"Types of number are not an integer",
46+
"number",
47+
"integer",
48+
False,
49+
),
50+
(
51+
"Types of integer is not compatible with boolean",
52+
"integer",
53+
"boolean",
54+
False,
55+
),
56+
(
57+
"Types of number is not compatible with boolean",
58+
"number",
59+
"boolean",
60+
False,
61+
),
62+
(
63+
"Types of number, string is compatible with boolean",
64+
["number", "string"],
65+
"boolean",
66+
True,
67+
),
68+
],
69+
)
70+
def test_validate(name, source, destination, expected):
71+
result = is_types_compatible(source, destination)
72+
assert result == expected, f"Test {name!r} got {result!r}"

test/unit/rules/functions/test_getatt.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ def cfn():
2626
return Template(
2727
"",
2828
{
29-
"Resources": {"MyBucket": {"Type": "AWS::S3::Bucket"}},
29+
"Resources": {
30+
"MyBucket": {"Type": "AWS::S3::Bucket"},
31+
"MyCodePipeline": {"Type": "AWS::CodePipeline::Pipeline"},
32+
},
3033
"Parameters": {
3134
"MyResourceParameter": {"Type": "String", "Default": "MyBucket"},
3235
"MyAttributeParameter": {"Type": "String", "AllowedValues": ["Arn"]},
@@ -99,7 +102,7 @@ def validate(self, validator, s, instance, schema):
99102
{},
100103
[
101104
ValidationError(
102-
"'Foo' is not one of ['MyBucket']",
105+
"'Foo' is not one of ['MyBucket', 'MyCodePipeline']",
103106
path=deque(["Fn::GetAtt", 0]),
104107
schema_path=deque(["enum"]),
105108
validator="fn_getatt",
@@ -151,6 +154,14 @@ def validate(self, validator, s, instance, schema):
151154
),
152155
],
153156
),
157+
(
158+
"Valid GetAtt with integer to string",
159+
{"Fn::GetAtt": "MyCodePipeline.Version"},
160+
{"type": ["integer"]},
161+
{},
162+
{},
163+
[],
164+
),
154165
(
155166
"Valid GetAtt with one good response type",
156167
{"Fn::GetAtt": "MyBucket.Arn"},
@@ -203,7 +214,7 @@ def validate(self, validator, s, instance, schema):
203214
[
204215
ValidationError(
205216
(
206-
"'Arn' is not one of ['MyBucket'] when "
217+
"'Arn' is not one of ['MyBucket', 'MyCodePipeline'] when "
207218
"{'Ref': 'MyAttributeParameter'} is resolved"
208219
),
209220
path=deque(["Fn::GetAtt", 0]),

0 commit comments

Comments
 (0)