6
6
from __future__ import annotations
7
7
8
8
import json
9
+ from collections import deque
10
+ from copy import deepcopy
9
11
from typing import Any
10
12
11
13
import cfnlint .data .schemas .other .resources
12
14
import cfnlint .data .schemas .other .step_functions
13
15
import cfnlint .helpers
14
16
from cfnlint .helpers import is_function
15
17
from cfnlint .jsonschema import ValidationError , ValidationResult , Validator
18
+ from cfnlint .rules .helpers import get_value_from_path
16
19
from cfnlint .rules .jsonschema .CfnLintJsonSchema import CfnLintJsonSchema , SchemaDetails
17
20
from cfnlint .schema .resolver import RefResolver
18
21
@@ -40,11 +43,11 @@ def __init__(self):
40
43
all_matches = True ,
41
44
)
42
45
43
- store = {
46
+ self . store = {
44
47
"definition" : self .schema ,
45
48
}
46
49
47
- self .resolver = RefResolver .from_schema (self .schema , store = store )
50
+ self .resolver = RefResolver .from_schema (self .schema , store = self . store )
48
51
49
52
def _fix_message (self , err : ValidationError ) -> ValidationError :
50
53
if len (err .path ) > 1 :
@@ -53,6 +56,66 @@ def _fix_message(self, err: ValidationError) -> ValidationError:
53
56
err .context [i ] = self ._fix_message (c_err )
54
57
return err
55
58
59
+ def _convert_schema_to_jsonata (self ):
60
+ schema = self .schema
61
+ schema = deepcopy (schema )
62
+ schema ["definitions" ]["common" ] = deepcopy (schema ["definitions" ]["common" ])
63
+ schema ["definitions" ]["common" ]["allOf" ] = [
64
+ {
65
+ "properties" : {
66
+ "QueryLanguage" : {"const" : "JSONata" },
67
+ "InputPath" : False ,
68
+ "OutputPath" : False ,
69
+ "Parameters" : False ,
70
+ "ResultPath" : False ,
71
+ "ResultSelector" : False ,
72
+ },
73
+ },
74
+ ]
75
+ return schema
76
+
77
+ def _clean_schema (self , validator : Validator , instance : Any ):
78
+ for ql , ql_validator in get_value_from_path (
79
+ validator , instance , deque (["QueryLanguage" ])
80
+ ):
81
+ if ql is None or ql == "JSONPath" :
82
+ yield self .schema , ql_validator
83
+
84
+ if ql == "JSONata" :
85
+ yield self ._convert_schema_to_jsonata (), ql_validator
86
+
87
+ def _validate_step (
88
+ self ,
89
+ validator : Validator ,
90
+ substitutions : Any ,
91
+ value : Any ,
92
+ add_path_to_message : bool ,
93
+ k : str ,
94
+ ) -> ValidationResult :
95
+
96
+ for err in validator .iter_errors (value ):
97
+ if validator .is_type (err .instance , "string" ):
98
+ if (
99
+ err .instance .replace ("${" , "" ).replace ("}" , "" ).strip ()
100
+ in substitutions
101
+ ):
102
+ continue
103
+ if add_path_to_message :
104
+ err = self ._fix_message (err )
105
+
106
+ err .path .appendleft (k )
107
+ if err .schema in [True , False ]:
108
+ err .message = (
109
+ f"Additional properties are not allowed ({ err .path [- 1 ]!r} "
110
+ "was unexpected)"
111
+ )
112
+ if not err .validator or (
113
+ not err .validator .startswith ("fn_" ) and err .validator not in ["cfnLint" ]
114
+ ):
115
+ err .rule = self
116
+
117
+ yield self ._clean_error (err )
118
+
56
119
def validate (
57
120
self , validator : Validator , keywords : Any , instance : Any , schema : dict [str , Any ]
58
121
) -> ValidationResult :
@@ -61,6 +124,11 @@ def validate(
61
124
if not validator .cfn .has_serverless_transform ():
62
125
definition_keys .append ("DefinitionString" )
63
126
127
+ substitutions = []
128
+ props_substitutions = instance .get ("DefinitionSubstitutions" , {})
129
+ if validator .is_type (props_substitutions , "object" ):
130
+ substitutions = list (props_substitutions .keys ())
131
+
64
132
# First time child rules are configured against the rule
65
133
# so we can run this now
66
134
for k in definition_keys :
@@ -74,48 +142,32 @@ def validate(
74
142
add_path_to_message = False
75
143
if validator .is_type (value , "string" ):
76
144
try :
77
- step_validator = validator .evolve (
78
- context = validator .context .evolve (
79
- functions = [],
80
- ),
81
- resolver = self .resolver ,
82
- schema = self .schema ,
83
- )
84
145
value = json .loads (value )
85
146
add_path_to_message = True
147
+ for schema , schema_validator in self ._clean_schema (
148
+ validator , value
149
+ ):
150
+ resolver = RefResolver .from_schema (schema , store = self .store )
151
+ step_validator = schema_validator .evolve (
152
+ context = validator .context .evolve (
153
+ functions = [],
154
+ ),
155
+ resolver = resolver ,
156
+ schema = self .schema ,
157
+ )
158
+
159
+ yield from self ._validate_step (
160
+ step_validator , substitutions , value , add_path_to_message , k
161
+ )
86
162
except json .JSONDecodeError :
87
163
return
88
164
else :
89
- step_validator = validator .evolve (
90
- resolver = self .resolver ,
91
- schema = self .schema ,
92
- )
93
-
94
- substitutions = []
95
- props_substitutions = instance .get ("DefinitionSubstitutions" , {})
96
- if validator .is_type (props_substitutions , "object" ):
97
- substitutions = list (props_substitutions .keys ())
98
-
99
- for err in step_validator .iter_errors (value ):
100
- if validator .is_type (err .instance , "string" ):
101
- if (
102
- err .instance .replace ("${" , "" ).replace ("}" , "" ).strip ()
103
- in substitutions
104
- ):
105
- continue
106
- if add_path_to_message :
107
- err = self ._fix_message (err )
108
-
109
- err .path .appendleft (k )
110
- if err .schema in [True , False ]:
111
- err .message = (
112
- f"Additional properties are not allowed ({ err .path [- 1 ]!r} "
113
- "was unexpected)"
165
+ for schema , schema_validator in self ._clean_schema (validator , value ):
166
+ resolver = RefResolver .from_schema (schema , store = self .store )
167
+ step_validator = schema_validator .evolve (
168
+ resolver = resolver ,
169
+ schema = schema ,
170
+ )
171
+ yield from self ._validate_step (
172
+ step_validator , substitutions , value , add_path_to_message , k
114
173
)
115
- if not err .validator or (
116
- not err .validator .startswith ("fn_" )
117
- and err .validator not in ["cfnLint" ]
118
- ):
119
- err .rule = self
120
-
121
- yield self ._clean_error (err )
0 commit comments