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_edges(id)
155 .map(|e| {
156 let (kind, attr) = ref_kind_to_str(&e.kind);
157 OutgoingRef {
158 source_path: e.source_path.clone(),
159 target: e.target.clone(),
160 kind: kind.to_string(),
161 attr,
162 condition_context: e.condition_context.clone(),
163 }
164 })
165 .collect();
166
167 let incoming = graph
168 .incoming(id)
169 .iter()
170 .map(|e| {
171 let (kind, attr) = ref_kind_to_str(&e.kind);
172 IncomingRef {
173 source: e.source_resource.clone(),
174 source_path: e.source_path.clone(),
175 kind: kind.to_string(),
176 attr,
177 }
178 })
179 .collect();
180
181 (
182 id.clone(),
183 DiagnosticResource {
184 resource_type: res.resource_type.clone(),
185 condition: res.condition.clone(),
186 depends_on: res.depends_on.clone(),
187 deletion_policy: res.deletion_policy.as_ref().map(|v| JsonValue(resolved_value_to_json(v))),
188 update_replace_policy: res
189 .update_replace_policy
190 .as_ref()
191 .map(|v| JsonValue(resolved_value_to_json(v))),
192 creation_policy: res.creation_policy.as_ref().map(|v| JsonValue(serde_json::json!(v))),
193 update_policy: res.update_policy.as_ref().map(|v| JsonValue(serde_json::json!(v))),
194 properties: props,
195 outgoing_refs: outgoing,
196 incoming_refs: incoming,
197 find_in_map_refs: res.diagnostics.find_in_map_refs.clone(),
198 simple_subs: res
199 .diagnostics
200 .simple_subs
201 .iter()
202 .map(|s| PathVariable { path: s.path.clone(), variable: s.value.clone() })
203 .collect(),
204 redundant_subs: res.diagnostics.redundant_subs.clone(),
205 empty_joins: res.diagnostics.empty_joins.clone(),
206 hardcoded_partition_arns: res.diagnostics.hardcoded_partition_arns.clone(),
207 conditionally_null_props: res
208 .diagnostics
209 .conditionally_null_props
210 .iter()
211 .map(|s| ConditionalNull {
212 path: s.path.clone(),
213 condition: s.condition.clone(),
214 null_in_true: s.null_in_true_branch,
215 })
216 .collect(),
217 condition_refs: res.diagnostics.condition_refs.clone(),
218 for_each_expansions: res
219 .diagnostics
220 .foreach_expansions
221 .iter()
222 .map(|fe| DiagnosticForEachExpansion {
223 path: fe.property_path.clone(),
224 identifier: fe.identifier.clone(),
225 collection: fe.collection_source.clone(),
226 })
227 .collect(),
228 unsubstituted_variables: res
229 .diagnostics
230 .unsubstituted_variables
231 .iter()
232 .map(|s| PathVariable { path: s.path.clone(), variable: s.value.clone() })
233 .collect(),
234 unused_sub_keys: res
235 .diagnostics
236 .unused_sub_keys
237 .iter()
238 .map(|s| PathVariable { path: s.path.clone(), variable: s.value.clone() })
239 .collect(),
240 raw_pseudo_params: res
241 .diagnostics
242 .raw_pseudo_params
243 .iter()
244 .map(|s| PathVariable { path: s.path.clone(), variable: s.value.clone() })
245 .collect(),
246 secretsmanager_ref_paths: res.diagnostics.secretsmanager_ref_paths.clone(),
247 invalid_refs: res
248 .diagnostics
249 .invalid_refs
250 .iter()
251 .map(|s| PathTarget { path: s.path.clone(), target: s.value.clone() })
252 .collect(),
253 },
254 )
255 })
256 .collect()
257}
258
259fn build_conditions(conditions: &crate::conditions::ConditionModel) -> HashMap<String, DiagnosticCondition> {
260 let mut out = HashMap::new();
261 for name in conditions.names().filter(|n| !n.starts_with("__")) {
262 let (expression, deps) = if let Some(expr) = conditions.get(name) {
263 let mut d = Vec::new();
264 crate::conditions::collect_condition_deps(expr, &mut d);
265 d.sort();
266 d.dedup();
267 (Some(crate::conditions::format_condition_expr(expr)), if d.is_empty() { None } else { Some(d) })
268 } else {
269 (None, None)
270 };
271
272 let mut mutex_with: Vec<String> = Vec::new();
273 for group in &conditions.mutex_groups {
274 if group.conditions.contains(&name.to_string()) {
275 for peer in &group.conditions {
276 if peer != name {
277 mutex_with.push(peer.clone());
278 }
279 }
280 }
281 }
282
283 out.insert(
284 name.to_string(),
285 DiagnosticCondition {
286 expression,
287 deps,
288 mutex_with: if mutex_with.is_empty() { None } else { Some(mutex_with) },
289 },
290 );
291 }
292 out
293}
294
295fn build_edges(graph: &crate::graph::ReferenceGraph) -> Vec<ReferenceEdge> {
296 graph
297 .edges
298 .iter()
299 .map(|e| {
300 let (kind, attr) = ref_kind_to_str(&e.kind);
301 ReferenceEdge {
302 source: e.source_resource.clone(),
303 source_path: e.source_path.clone(),
304 target: e.target.clone(),
305 kind: kind.to_string(),
306 attr,
307 condition_context: e.condition_context.clone(),
308 }
309 })
310 .collect()
311}
312
313fn build_outputs(
314 outputs: &HashMap<String, ResolvedOutput>,
315 graph: &crate::graph::ReferenceGraph,
316) -> HashMap<String, DiagnosticOutput> {
317 let mut output_getatt_refs: HashMap<String, Vec<(String, String)>> = HashMap::new();
318 for edge in &graph.edges {
319 if let Some(output_name) = edge.source_resource.strip_prefix(OUTPUT_PSEUDO_RESOURCE_PREFIX)
320 && let RefKind::GetAtt { attr } = &edge.kind
321 && getatt_is_in_string_position(&edge.source_path)
322 {
323 output_getatt_refs.entry(output_name.to_string()).or_default().push((edge.target.clone(), attr.clone()));
324 }
325 }
326
327 outputs
328 .iter()
329 .map(|(name, output)| {
330 let mut getatt_refs = Vec::new();
336 collect_getatt_refs_string_position(&output.value, &mut getatt_refs);
337 if let Some(edge_refs) = output_getatt_refs.get(name) {
338 for (t, a) in edge_refs {
339 if !getatt_refs.iter().any(|(rt, ra)| rt == t && ra == a) {
340 getatt_refs.push((t.clone(), a.clone()));
341 }
342 }
343 }
344 (
345 name.clone(),
346 DiagnosticOutput {
347 value: JsonValue(resolved_value_to_json(&output.value)),
348 description: output.description.clone(),
349 condition: output.condition.clone(),
350 export_name: output.export_name.as_ref().map(|v| JsonValue(resolved_value_to_json(v))),
351 getatt_refs: getatt_refs
352 .into_iter()
353 .map(|(r, a)| GetAttRef { resource: r, attribute: a })
354 .collect(),
355 condition_refs: {
356 let mut crefs = Vec::new();
357 collect_condition_refs_from_resolved(&output.value, &mut crefs);
358 crefs.sort();
359 crefs.dedup();
360 crefs
361 },
362 },
363 )
364 })
365 .collect()
366}
367
368fn build_rule(r: &TemplateRule) -> DiagnosticRule {
369 DiagnosticRule {
370 name: r.name.clone(),
371 condition: r.condition.as_ref().map(|v| JsonValue(v.clone())),
372 assertions: r
373 .assertions
374 .iter()
375 .map(|a| DiagnosticRuleAssertion {
376 assert_expr: JsonValue(a.assert.clone()),
377 assert_description: a.description.clone(),
378 })
379 .collect(),
380 }
381}
382
383fn filter_sam_cycles(
384 raw_cycles: &[Vec<String>],
385 transforms: &[String],
386 resources: &HashMap<String, ResolvedResource>,
387) -> Vec<Vec<String>> {
388 let has_sam = transforms.iter().any(|t| t == TRANSFORM_SERVERLESS);
389 if has_sam {
390 raw_cycles
391 .iter()
392 .filter(|cycle| {
393 !cycle.iter().any(|rid| {
394 resources.get(rid).map(|r| r.resource_type.starts_with(SAM_SERVERLESS_TYPE_PREFIX)).unwrap_or(false)
395 })
396 })
397 .cloned()
398 .collect()
399 } else {
400 raw_cycles.to_vec()
401 }
402}
403
404pub fn resolved_value_to_json(val: &ResolvedValue) -> serde_json::Value {
405 match val {
406 ResolvedValue::Concrete { value: v } => v.0.clone(),
407 ResolvedValue::List { items } => serde_json::Value::Array(items.iter().map(resolved_value_to_json).collect()),
408 ResolvedValue::Map { entries } => {
409 let mut map = serde_json::Map::new();
410 for MapEntry { key: k, value: v } in entries {
411 map.insert(k.clone(), resolved_value_to_json(v));
412 }
413 serde_json::Value::Object(map)
414 }
415 ResolvedValue::Enum { variants: vals } => {
416 serde_json::json!({MARKER_ENUM: vals.iter().map(resolved_value_to_json).collect::<Vec<_>>()})
417 }
418 ResolvedValue::Conditional { condition: cond, if_true: t, if_false: f } => {
419 serde_json::json!({MARKER_CONDITIONAL: cond, MARKER_IF_TRUE: resolved_value_to_json(t), MARKER_IF_FALSE: resolved_value_to_json(f)})
420 }
421 ResolvedValue::Reference { target, kind } => {
422 let kind_str = match kind {
423 RefKind::Ref => "resource".to_string(),
424 RefKind::GetAtt { attr: a } => format!("getatt:{}", a),
425 RefKind::Sub { var: v } => format!("sub:{}", v),
426 RefKind::DependsOn => "dependson".to_string(),
427 };
428 serde_json::json!({MARKER_REF: target, MARKER_KIND: kind_str})
429 }
430 ResolvedValue::Dynamic { reason } => serde_json::json!({MARKER_DYNAMIC: reason}),
431 ResolvedValue::TypedDynamic { reason, param_type } => {
432 serde_json::json!({MARKER_DYNAMIC: reason, MARKER_PARAM_TYPE: param_type})
433 }
434 }
435}
436
437pub fn resolved_value_to_json_clean(val: &ResolvedValue) -> Option<serde_json::Value> {
438 match val {
439 ResolvedValue::Concrete { value: v } => Some(v.0.clone()),
440 ResolvedValue::List { items } => {
441 Some(serde_json::Value::Array(items.iter().filter_map(resolved_value_to_json_clean).collect()))
442 }
443 ResolvedValue::Map { entries } => {
444 let mut map = serde_json::Map::new();
445 for MapEntry { key: k, value: v } in entries {
446 if let Some(jv) = resolved_value_to_json_clean(v) {
447 map.insert(k.clone(), jv);
448 }
449 }
450 Some(serde_json::Value::Object(map))
451 }
452 ResolvedValue::Enum { variants: vals } => {
453 let concrete: Vec<serde_json::Value> = vals.iter().filter_map(resolved_value_to_json_clean).collect();
454 match concrete.len() {
455 0 => None,
456 1 => Some(concrete.into_iter().next().unwrap()),
457 _ => Some(serde_json::Value::Array(concrete)),
458 }
459 }
460 ResolvedValue::Conditional { condition: _, if_true: t, if_false: _ } => resolved_value_to_json_clean(t),
461 ResolvedValue::Dynamic { reason: _ }
462 | ResolvedValue::TypedDynamic { reason: _, param_type: _ }
463 | ResolvedValue::Reference { target: _, kind: _ } => None,
464 }
465}
466
467fn ref_kind_to_str(kind: &RefKind) -> (&'static str, Option<String>) {
468 match kind {
469 RefKind::Ref => (EDGE_KIND_REF, None),
470 RefKind::GetAtt { attr } => (EDGE_KIND_GET_ATT, Some(attr.clone())),
471 RefKind::Sub { var } => (EDGE_KIND_SUB, Some(var.clone())),
472 RefKind::DependsOn => (EDGE_KIND_DEPENDS_ON, None),
473 }
474}
475
476fn collect_getatt_refs_string_position(val: &ResolvedValue, out: &mut Vec<(String, String)>) {
483 match val {
484 ResolvedValue::Reference { target, kind: RefKind::GetAtt { attr } } => out.push((target.clone(), attr.clone())),
485 ResolvedValue::Conditional { condition: _, if_true: t, if_false: f } => {
486 collect_getatt_refs_string_position(t, out);
487 collect_getatt_refs_string_position(f, out);
488 }
489 _ => {}
490 }
491}
492
493fn getatt_is_in_string_position(source_path: &str) -> bool {
502 let after_value = source_path
507 .rsplit_once("/Value")
508 .map(|(_, tail)| tail)
509 .or_else(|| source_path.strip_prefix("Value"))
510 .unwrap_or("");
511 let mut segments = after_value.split('.').filter(|s| !s.is_empty());
512 while let Some(segment) = segments.next() {
513 if segment == FN_IF {
514 segments.next();
516 continue;
517 }
518 if segment.starts_with("Fn::") {
521 return true;
522 }
523 return false;
525 }
526 true
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use serde_json::json;
533
534 #[test]
535 fn to_json_concrete() {
536 assert_eq!(resolved_value_to_json(&ResolvedValue::Concrete { value: json!("hi").into() }), json!("hi"));
537 assert_eq!(resolved_value_to_json(&ResolvedValue::Concrete { value: json!(42).into() }), json!(42));
538 }
539
540 #[test]
541 fn to_json_list() {
542 let val = ResolvedValue::List {
543 items: vec![
544 ResolvedValue::Concrete { value: json!(1).into() },
545 ResolvedValue::Concrete { value: json!(2).into() },
546 ],
547 };
548 assert_eq!(resolved_value_to_json(&val), json!([1, 2]));
549 }
550
551 #[test]
552 fn to_json_map() {
553 let val = ResolvedValue::Map {
554 entries: vec![MapEntry { key: "k".into(), value: ResolvedValue::Concrete { value: json!("v").into() } }],
555 };
556 assert_eq!(resolved_value_to_json(&val), json!({"k": "v"}));
557 }
558
559 #[test]
560 fn to_json_enum_uses_marker() {
561 let val = ResolvedValue::Enum { variants: vec![ResolvedValue::Concrete { value: json!("a").into() }] };
562 let j = resolved_value_to_json(&val);
563 assert_ne!(j.get(MARKER_ENUM), None, "expected MARKER_ENUM key in serialized enum");
564 }
565
566 #[test]
567 fn to_json_conditional_uses_markers() {
568 let val = ResolvedValue::Conditional {
569 condition: "C".into(),
570 if_true: Box::new(ResolvedValue::Concrete { value: json!(1).into() }),
571 if_false: Box::new(ResolvedValue::Concrete { value: json!(2).into() }),
572 };
573 let j = resolved_value_to_json(&val);
574 assert_eq!(j[MARKER_CONDITIONAL], "C");
575 assert_eq!(j[MARKER_IF_TRUE], 1);
576 assert_eq!(j[MARKER_IF_FALSE], 2);
577 }
578
579 #[test]
580 fn to_json_reference_uses_markers() {
581 let val = ResolvedValue::Reference { target: "R".into(), kind: RefKind::GetAtt { attr: "Arn".into() } };
582 let j = resolved_value_to_json(&val);
583 assert_eq!(j[MARKER_REF], "R");
584 assert_eq!(j[MARKER_KIND], "getatt:Arn");
585 }
586
587 #[test]
588 fn to_json_dynamic_uses_marker() {
589 let val = ResolvedValue::Dynamic { reason: "reason".into() };
590 assert_eq!(resolved_value_to_json(&val)[MARKER_DYNAMIC], "reason");
591 }
592
593 #[test]
594 fn to_json_typed_dynamic_includes_param_type() {
595 let val = ResolvedValue::TypedDynamic { reason: "reason".into(), param_type: "Number".into() };
596 let j = resolved_value_to_json(&val);
597 assert_eq!(j[MARKER_DYNAMIC], "reason");
598 assert_eq!(j[MARKER_PARAM_TYPE], "Number");
599 }
600
601 #[test]
602 fn to_json_clean_concrete() {
603 assert_eq!(
604 resolved_value_to_json_clean(&ResolvedValue::Concrete { value: json!("x").into() }),
605 Some(json!("x"))
606 );
607 }
608
609 #[test]
610 fn to_json_clean_dynamic_returns_none() {
611 assert_eq!(
612 resolved_value_to_json_clean(&ResolvedValue::Dynamic { reason: "x".into() }),
613 None,
614 "Dynamic should return None"
615 );
616 }
617
618 #[test]
619 fn to_json_clean_reference_returns_none() {
620 assert_eq!(
621 resolved_value_to_json_clean(&ResolvedValue::Reference { target: "R".into(), kind: RefKind::Ref }),
622 None,
623 "Reference should return None"
624 );
625 }
626
627 #[test]
628 fn to_json_clean_conditional_takes_true_branch() {
629 let val = ResolvedValue::Conditional {
630 condition: "C".into(),
631 if_true: Box::new(ResolvedValue::Concrete { value: json!("yes").into() }),
632 if_false: Box::new(ResolvedValue::Concrete { value: json!("no").into() }),
633 };
634 assert_eq!(resolved_value_to_json_clean(&val), Some(json!("yes")));
635 }
636
637 #[test]
638 fn to_json_clean_enum_single_unwraps() {
639 let val = ResolvedValue::Enum { variants: vec![ResolvedValue::Concrete { value: json!("only").into() }] };
640 assert_eq!(resolved_value_to_json_clean(&val), Some(json!("only")));
641 }
642
643 #[test]
644 fn to_json_clean_enum_multiple_returns_array() {
645 let val = ResolvedValue::Enum {
646 variants: vec![
647 ResolvedValue::Concrete { value: json!("a").into() },
648 ResolvedValue::Concrete { value: json!("b").into() },
649 ],
650 };
651 assert_eq!(resolved_value_to_json_clean(&val), Some(json!(["a", "b"])));
652 }
653
654 #[test]
655 fn to_json_clean_enum_all_dynamic_returns_none() {
656 let val = ResolvedValue::Enum { variants: vec![ResolvedValue::Dynamic { reason: "x".into() }] };
657 assert_eq!(resolved_value_to_json_clean(&val), None, "all-dynamic enum should return None");
658 }
659
660 #[test]
661 fn to_json_clean_list_filters_dynamic() {
662 let val = ResolvedValue::List {
663 items: vec![
664 ResolvedValue::Concrete { value: json!(1).into() },
665 ResolvedValue::Dynamic { reason: "x".into() },
666 ],
667 };
668 assert_eq!(resolved_value_to_json_clean(&val), Some(json!([1])));
669 }
670
671 #[test]
672 fn to_json_clean_map_filters_dynamic() {
673 let val = ResolvedValue::Map {
674 entries: vec![
675 MapEntry { key: "a".into(), value: ResolvedValue::Concrete { value: json!(1).into() } },
676 MapEntry { key: "b".into(), value: ResolvedValue::Dynamic { reason: "x".into() } },
677 ],
678 };
679 assert_eq!(resolved_value_to_json_clean(&val), Some(json!({"a": 1})));
680 }
681
682 #[test]
683 fn ref_kind_to_str_all_variants() {
684 assert_eq!(ref_kind_to_str(&RefKind::Ref), ("Ref", None));
685 assert_eq!(ref_kind_to_str(&RefKind::GetAtt { attr: "A".into() }), ("GetAtt", Some("A".into())));
686 assert_eq!(ref_kind_to_str(&RefKind::Sub { var: "V".into() }), ("Sub", Some("V".into())));
687 assert_eq!(ref_kind_to_str(&RefKind::DependsOn), ("DependsOn", None));
688 }
689
690 #[test]
691 fn to_json_reference_sub_kind() {
692 let val = ResolvedValue::Reference { target: "R".into(), kind: RefKind::Sub { var: "V".into() } };
693 let j = resolved_value_to_json(&val);
694 assert_eq!(j[MARKER_KIND], "sub:V");
695 }
696
697 #[test]
698 fn to_json_reference_dependson_kind() {
699 let val = ResolvedValue::Reference { target: "R".into(), kind: RefKind::DependsOn };
700 let j = resolved_value_to_json(&val);
701 assert_eq!(j[MARKER_KIND], "dependson");
702 }
703
704 #[test]
705 fn getatt_string_position_paths() {
706 assert!(getatt_is_in_string_position("Outputs/O/Value"));
711 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::If.1"));
712 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::Join.1.0"));
713 assert!(!getatt_is_in_string_position("Outputs/O/Value.0"));
714 assert!(!getatt_is_in_string_position("Outputs/O/Value.k"));
715 assert!(!getatt_is_in_string_position("Outputs/O/Value.Fn::If.2.0"));
716 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::GetAtt"));
718 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::Sub"));
719 assert!(getatt_is_in_string_position("Outputs/O/Value.Fn::Join.1.0.Fn::GetAtt"));
720 assert!(!getatt_is_in_string_position("Outputs/O/Value.0.Fn::GetAtt"));
722 }
723
724 #[test]
725 fn getatt_string_position_value_prefixed_output_names() {
726 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value"));
730 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::Join.1.0"));
731 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::If.1"));
732 assert!(!getatt_is_in_string_position("Outputs/ValueFoo/Value.0"));
733 assert!(!getatt_is_in_string_position("Outputs/ValueFoo/Value.k"));
734 assert!(getatt_is_in_string_position("Outputs/Value/Value"));
736 assert!(getatt_is_in_string_position("Outputs/Value/Value.Fn::Join.1.0"));
737 assert!(!getatt_is_in_string_position("Outputs/Value/Value.0"));
738 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::GetAtt"));
740 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::Join.1.0.Fn::GetAtt"));
741 assert!(getatt_is_in_string_position("Outputs/ValueFoo/Value.Fn::Sub"));
742 }
743}