1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
9#[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))]
10#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))]
11pub enum TopLevelSection {
12 Resources,
13 Parameters,
14 Outputs,
15 Mappings,
16 Metadata,
17 Rules,
18 Conditions,
19 Transform,
20 FormatVersion,
21 Description,
22}
23
24impl TopLevelSection {
25 const ALL: [TopLevelSection; 10] = [
26 TopLevelSection::Resources,
27 TopLevelSection::Parameters,
28 TopLevelSection::Outputs,
29 TopLevelSection::Mappings,
30 TopLevelSection::Metadata,
31 TopLevelSection::Rules,
32 TopLevelSection::Conditions,
33 TopLevelSection::Transform,
34 TopLevelSection::FormatVersion,
35 TopLevelSection::Description,
36 ];
37
38 pub const fn name(self) -> &'static str {
40 match self {
41 TopLevelSection::Resources => "Resources",
42 TopLevelSection::Parameters => "Parameters",
43 TopLevelSection::Outputs => "Outputs",
44 TopLevelSection::Mappings => "Mappings",
45 TopLevelSection::Metadata => "Metadata",
46 TopLevelSection::Rules => "Rules",
47 TopLevelSection::Conditions => "Conditions",
48 TopLevelSection::Transform => "Transform",
49 TopLevelSection::FormatVersion => "AWSTemplateFormatVersion",
50 TopLevelSection::Description => "Description",
51 }
52 }
53
54 pub fn from_name(name: &str) -> Option<TopLevelSection> {
56 Self::ALL.into_iter().find(|section| section.name() == name)
57 }
58
59 pub const fn entity_type(self) -> EntityType {
61 match self {
62 TopLevelSection::Resources => EntityType::Resource,
63 TopLevelSection::Parameters => EntityType::Parameter,
64 TopLevelSection::Outputs => EntityType::Output,
65 TopLevelSection::Mappings => EntityType::Mapping,
66 TopLevelSection::Metadata => EntityType::Metadata,
67 TopLevelSection::Rules => EntityType::Rule,
68 TopLevelSection::Conditions => EntityType::Condition,
69 TopLevelSection::Transform => EntityType::Transform,
70 TopLevelSection::FormatVersion => EntityType::FormatVersion,
71 TopLevelSection::Description => EntityType::Description,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
82#[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))]
83#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))]
84pub enum EntityType {
85 Resource,
86 Parameter,
87 Output,
88 Mapping,
89 Metadata,
90 Rule,
91 Condition,
92 Transform,
93 FormatVersion,
94 Description,
95}
96
97impl EntityType {
98 const ALL: [EntityType; 10] = [
99 EntityType::Resource,
100 EntityType::Parameter,
101 EntityType::Output,
102 EntityType::Mapping,
103 EntityType::Metadata,
104 EntityType::Rule,
105 EntityType::Condition,
106 EntityType::Transform,
107 EntityType::FormatVersion,
108 EntityType::Description,
109 ];
110
111 pub const fn name(self) -> &'static str {
114 match self {
115 EntityType::Resource => "Resource",
116 EntityType::Parameter => "Parameter",
117 EntityType::Output => "Output",
118 EntityType::Mapping => "Mapping",
119 EntityType::Metadata => "Metadata",
120 EntityType::Rule => "Rule",
121 EntityType::Condition => "Condition",
122 EntityType::Transform => "Transform",
123 EntityType::FormatVersion => "FormatVersion",
124 EntityType::Description => "Description",
125 }
126 }
127
128 pub const fn section(self) -> TopLevelSection {
130 match self {
131 EntityType::Resource => TopLevelSection::Resources,
132 EntityType::Parameter => TopLevelSection::Parameters,
133 EntityType::Output => TopLevelSection::Outputs,
134 EntityType::Mapping => TopLevelSection::Mappings,
135 EntityType::Metadata => TopLevelSection::Metadata,
136 EntityType::Rule => TopLevelSection::Rules,
137 EntityType::Condition => TopLevelSection::Conditions,
138 EntityType::Transform => TopLevelSection::Transform,
139 EntityType::FormatVersion => TopLevelSection::FormatVersion,
140 EntityType::Description => TopLevelSection::Description,
141 }
142 }
143
144 pub fn from_section(section: &str) -> Option<EntityType> {
147 TopLevelSection::from_name(section).map(TopLevelSection::entity_type)
148 }
149}
150
151impl std::str::FromStr for EntityType {
152 type Err = String;
153
154 fn from_str(s: &str) -> Result<Self, Self::Err> {
155 Self::ALL.into_iter().find(|entity_type| entity_type.name().eq_ignore_ascii_case(s)).ok_or_else(|| {
156 format!("Invalid entity type '{s}'; expected one of: {}", Self::ALL.map(EntityType::name).join(", "))
157 })
158 }
159}
160
161pub fn entity_identity(path: &str) -> Option<(EntityType, &str)> {
168 let mut segments = path.split('/');
169 let entity_type = EntityType::from_section(segments.next()?)?;
170 let logical_id = segments.next().filter(|id| !id.is_empty())?;
171 Some((entity_type, logical_id))
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn section_names_round_trip_through_from_name() {
180 for section in TopLevelSection::ALL {
181 assert_eq!(TopLevelSection::from_name(section.name()), Some(section), "{section:?}");
182 }
183 assert_eq!(TopLevelSection::from_name("Globals"), None, "SAM Globals is not a documented section");
184 assert_eq!(TopLevelSection::from_name("Constants"), None);
185 assert_eq!(TopLevelSection::from_name(""), None);
186 }
187
188 #[test]
189 fn format_version_uses_the_full_template_key() {
190 assert_eq!(TopLevelSection::FormatVersion.name(), "AWSTemplateFormatVersion");
191 assert_eq!(TopLevelSection::from_name("AWSTemplateFormatVersion"), Some(TopLevelSection::FormatVersion));
192 }
193
194 #[test]
195 fn entity_type_and_section_are_inverse_mappings() {
196 for section in TopLevelSection::ALL {
197 assert_eq!(section.entity_type().section(), section, "{section:?}");
198 }
199 }
200
201 #[test]
202 fn from_section_maps_names_to_singular_entity_types() {
203 assert_eq!(EntityType::from_section("Resources"), Some(EntityType::Resource));
204 assert_eq!(EntityType::from_section("Parameters"), Some(EntityType::Parameter));
205 assert_eq!(EntityType::from_section("Outputs"), Some(EntityType::Output));
206 assert_eq!(EntityType::from_section("Mappings"), Some(EntityType::Mapping));
207 assert_eq!(EntityType::from_section("Conditions"), Some(EntityType::Condition));
208 assert_eq!(EntityType::from_section("Rules"), Some(EntityType::Rule));
209 assert_eq!(EntityType::from_section("Metadata"), Some(EntityType::Metadata));
210 assert_eq!(EntityType::from_section("Transform"), Some(EntityType::Transform));
211 assert_eq!(EntityType::from_section("AWSTemplateFormatVersion"), Some(EntityType::FormatVersion));
212 assert_eq!(EntityType::from_section("Description"), Some(EntityType::Description));
213 assert_eq!(EntityType::from_section("Resource"), None, "singular form is not a section name");
214 }
215
216 #[test]
217 fn entity_identity_splits_section_absolute_paths() {
218 assert_eq!(entity_identity("Parameters/MyParam/Type"), Some((EntityType::Parameter, "MyParam")));
219 assert_eq!(entity_identity("Parameters/MyParam"), Some((EntityType::Parameter, "MyParam")));
220 assert_eq!(entity_identity("Outputs/MyOutput/Value"), Some((EntityType::Output, "MyOutput")));
221 assert_eq!(entity_identity("Mappings/MyMap/Key1/Key2"), Some((EntityType::Mapping, "MyMap")));
222 assert_eq!(entity_identity("Conditions/IsProd"), Some((EntityType::Condition, "IsProd")));
223 assert_eq!(entity_identity("Rules/MyRule/Assertions/0"), Some((EntityType::Rule, "MyRule")));
224 assert_eq!(entity_identity("Resources/MyBucket/Properties/Name"), Some((EntityType::Resource, "MyBucket")));
225 }
226
227 #[test]
228 fn entity_identity_handles_mixed_dot_segments_after_the_entity() {
229 assert_eq!(entity_identity("Outputs/X/Value.Fn::If.1"), Some((EntityType::Output, "X")));
232 }
233
234 #[test]
235 fn entity_identity_rejects_non_entity_paths() {
236 assert_eq!(entity_identity("Properties.BucketName"), None, "resource-relative dotted path");
237 assert_eq!(entity_identity("Parameters"), None, "bare section has no entity");
238 assert_eq!(entity_identity("Description"), None, "bare section has no entity");
239 assert_eq!(entity_identity("AWSTemplateFormatVersion"), None, "bare section has no entity");
240 assert_eq!(entity_identity("Globals/Function"), None, "SAM Globals is not a documented section");
241 assert_eq!(entity_identity(""), None);
242 assert_eq!(entity_identity("Parameters//Type"), None, "empty entity segment");
243 }
244}