Skip to content

Commit 0449fe3

Browse files
authored
Greatly simplify findinmap resolution (#3406)
1 parent db0097e commit 0449fe3

File tree

3 files changed

+125
-90
lines changed

3 files changed

+125
-90
lines changed

src/cfnlint/jsonschema/_resolvers_cfn.py

+44-89
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from cfnlint.helpers import AVAILABILITY_ZONES, REGEX_SUB_PARAMETERS
1515
from cfnlint.jsonschema import ValidationError, Validator
1616
from cfnlint.jsonschema._typing import ResolutionResult
17-
from cfnlint.jsonschema._utils import equal
1817

1918

2019
def unresolvable(validator: Validator, instance: Any) -> ResolutionResult:
@@ -55,94 +54,50 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
5554
), None
5655
default_value = value
5756

58-
for map_name, map_v, _ in validator.resolve_value(instance[0]):
59-
if not validator.is_type(map_name, "string"):
60-
continue
61-
for top_level_key, top_v, _ in validator.resolve_value(instance[1]):
62-
if validator.is_type(top_level_key, "integer"):
63-
top_level_key = str(top_level_key)
64-
if not validator.is_type(top_level_key, "string"):
65-
continue
66-
for second_level_key, second_v, err in validator.resolve_value(instance[2]):
67-
if validator.is_type(second_level_key, "integer"):
68-
second_level_key = str(second_level_key)
69-
if not validator.is_type(second_level_key, "string"):
70-
continue
71-
try:
72-
mappings = list(validator.context.mappings.keys())
73-
if not default_value and all(
74-
not (equal(map_name, each)) for each in mappings
75-
):
76-
yield None, map_v.evolve(
77-
context=map_v.context.evolve(
78-
path=map_v.context.path.evolve(value_path=deque([0])),
79-
),
80-
), ValidationError(
81-
f"{map_name!r} is not one of {mappings!r}", path=[0]
82-
)
83-
continue
84-
85-
top_level_keys = list(
86-
validator.context.mappings[map_name].keys.keys()
87-
)
88-
if not default_value and all(
89-
not (equal(top_level_key, each)) for each in top_level_keys
90-
):
91-
yield None, top_v.evolve(
92-
context=top_v.context.evolve(
93-
path=top_v.context.path.evolve(value_path=deque([1])),
94-
),
95-
), ValidationError(
96-
f"{top_level_key!r} is not one of {top_level_keys!r}",
97-
path=[1],
98-
)
99-
continue
100-
101-
second_level_keys = list(
102-
validator.context.mappings[map_name]
103-
.keys[top_level_key]
104-
.keys.keys()
105-
)
106-
if not default_value and all(
107-
not (equal(second_level_key, each))
108-
for each in second_level_keys
109-
):
110-
yield None, second_v.evolve(
111-
context=second_v.context.evolve(
112-
path=second_v.context.path.evolve(
113-
value_path=deque([2])
114-
),
115-
),
116-
), ValidationError(
117-
f"{second_level_key!r} is not one of {second_level_keys!r}",
118-
path=[2],
119-
)
120-
continue
121-
122-
for value in validator.context.mappings[map_name].find_in_map(
123-
top_level_key,
124-
second_level_key,
125-
):
126-
yield (
127-
value,
128-
validator.evolve(
129-
context=validator.context.evolve(
130-
path=validator.context.path.evolve(
131-
value_path=deque(
132-
[
133-
"Mappings",
134-
map_name,
135-
top_level_key,
136-
second_level_key,
137-
]
138-
)
139-
)
140-
)
141-
),
142-
None,
143-
)
144-
except KeyError:
145-
pass
57+
if (
58+
validator.is_type(instance[0], "string")
59+
and (
60+
validator.is_type(instance[1], "string")
61+
or validator.is_type(instance[1], "integer")
62+
)
63+
and validator.is_type(instance[2], "string")
64+
):
65+
map = validator.context.mappings.get(instance[0])
66+
if not map:
67+
if not default_value:
68+
yield None, validator, ValidationError(
69+
(
70+
f"{instance[0]!r} is not one of "
71+
f"{list(validator.context.mappings.keys())!r}"
72+
),
73+
path=deque([0]),
74+
)
75+
return
76+
77+
top_key = map.keys.get(instance[1])
78+
if not top_key:
79+
if not default_value:
80+
yield None, validator, ValidationError(
81+
(
82+
f"{instance[1]!r} is not one of "
83+
f"{list(map.keys.keys())!r} for "
84+
f"mapping {instance[0]!r}"
85+
),
86+
path=deque([1]),
87+
)
88+
return
89+
90+
value = top_key.keys.get(instance[2])
91+
if not value:
92+
if not default_value:
93+
yield default_value, validator, ValidationError(
94+
(
95+
f"{instance[2]!r} is not one of "
96+
f"{list(top_key.keys.keys())!r} for mapping "
97+
f"{instance[0]!r} and key {instance[1]!r}"
98+
),
99+
path=deque([2]),
100+
)
146101

