1use crate::consts::*;
2use crate::defect::DefectPhase;
3use crate::diagnostic::*;
4use crate::json_value::JsonValue;
5use crate::model::{ResolvedOutput, ResolvedResource, SemanticModel, TemplateRule};
6use crate::resolved_value::collect_condition_refs_from_resolved;
7use crate::resolver::{MapEntry, RefKind, ResolvedValue};
8use std::collections::HashMap;
9
10impl SemanticModel {
11 pub fn to_diagnostic_json(&self) -> DiagnosticModel {
12 let resources = build_resources(&self.resources, &self.graph);
13 let conditions = build_conditions(&self.conditions);
14 let cycles = filter_sam_cycles(self.graph.cycles(), &self.transforms, &self.resources);
15 let outputs = build_outputs(&self.outputs, &self.graph);
16
17 DiagnosticModel {
18 template: DiagnosticTemplate {
19 format_version: self.format_version.clone(),
20 description: self.description.clone(),
21 transforms: self.transforms.clone(),
22 raw_top_level_keys: self.raw_top_level_keys.clone(),
23 },
24 parameters: self
25 .parameters
26 .iter()
27 .map(|(k, v)| {
28 (
29 k.clone(),
30 JsonValue(serde_json::json!({
31 "type": v.param_type,
32 "default": v.default,
33 "allowedValues": v.allowed_values,
34 "allowedPattern": v.allowed_pattern,
35 "allowedPatternValid": v.allowed_pattern_valid,
36 "defaultMatchesAllowedPattern": v.default_matches_allowed_pattern,
37 "minLength": v.min_length,
38 "maxLength": v.max_length,
39 "minValue": v.min_value,
40 "maxValue": v.max_value,
41 "noEcho": v.no_echo,
42 })),
43 )
44 })
45 .collect(),
46 conditions,
47 condition_param_refs: self.conditions.referenced_params(),
48 condition_implications: self
55 .conditions
56 .implications
57 .iter()
58 .filter(|i| !i.antecedent.starts_with("__") && !i.consequent.starts_with("__"))
59 .map(|i| DiagnosticImplication { antecedent: i.antecedent.clone(), consequent: i.consequent.clone() })
60 .collect(),
61 condition_mutex_groups: self
62 .conditions
63 .mutex_groups
64 .iter()
65 .filter(|g| !g.conditions.iter().any(|c| c.starts_with("__")))
66 .map(|g| DiagnosticMutexGroup {
67 conditions: g.conditions.clone(),
68 parameter: g.parameter.clone(),
69 values: g.values.clone(),
70 })
71 .collect(),
72 condition_exclusions: {
73 let mut cond_names: Vec<&String> =
82 self.conditions.conditions.keys().filter(|n| !n.starts_with("__")).collect();
83 cond_names.sort();
84 let mut exclusions = Vec::new();
85 'pairs: for i in 0..cond_names.len() {
86 for j in (i + 1)..cond_names.len() {
87 if self.conditions.satisfiability_budget_exhausted() {
93 break 'pairs;
94 }
95 if !self.conditions.conditions_compatible(cond_names[i], cond_names[j]) {
96 exclusions.push(vec![cond_names[i].clone(), cond_names[j].clone()]);
97 }
98 }
99 }
100 exclusions
101 },
102 resource_condition_map: self
103 .resources
104 .iter()
105 .filter_map(|(id, r)| r.condition.as_ref().map(|c| (id.clone(), c.clone())))
106 .collect(),
107 mappings: JsonValue(serde_json::json!(self.mappings)),
108 resources,
109 outputs,
110 edges: build_edges(&self.graph),
111 cycles,
112 output_empty_joins: self.output_empty_joins.clone(),
113 sam_implicit_resources: self.sam_implicit_resources.iter().cloned().collect(),
114 globals_param_refs: self.globals_param_refs.clone(),
115 is_cdk: self.is_cdk,
116 fn_if_conditions: self.fn_if_conditions.clone(),
117 find_in_map_names: {
118 let mut names: Vec<String> = self.find_in_map_names.iter().cloned().collect();
119 names.sort();
120 names
121 },
122 params_referenced_in_definitions: {
123 let mut names: Vec<String> = self.params_referenced_in_definitions.iter().cloned().collect();
124 names.sort();
125 names
126 },
127 has_dynamic_findinmap_name: self.has_dynamic_findinmap_name,
128 has_parse_errors: self.diagnostics.iter().any(|d| d.is_fatal() && d.phase == Some(DefectPhase::Parse)),
129 parsed_rules: self.parsed_rules.iter().map(build_rule).collect(),
130 resolution_sources: self
131 .resolution_sources
132 .iter()
133 .map(|((rid, path), source)| ResolutionSource {
134 resource_id: rid.clone(),
135 property_path: path.clone(),
136 source: source.clone(),
137 })
138 .collect(),
139 }
140 }
141}
142
143fn build_resources(
144 resources: &HashMap<String, ResolvedResource>,
145 graph: &crate::graph::ReferenceGraph,
146) -> HashMap<String, DiagnosticResource> {
147 resources
148 .iter()
149 .map(|(id, res)| {
150 let props: HashMap<String, JsonValue> =
151 res.properties.iter().map(|(k, v)| (k.clone(), JsonValue(resolved_value_to_json(v)))).collect();
152
153 let outgoing = graph
154 .outgoing(id)
155 .iter()
156 .map(|e| {
157 let (kind, attr) = ref_kind_to_str(&e.kind);
158 OutgoingRef {
159 source_path: e.source_path.clone(),
160 target: e.target.clone(),
161 kind: kind.to_string(),
162 attr,
163 condition_context: e.condition_context.clone(),
164 }
165 })
166 .collect();
167
168 let incoming = graph
169 .incoming(id)
170 .iter()
171 .map(|e| {
172 let (kind, attr) = ref_kind_to_str(&e.kind);
173 IncomingRef {
174 source: e.source_resource.clone(),
175 source_path: e.source_path.clone(),
176 kind: kind.to_string(),
177 attr,
178 }
179 })
180 .collect();
181
182 (
183 id.clone(),
184 DiagnosticResource {
185 resource_type: res.resource_type.clone(),
186 condition: res.condition.clone(),
187 depends_on: res.depends_on.clone(),
188 deletion_policy: res.deletion_policy.as_ref().map(|v| JsonValue(resolved_value_to_json(v))),
189 update_replace_policy: res
190 .update_replace_policy
191 .as_ref()
192 .map(|v| JsonValue(resolved_value_to_json(v))),
193 creation_policy: res.creation_policy.as_ref().map(|v| JsonValue(serde_json::json!(v))),
194 update_policy: res.update_policy.as_ref().map(|v| JsonValue(serde_json::json!(v))),
195 properties: props,
196 outgoing_refs: outgoing,
197 incoming_refs: incoming,
198 find_in_map_refs: res.diagnostics.find_in_map_refs.clone(),
199 simple_subs: res
200 .diagnostics
201 .simple_subs
202 .iter()
203 .map(|s| PathVariable { path: s.path.clone(), variable: s.value.clone() })
204 .collect(),
205 redundant_subs: res.diagnostics.redundant_subs.clone(),
206 empty_joins: res.diagnostics.empty_joins.clone(),
207 hardcoded_partition_arns: res.diagnostics.hardcoded_partition_arns.clone(),
208 conditionally_null_props: res
209 .diagnostics
210 .conditionally_null_props
211 .iter()
212 .map(|s| ConditionalNull {
213 path: s.path.clone(),
214 condition: s.condition.clone(),
215 null_in_true: s.null_in_true_branch,
216 })
217 .collect(),
218 condition_refs: res.diagnostics.condition_refs.clone(),
219 for_each_expansions: res
220 .diagnostics
221 .foreach_expansions
222 .iter()
223 .map(|fe| DiagnosticForEachExpansion {
224 path: fe.property_path.clone(),
225 identifier: fe.identifier.clone(),
226 collection: fe.collection_source.clone(),
227 })
228 .collect(),
229 unsubstituted_variables: res
230 .diagnostics
231 .unsubstituted_variables
232 .iter()
233 .map(|s| PathVariable { path: s.path.clone(), variable: s.value.clone() })
234 .collect(),
235 unused_sub_keys: res
236 .diagnostics
237 .unused_sub_keys
238 .iter()
239 .map(|s| PathVariable { path: s.path.clone(), variable: s.value.clone() })
240 .collect(),
241 raw_pseudo_params: res
242 .diagnostics
243 .raw_pseudo_params
244 .iter()
245 .map(|s| PathVariable { path: s.path.clone(), variable: s.value.clone() })
246 .collect(),
247 secretsmanager_ref_paths: res.diagnostics.secretsmanager_ref_paths.clone(),
248 invalid_refs: res
249 .diagnostics
250 .invalid_refs
251 .iter()
252 .map(|s| PathTarget { path: s.path.clone(), target: s.value.clone() })
253 .collect(),
254 },
255 )
256 })
257 .collect()
258}
259
260fn build_conditions(conditions: &crate::conditions::ConditionModel) -> HashMap<String, DiagnosticCondition> {
261 let mut out = HashMap::new();
262 for name in conditions.names().filter(|n| !n.starts_with("__")) {
263 let (expression, deps) = if let Some(expr) = conditions.get(name) {
264 let mut d = Vec::new();
265 crate::conditions::collect_condition_deps(expr, &mut d);
266 d.sort();
267 d.dedup();
268 (Some(crate::conditions::format_condition_expr(expr)), if d.is_empty() { None } else { Some(d) })
269 } else {
270 (None, None)
271 };
272
273 let mut mutex_with: Vec<String> = Vec::new();
274 for group in &conditions.mutex_groups {
275 if group.conditions.contains(&name.to_string()) {
276 for peer in &group.conditions {
277 if peer != name {
278 mutex_with.push(peer.clone());
279 }
280 }
281 }
282 }
283
284 out.insert(
285 name.to_string(),
286 DiagnosticCondition {
287 expression,
288 deps,
289 mutex_with: if mutex_with.is_empty() { None } else { Some(mutex_with) },
290 },
291 );
292 }
293 out
294}
295
296fn build_edges(graph: &crate::graph::ReferenceGraph) -> Vec<ReferenceEdge> {
297 graph
298 .edges
299 .iter()
300 .map(|e| {
301 let (kind, attr) = ref_kind_to_str(&e.kind);
302 ReferenceEdge {
303 source: e.source_resource.clone(),
304 source_path: e.source_path.clone(),
305 target: e.target.clone(),
306 kind: kind.to_string(),
307 attr,
308 condition_context: e.condition_context.clone(),
309 }
310 })
311 .collect()
312}
313
314fn build_outputs(
315 outputs: &HashMap<String, ResolvedOutput>,
316 graph: &crate::graph::ReferenceGraph,
317) -> HashMap<String, DiagnosticOutput> {
318 let mut output_getatt_refs: HashMap<String, Vec<(String, String)>> = HashMap::new();
319 for edge in &graph.edges {
320 if let Some(output_name) = edge.source_resource.strip_prefix(OUTPUT_PSEUDO_RESOURCE_PREFIX)
321 && let RefKind::GetAtt { attr } = &edge.kind
322 && getatt_is_in_string_position(&edge.source_path)
323 {
324 output_getatt_refs.entry(output_name.to_string()).or_default().push((edge.target.clone(), attr.clone()));
325 }
326 }
327
328 outputs
329 .iter()
330 .map(|(name, output)| {
331 let mut getatt_refs = Vec::new();
337 collect_getatt_refs_string_position(&output.value, &mut getatt_refs);
338 if let Some(edge_refs) = output_getatt_refs.get(name) {
339 for (t, a) in edge_refs {
340 if !getatt_refs.iter().any(|(rt, ra)| rt == t && ra == a) {
341 getatt_refs.push((t.clone(), a.clone()));
342 }
343 }
344 }
345 (
346 name.clone(),
347 DiagnosticOutput {
348 value: JsonValue(resolved_value_to_json(&output.value)),
349 description: output.description.clone(),
350 condition: output.condition.clone(),
351 export_name: output.export_name.as_ref().map(|v| JsonValue(resolved_value_to_json(v))),
352 getatt_refs: getatt_refs
353 .into_iter()
354 .map(|(r, a)| GetAttRef { resource: r, attribute: a })
355 .collect(),
356 condition_refs: {
357 let mut crefs = Vec::new();
358 collect_condition_refs_from_resolved(&output.value, &mut crefs);
359 crefs.sort();
360 crefs.dedup();
361 crefs
362 },
363 },
364 )
365 })
366 .collect()
367}
368
369fn build_rule(r: &TemplateRule) -> DiagnosticRule {
370 DiagnosticRule {
371 name: r.name.clone(),
372 condition: r.condition.as_ref().map(|v| JsonValue(v.clone())),
373 assertions: r
374 .assertions
375 .iter()
376 .map(|a| DiagnosticRuleAssertion {
377 assert_expr: JsonValue(a.assert.clone()),
378 assert_description: a.description.clone(),
379 })
380 .collect(),
381 }
382}
383
384fn filter_sam_cycles(
385 raw_cycles: &[Vec<String>],
386 transforms: &[String],
387 resources: &HashMap<String, ResolvedResource>,
388) -> Vec<Vec<String>> {
389 let has_sam = transforms.iter().any(|t| t == TRANSFORM_SERVERLESS);
390 if has_sam {
391 raw_cycles
392 .iter()
393 .filter(|cycle| {
394 !cycle.iter().any(|rid| {
395 resources.get(rid).map(|r| r.resource_type.starts_with(SAM_SERVERLESS_TYPE_PREFIX)).unwrap_or(false)
396 })
397 })
398 .cloned()
399 .collect()
400 } else {
401 raw_cycles.to_vec()
402 }
403}
404
405pub fn resolved_value_to_json(val: &ResolvedValue) -> serde_json::Value {
406 match val {
407 ResolvedValue::Concrete { value: v } => v.0.clone(),
408 ResolvedValue::List { items } => serde_json::Value::Array(items.iter().map(resolved_value_to_json).collect()),
409 ResolvedValue::Map { entries } => {
410 let mut map = serde_json::Map::new();
411 for MapEntry { key: k, value: v } in entries {
412 map.insert(k.clone(), resolved_value_to_json(v));
413 }
414 serde_json::Value::Object(map)
415 }
416 ResolvedValue::Enum { variants: vals } => {
417 serde_json::json!({MARKER_ENUM: vals.iter().map(resolved_value_to_json).collect::<Vec<_>>()})
418 }
419 ResolvedValue::Conditional { condition: cond, if_true: t, if_false: f } => {
420 serde_json::json!({MARKER_CONDITIONAL: cond, MARKER_IF_TRUE: resolved_value_to_json(t), MARKER_IF_FALSE: resolved_value_to_json(f)})
421 }
422 ResolvedValue::Reference { target, kind } => {
423 let kind_str = match kind {
424 RefKind::Ref => "resource".to_string(),
425 RefKind::GetAtt { attr: a } => format!("getatt:{}", a),
426 RefKind::Sub { var: v } => format!("sub:{}", v),
427 RefKind::DependsOn => "dependson".to_string(),
428 };
429 serde_json::json!({MARKER_REF: target, MARKER_KIND: kind_str})
430 }
431 ResolvedValue::Dynamic { reason } => serde_json::json!({MARKER_DYNAMIC: reason}),
432 ResolvedValue::TypedDynamic { reason, param_type } => {
433 serde_json::json!({MARKER_DYNAMIC: reason, MARKER_PARAM_TYPE: param_type})
434 }
435 }
436}
437
438pub fn resolved_value_to_json_clean(val: &ResolvedValue) -> Option<serde_json::Value> {
439 match val {
440 ResolvedValue::Concrete { value: v } => Some(v.0.clone()),
441 ResolvedValue::List { items } => {
442 Some(serde_json::Value::Array(items.iter().filter_map(resolved_value_to_json_clean).collect()))
443 }
444 ResolvedValue::Map { entries } => {
445 let mut map = serde_json::Map::new();
446 for MapEntry { key: k, value: v } in entries {
447 if let Some(jv) = resolved_value_to_json_clean(v) {
448 map.insert(k.clone(), jv);
449 }
450 }
451 Some(serde_json::Value::Object(map))
452 }
453 ResolvedValue::Enum { variants: vals } => {
454 let concrete: Vec<serde_json::Value> = vals.iter().filter_map(resolved_value_to_json_clean).collect();
455 match concrete.len() {
456 0 => None,
457 1 => Some(concrete.into_iter().next().unwrap()),
458 _ => Some(serde_json::Value::Array(concrete)),
459 }
460 }
461 ResolvedValue::Conditional { condition: _, if_true: t, if_false: _ } => resolved_value_to_json_clean(t),
462 ResolvedValue::Dynamic { reason: _ }
463 | ResolvedValue::TypedDynamic { reason: _, param_type: _ }
464 | ResolvedValue::Reference { target: _, kind: _ } => None,
465 }
466}
467
468fn ref_kind_to_str(kind: &RefKind) -> (&'static str, Option<String>) {
469 match kind {
470 RefKind::Ref => (EDGE_KIND_REF, None),
471 RefKind::GetAtt { attr } => (EDGE_KIND_GET_ATT, Some(attr.clone())),
472 RefKind::Sub { var } => (EDGE_KIND_SUB, Some(var.clone())),
473 RefKind::DependsOn => (EDGE_KIND_DEPENDS_ON, None),
474 }
475}
476
477fn collect_getatt_refs_string_position(val: &ResolvedValue, out: &mut Vec<(String, String)>) {
484 match val {
485 ResolvedValue::Reference { target, kind: RefKind::GetAtt { attr } } => out.push((target.clone(), attr.clone())),
486 ResolvedValue::Conditional { condition: _, if_true: t, if_false: f } => {
487 collect_getatt_refs_string_position(t, out);
488 collect_getatt_refs_string_position(f, out);
489 }
490 _ => {}
491 }
492}
493
494fn getatt_is_in_string_position(source_path: &str) -> bool {
503 let after_value = source_path
508 .rsplit_once("/Value")
509 .map(|(_, tail)| tail)
510 .or_else(|| source_path.strip_prefix("Value"))
511 .unwrap_or("");
512 let mut segments = after_value.split('.').filter(|s| !s.is_empty());
513 while let Some(segment) = segments.next() {
514 if segment == FN_IF {
515 segments.next();
517 continue;
518 }
519 if segment.starts_with("Fn::") {
522 return true;
523 }
524 return false;
526 }
527 true
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use serde_json::json;
534
535 #[test]
536 fn to_json_concrete() {
537 assert_eq!(resolved_value_to_json(&ResolvedValue::Concrete { value: json!("hi").into() }), json!("hi"));
538 assert_eq!(resolved_value_to_json(&ResolvedValue::Concrete { value: json!(42).into() }), json!(42));
539 }
540
541 #[test]
542 fn to_json_list() {
543 let val = ResolvedValue::List {
544 items: vec![
545 ResolvedValue::Concrete { value: json!(1).into() },
546 ResolvedValue::Concrete { value: json!(2).into() },
547 ],
548 };
549 assert_eq!(resolved_value_to_json(&val), json!([1, 2]));
550 }
551
552 #[test]
553 fn to_json_map() {
554 let val = ResolvedValue::Map {
555 entries: vec![MapEntry { key: "k".into(), value: ResolvedValue::Concrete { value: json!("v").into() } }],
556 };
557 assert_eq!(resolved_value_to_json(&val), json!({"k": "v"}));
558 }
559
560 #[test]
561 fn to_json_enum_uses_marker() {
562 let val = ResolvedValue::Enum { variants: vec![ResolvedValue::Concrete { value: json!("a").into() }] };
563 let j = resolved_value_to_json(&val);
564 assert_ne!(j.get(MARKER_ENUM), None, "expected MARKER_ENUM key in serialized enum");
565 }
566
567 #[test]
568 fn to_json_conditional_uses_markers() {
569 let val = ResolvedValue::Conditional {
570 condition: "C".into(),
571 if_true: Box::new(ResolvedValue::Concrete { value: json!(1).into() }),
572 if_false: Box::new(ResolvedValue::Concrete { value: json!(2).into() }),
573 };
574 let j = resolved_value_to_json(&val);
575 assert_eq!(j[MARKER_CONDITIONAL], "C");
576 assert_eq!(j[MARKER_IF_TRUE], 1);
577 assert_eq!(j[MARKER_IF_FALSE], 2);
578 }
579
580 #[test]
581 fn to_json_reference_uses_markers() {
582 let val = ResolvedValue::Reference { target: "R".into(), kind: RefKind::GetAtt { attr: "Arn".into() } };
583 let j = resolved_value_to_json(&val);
584 assert_eq!(j[MARKER_REF], "R");
585 assert_eq!(j[MARKER_KIND], "getatt:Arn");
586 }
587
588 #[test]
589 fn to_json_dynamic_uses_marker() {
590 let val = ResolvedValue::Dynamic { reason: "reason".into() };
591 assert_eq!(resolved_value_to_json(&val)[MARKER_DYNAMIC], "reason");
592 }
593
594 #[test]
595 fn to_json_typed_dynamic_includes_param_type() {
596 let val = ResolvedValue::TypedDynamic { reason: "reason".into(), param_type: "Number".into() };
597 let j = resolved_value_to_json(&val);
598 assert_eq!(j[MARKER_DYNAMIC], "reason");
599 assert_eq!(j[MARKER_PARAM_TYPE], "Number");
600 }
601
602 #[test]
603 fn to_json_clean_concrete() {
604 assert_eq!(
605 resolved_value_to_json_clean(&ResolvedValue::Concrete { value: json!("x").into() }),
606 Some(json!("x"))
607 );
608 }
609
610 #[test]
611 fn to_json_clean_dynamic_returns_none() {
612 assert_eq!(
613 resolved_value_to_json_clean(&ResolvedValue::Dynamic { reason: "x".into() }),
614 None,
615 "Dynamic should return None"
616 );
617 }
618
619 #[test]
620 fn to_json_clean_reference_returns_none() {
621 assert_eq!(
622 resolved_value_to_json_clean(&ResolvedValue::Reference { target: "R".into(), kind: RefKind::Ref }),
623 None,
624 "Reference should return None"
625 );
626 }
627
628 #[test]
629 fn to_json_clean_conditional_takes_true_branch() {
630 let val = ResolvedValue::Conditional {
631 condition: "C".into(),
632 if_true: Box::new(ResolvedValue::Concrete { value: json!("yes").into() }),
633 if_false: Box::new(ResolvedValue::Concrete { value: json!("no").into() }),
634 };
635 assert_eq!(resolved_value_to_json_clean(&val), Some(json!("yes")));
636 }
637
638 #[test]
639 fn to_json_clean_enum_single_unwraps() {
640 let val = ResolvedValue::Enum { variants: vec![ResolvedValue::Concrete { value: json!("only").into() }] };
641 assert_eq!(resolved_value_to_json_clean(&val), Some(json!("only")));
642 }
643
644 #[test]
645 fn to_json_clean_enum_multiple_returns_array() {
646 let val = ResolvedValue::Enum {
647 variants: vec![
648 ResolvedValue::Concrete { value: json!("a").into() },
649 ResolvedValue::Concrete { value: json!("b").into() },
650 ],
651 };
652 assert_eq!(resolved_value_to_json_clean(&val), Some(json!(["a", "b"])));
653 }
654
655 #[test]
656 fn to_json_clean_enum_all_dynamic_returns_none() {
657 let val = ResolvedValue::Enum { variants: vec![ResolvedValue::Dynamic { reason: "x".into() }] };
658 assert_eq!(resolved_value_to_json_clean(&val), None, "all-dynamic enum should return None");
659 }
660
661 #[test]
662 fn to_json_clean_list_filters_dynamic() {
663 let val = ResolvedValue::List {
664 items: vec![
665 ResolvedValue::Concrete { value: json!(1).into() },
666 ResolvedValue::Dynamic { reason: "x".into() },
667 ],
668 };
669 assert_eq!(resolved_value_to_json_clean(&val), Some(json!([1])));
670 }
671
672 #[test]
673 fn to_json_clean_map_filters_dynamic() {
674 let val = ResolvedValue::Map {
675 entries: vec![
676 MapEntry { key: "a".into(), value: ResolvedValue::Concrete { value: json!(1).into() } },
677 MapEntry { key: "b".into(), value: ResolvedValue::Dynamic { reason: "x".into() } },
678 ],
679 };
680 assert_eq!(resolved_value_to_json_clean(&val), Some(json!({"a": 1})));
681 }
682
683 #[test]
684 fn ref_kind_to_str_all_variants() {
685 assert_eq!(ref_kind_to_str(&RefKind::Ref), ("Ref", None));
686 assert_eq!(ref_kind_to_str(&RefKind::GetAtt { attr: "A".into() }), ("GetAtt", Some("A".into())));
687 assert_eq!(ref_kind_to_str(&RefKind::Sub { var: "V".into() }), ("Sub", Some("V".into())));
688 assert_eq!(ref_kind_to_str(&RefKind::DependsOn), ("DependsOn", None));
689 }
690
691 #[test]
692 fn to_json_reference_sub_kind() {
693 let val = ResolvedValue::Reference { target: "R".into(), kind: RefKind::Sub { var: "V".into() } };
694 let j = resolved_value_to_json(&val);
695 assert_eq!(j[MARKER_KIND], "sub:V");
696 }
697
698 #[test]
699 fn to_json_reference_dependson_kind() {
700 let val = ResolvedValue::Reference { target: "R".into(), kind: RefKind::DependsOn };
701 let j = resolved_value_to_json(&val);
702 assert_eq!(j[MARKER_KIND], "dependson");
703 }
704
705 #[test]
706 fn getatt_string_position_paths() {
707 assert!(getatt_is_in_string_position("Outputs/O/Value"));
712 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::If.1"));
713 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::Join.1.0"));
714 assert!(!getatt_is_in_string_position("Outputs/O/Value.0"));
715 assert!(!getatt_is_in_string_position("Outputs/O/Value.k"));
716 assert!(!getatt_is_in_string_position("Outputs/O/Value.Fn::If.2.0"));
717 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::GetAtt"));
719 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::Sub"));
720 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::Join.1.0.Fn::GetAtt"));
721 assert!(!getatt_is_in_string_position("Outputs/O/Value.0.Fn::GetAtt"));
723 }
724
725 #[test]
726 fn getatt_string_position_value_prefixed_output_names() {
727 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value"));
731 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::Join.1.0"));
732 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::If.1"));
733 assert!(!getatt_is_in_string_position("Outputs/ValueFoo/Value.0"));
734 assert!(!getatt_is_in_string_position("Outputs/ValueFoo/Value.k"));
735 assert!(getatt_is_in_string_position("Outputs/Value/Value"));
737 assert!(getatt_is_in_string_position("Outputs/Value/Value.Fn::Join.1.0"));
738 assert!(!getatt_is_in_string_position("Outputs/Value/Value.0"));
739 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::GetAtt"));
741 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::Join.1.0.Fn::GetAtt"));
742 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::Sub"));
743 }
744}