1use serde_json::Value;
30
31use super::gateway::GatewayConfig;
32use crate::vars::template::Template;
33
34pub fn collect_template_warnings(gw: &GatewayConfig) -> Vec<String> {
38 let mut warnings = Vec::new();
39
40 for policy in &gw.policies {
41 for node in &policy.nodes {
42 for (key, value) in &node.config {
43 for (path, warning) in leaves(value, key) {
44 warnings.push(format!(
45 "policy '{}' node '{}' key '{}': {}",
46 policy.name, node.id, path, warning
47 ));
48 }
49 }
50 }
51 }
52
53 for sn in &gw.supernodes {
54 for node in &sn.nodes {
55 for (key, value) in &node.config {
56 for (path, warning) in leaves(value, key) {
57 warnings.push(format!(
58 "supernode '{}' node '{}' key '{}': {}",
59 sn.name, node.id, path, warning
60 ));
61 }
62 }
63 }
64 }
65
66 for def in &gw.plugin_configs {
67 for (key, value) in &def.config {
68 for (path, warning) in leaves(value, key) {
69 warnings.push(format!(
70 "plugin-config '{}' key '{}': {}",
71 def.name, path, warning
72 ));
73 }
74 }
75 }
76
77 warnings
78}
79
80fn leaves(value: &Value, root: &str) -> Vec<(String, String)> {
85 let mut out = Vec::new();
86 walk(value, root, &mut out);
87 out
88}
89
90fn walk(value: &Value, path: &str, out: &mut Vec<(String, String)>) {
91 match value {
92 Value::String(s) => {
93 for warning in Template::source_warnings_only(s) {
94 out.push((path.to_string(), warning));
95 }
96 }
97 Value::Array(items) => {
98 for (i, item) in items.iter().enumerate() {
99 walk(item, &format!("{path}[{i}]"), out);
100 }
101 }
102 Value::Object(map) => {
103 for (k, v) in map {
104 walk(v, &format!("{path}.{k}"), out);
105 }
106 }
107 _ => {}
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::config::{NodeConfig, PluginConfigDef, PolicyConfig, SupernodeConfig};
115 use serde_json::json;
116
117 fn node(id: &str, ty: &str, config: serde_json::Value) -> NodeConfig {
118 NodeConfig {
119 id: id.into(),
120 node_type: ty.into(),
121 config: serde_json::from_value(config).unwrap(),
122 config_ref: None,
123 position: None,
124 }
125 }
126
127 fn gw() -> GatewayConfig {
128 serde_yaml::from_str("{}").unwrap()
129 }
130
131 #[test]
135 fn test_unknown_reference_yields_one_named_warning() {
136 let mut gw = gw();
137 gw.policies = vec![PolicyConfig {
138 name: "p".into(),
139 error_handler: None,
140 nodes: vec![node(
141 "n",
142 "proxy-rewrite",
143 json!({"uri": "{{request.headres.x}}"}),
144 )],
145 edges: Vec::new(),
146 }];
147
148 let warnings = collect_template_warnings(&gw);
149
150 assert_eq!(warnings.len(), 1, "{warnings:?}");
151 assert!(warnings[0].contains("policy 'p'"), "{}", warnings[0]);
152 assert!(warnings[0].contains("node 'n'"), "{}", warnings[0]);
153 assert!(warnings[0].contains("key 'uri'"), "{}", warnings[0]);
154 assert!(
155 warnings[0].contains("{{request.headres.x}}"),
156 "{}",
157 warnings[0]
158 );
159 }
160
161 #[test]
163 fn test_valid_reference_yields_no_warnings() {
164 let mut gw = gw();
165 gw.policies = vec![PolicyConfig {
166 name: "p".into(),
167 error_handler: None,
168 nodes: vec![node(
169 "n",
170 "proxy-rewrite",
171 json!({"uri": "{{request.path}}"}),
172 )],
173 edges: Vec::new(),
174 }];
175
176 let warnings = collect_template_warnings(&gw);
177
178 assert!(warnings.is_empty(), "{warnings:?}");
179 }
180
181 #[test]
184 fn test_nested_object_and_array_leaves_covered() {
185 let mut gw = gw();
186 gw.policies = vec![PolicyConfig {
187 name: "p".into(),
188 error_handler: None,
189 nodes: vec![node(
190 "n",
191 "proxy-rewrite",
192 json!({
193 "headers": { "x-custom": "{{request.headres.x}}" },
194 "list": ["fine", "{{client.bogus}}"]
195 }),
196 )],
197 edges: Vec::new(),
198 }];
199
200 let warnings = collect_template_warnings(&gw);
201
202 assert_eq!(warnings.len(), 2, "{warnings:?}");
203 assert!(
204 warnings
205 .iter()
206 .any(|w| w.contains("key 'headers.x-custom'")),
207 "{warnings:?}"
208 );
209 assert!(
210 warnings.iter().any(|w| w.contains("key 'list[1]'")),
211 "{warnings:?}"
212 );
213 }
214
215 #[test]
218 fn test_supernode_container_named() {
219 let mut gw = gw();
220 gw.supernodes = vec![SupernodeConfig {
221 name: "sn".into(),
222 description: None,
223 nodes: vec![node(
224 "inner",
225 "proxy-rewrite",
226 json!({"uri": "{{request.headres.x}}"}),
227 )],
228 edges: Vec::new(),
229 }];
230
231 let warnings = collect_template_warnings(&gw);
232
233 assert_eq!(warnings.len(), 1, "{warnings:?}");
234 assert!(warnings[0].contains("supernode 'sn'"), "{}", warnings[0]);
235 assert!(warnings[0].contains("node 'inner'"), "{}", warnings[0]);
236 assert!(warnings[0].contains("key 'uri'"), "{}", warnings[0]);
237 }
238
239 #[test]
243 fn test_plugin_config_container_named() {
244 let mut gw = gw();
245 gw.plugin_configs = vec![PluginConfigDef {
246 name: "shared".into(),
247 plugin_type: "proxy-rewrite".into(),
248 description: None,
249 config: serde_json::from_value(json!({"uri": "{{request.headres.x}}"})).unwrap(),
250 }];
251
252 let warnings = collect_template_warnings(&gw);
253
254 assert_eq!(warnings.len(), 1, "{warnings:?}");
255 assert!(
256 warnings[0].contains("plugin-config 'shared'"),
257 "{}",
258 warnings[0]
259 );
260 assert!(warnings[0].contains("key 'uri'"), "{}", warnings[0]);
261 assert!(!warnings[0].contains("node "), "{}", warnings[0]);
262 }
263
264 #[test]
265 fn test_empty_gateway_yields_no_warnings() {
266 assert!(collect_template_warnings(&gw()).is_empty());
267 }
268}