147102

148103
def get_azs(validator: Validator, instance: Any) -> ResolutionResult:

test/unit/module/jsonschema/test_resolvers_cfn.py

+65
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99

1010
from cfnlint.context.context import Context, Map
11+
from cfnlint.jsonschema import ValidationError
1112
from cfnlint.jsonschema.validators import CfnTemplateValidator
1213

1314

@@ -19,6 +20,10 @@ def _resolve(name, instance, expected_results, **kwargs):
1920
for i, (instance, v, errors) in enumerate(resolutions):
2021
assert instance == expected_results[i][0]
2122
assert v.context.path.value_path == expected_results[i][1]
23+
if errors:
24+
print(errors.validator)
25+
print(errors.path)
26+
print(errors.schema_path)
2227
assert errors == expected_results[i][2]
2328

2429

@@ -216,6 +221,66 @@ def test_invalid_functions(name, instance, response):
216221
{"Fn::FindInMap": ["foo", "bar", "value", {"DefaultValue": "default"}]},
217222
[("default", deque([4, "DefaultValue"]), None)],
218223
),
224+
(
225+
"Valid FindInMap with a bad mapping",
226+
{"Fn::FindInMap": ["bar", "first", "second"]},
227+
[
228+
(
229+
None,
230+
deque([]),
231+
ValidationError(
232+
("'bar' is not one of ['foo']"),
233+
path=deque(["Fn::FindInMap", 0]),
234+
),
235+
)
236+
],
237+
),
238+
(
239+
"Valid FindInMap with a bad mapping and default",
240+
{"Fn::FindInMap": ["bar", "first", "second", {"DefaultValue": "default"}]},
241+
[("default", deque([4, "DefaultValue"]), None)],
242+
),
243+
(
244+
"Valid FindInMap with a bad top key",
245+
{"Fn::FindInMap": ["foo", "second", "first"]},
246+
[
247+
(
248+
None,
249+
deque([]),
250+
ValidationError(
251+
("'second' is not one of ['first'] for " "mapping 'foo'"),
252+
path=deque(["Fn::FindInMap", 1]),
253+
),
254+
)
255+
],
256+
),
257+
(
258+
"Valid FindInMap with a bad top key and default",
259+
{"Fn::FindInMap": ["foo", "second", "first", {"DefaultValue": "default"}]},
260+
[("default", deque([4, "DefaultValue"]), None)],
261+
),
262+
(
263+
"Valid FindInMap with a bad third key",
264+
{"Fn::FindInMap": ["foo", "first", "third"]},
265+
[
266+
(
267+
None,
268+
deque([]),
269+
ValidationError(
270+
(
271+
"'third' is not one of ['second'] for "
272+
"mapping 'foo' and key 'first'"
273+
),
274+
path=deque(["Fn::FindInMap", 2]),
275+
),
276+
)
277+
],
278+
),
279+
(
280+
"Valid FindInMap with a bad second key and default",
281+
{"Fn::FindInMap": ["foo", "first", "third", {"DefaultValue": "default"}]},
282+
[("default", deque([4, "DefaultValue"]), None)],
283+
),
219284
(
220285
"Valid Sub with a resolvable values",
221286
{"Fn::Sub": ["${a}-${b}", {"a": "foo", "b": "bar"}]},

test/unit/rules/functions/test_find_in_map.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ def cfn():
2626
return Template(
2727
"",
2828
{
29+
"Parameters": {
30+
"MyParameter": {
31+
"Type": "String",
32+
"AllowedValues": ["A", "B", "C"],
33+
}
34+
},
2935
"Resources": {"MyResource": Resource({"Type": "AWS::SSM::Parameter"})},
3036
"Mappings": {"A": {"B": {"C": "Value"}}},
3137
},
@@ -155,12 +161,20 @@ def context(cfn):
155161
[ValidationError("Foo")],
156162
[
157163
ValidationError(
158-
"'C' is not one of ['B']",
164+
"'C' is not one of ['B'] for mapping 'A'",
159165
path=deque(["Fn::FindInMap", 1]),
160166
schema_path=deque([]),
161167
),
162168
],
163169
),
170+
(
171+
"Valid Fn::FindInMap as the Ref could work",
172+
{"Fn::FindInMap": ["A", {"Ref": "MyParameter"}, "C"]},
173+
{"type": "string"},
174+
{"transforms": Transforms(["AWS::LanguageExtensions"])},
175+
[],
176+
[],
177+
),
164178
(
165179
"Valid Fn::FindInMap with a Ref to AWS::NoValue",
166180
{
@@ -201,4 +215,5 @@ def test_validate(
201215
ref_mock.assert_not_called()
202216
else:
203217
assert ref_mock.call_count == len(ref_mock_values) or 1
218+
204219
assert errs == expected, f"Test {name!r} got {errs!r}"

0 commit comments

Comments
 (0)