use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))]
#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))]
pub enum TopLevelSection {
Resources,
Parameters,
Outputs,
Mappings,
Metadata,
Rules,
Conditions,
Transform,
FormatVersion,
Description,
}
impl TopLevelSection {
const ALL: [TopLevelSection; 10] = [
TopLevelSection::Resources,
TopLevelSection::Parameters,
TopLevelSection::Outputs,
TopLevelSection::Mappings,
TopLevelSection::Metadata,
TopLevelSection::Rules,
TopLevelSection::Conditions,
TopLevelSection::Transform,
TopLevelSection::FormatVersion,
TopLevelSection::Description,
];
pub const fn name(self) -> &'static str {
match self {
TopLevelSection::Resources => "Resources",
TopLevelSection::Parameters => "Parameters",
TopLevelSection::Outputs => "Outputs",
TopLevelSection::Mappings => "Mappings",
TopLevelSection::Metadata => "Metadata",
TopLevelSection::Rules => "Rules",
TopLevelSection::Conditions => "Conditions",
TopLevelSection::Transform => "Transform",
TopLevelSection::FormatVersion => "AWSTemplateFormatVersion",
TopLevelSection::Description => "Description",
}
}
pub fn from_name(name: &str) -> Option<TopLevelSection> {
Self::ALL.into_iter().find(|section| section.name() == name)
}
pub const fn entity_type(self) -> EntityType {
match self {
TopLevelSection::Resources => EntityType::Resource,
TopLevelSection::Parameters => EntityType::Parameter,
TopLevelSection::Outputs => EntityType::Output,
TopLevelSection::Mappings => EntityType::Mapping,
TopLevelSection::Metadata => EntityType::Metadata,
TopLevelSection::Rules => EntityType::Rule,
TopLevelSection::Conditions => EntityType::Condition,
TopLevelSection::Transform => EntityType::Transform,
TopLevelSection::FormatVersion => EntityType::FormatVersion,
TopLevelSection::Description => EntityType::Description,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[cfg_attr(feature = "wasm-bindings", derive(tsify::Tsify))]
#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))]
pub enum EntityType {
Resource,
Parameter,
Output,
Mapping,
Metadata,
Rule,
Condition,
Transform,
FormatVersion,
Description,
}
impl EntityType {
const ALL: [EntityType; 10] = [
EntityType::Resource,
EntityType::Parameter,
EntityType::Output,
EntityType::Mapping,
EntityType::Metadata,
EntityType::Rule,
EntityType::Condition,
EntityType::Transform,
EntityType::FormatVersion,
EntityType::Description,
];
pub const fn name(self) -> &'static str {
match self {
EntityType::Resource => "Resource",
EntityType::Parameter => "Parameter",
EntityType::Output => "Output",
EntityType::Mapping => "Mapping",
EntityType::Metadata => "Metadata",
EntityType::Rule => "Rule",
EntityType::Condition => "Condition",
EntityType::Transform => "Transform",
EntityType::FormatVersion => "FormatVersion",
EntityType::Description => "Description",
}
}
pub const fn section(self) -> TopLevelSection {
match self {
EntityType::Resource => TopLevelSection::Resources,
EntityType::Parameter => TopLevelSection::Parameters,
EntityType::Output => TopLevelSection::Outputs,
EntityType::Mapping => TopLevelSection::Mappings,
EntityType::Metadata => TopLevelSection::Metadata,
EntityType::Rule => TopLevelSection::Rules,
EntityType::Condition => TopLevelSection::Conditions,
EntityType::Transform => TopLevelSection::Transform,
EntityType::FormatVersion => TopLevelSection::FormatVersion,
EntityType::Description => TopLevelSection::Description,
}
}
pub fn from_section(section: &str) -> Option<EntityType> {
TopLevelSection::from_name(section).map(TopLevelSection::entity_type)
}
}
impl std::str::FromStr for EntityType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::ALL.into_iter().find(|entity_type| entity_type.name().eq_ignore_ascii_case(s)).ok_or_else(|| {
format!("Invalid entity type '{s}'; expected one of: {}", Self::ALL.map(EntityType::name).join(", "))
})
}
}
pub fn entity_identity(path: &str) -> Option<(EntityType, &str)> {
let mut segments = path.split('/');
let entity_type = EntityType::from_section(segments.next()?)?;
let logical_id = segments.next().filter(|id| !id.is_empty())?;
Some((entity_type, logical_id))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn section_names_round_trip_through_from_name() {
for section in TopLevelSection::ALL {
assert_eq!(TopLevelSection::from_name(section.name()), Some(section), "{section:?}");
}
assert_eq!(TopLevelSection::from_name("Globals"), None, "SAM Globals is not a documented section");
assert_eq!(TopLevelSection::from_name("Constants"), None);
assert_eq!(TopLevelSection::from_name(""), None);
}
#[test]
fn format_version_uses_the_full_template_key() {
assert_eq!(TopLevelSection::FormatVersion.name(), "AWSTemplateFormatVersion");
assert_eq!(TopLevelSection::from_name("AWSTemplateFormatVersion"), Some(TopLevelSection::FormatVersion));
}
#[test]
fn entity_type_and_section_are_inverse_mappings() {
for section in TopLevelSection::ALL {
assert_eq!(section.entity_type().section(), section, "{section:?}");
}
}
#[test]
fn from_section_maps_names_to_singular_entity_types() {
assert_eq!(EntityType::from_section("Resources"), Some(EntityType::Resource));
assert_eq!(EntityType::from_section("Parameters"), Some(EntityType::Parameter));
assert_eq!(EntityType::from_section("Outputs"), Some(EntityType::Output));
assert_eq!(EntityType::from_section("Mappings"), Some(EntityType::Mapping));
assert_eq!(EntityType::from_section("Conditions"), Some(EntityType::Condition));
assert_eq!(EntityType::from_section("Rules"), Some(EntityType::Rule));
assert_eq!(EntityType::from_section("Metadata"), Some(EntityType::Metadata));
assert_eq!(EntityType::from_section("Transform"), Some(EntityType::Transform));
assert_eq!(EntityType::from_section("AWSTemplateFormatVersion"), Some(EntityType::FormatVersion));
assert_eq!(EntityType::from_section("Description"), Some(EntityType::Description));
assert_eq!(EntityType::from_section("Resource"), None, "singular form is not a section name");
}
#[test]
fn entity_identity_splits_section_absolute_paths() {
assert_eq!(entity_identity("Parameters/MyParam/Type"), Some((EntityType::Parameter, "MyParam")));
assert_eq!(entity_identity("Parameters/MyParam"), Some((EntityType::Parameter, "MyParam")));
assert_eq!(entity_identity("Outputs/MyOutput/Value"), Some((EntityType::Output, "MyOutput")));
assert_eq!(entity_identity("Mappings/MyMap/Key1/Key2"), Some((EntityType::Mapping, "MyMap")));
assert_eq!(entity_identity("Conditions/IsProd"), Some((EntityType::Condition, "IsProd")));
assert_eq!(entity_identity("Rules/MyRule/Assertions/0"), Some((EntityType::Rule, "MyRule")));
assert_eq!(entity_identity("Resources/MyBucket/Properties/Name"), Some((EntityType::Resource, "MyBucket")));
}
#[test]
fn entity_identity_handles_mixed_dot_segments_after_the_entity() {
assert_eq!(entity_identity("Outputs/X/Value.Fn::If.1"), Some((EntityType::Output, "X")));
}
#[test]
fn entity_identity_rejects_non_entity_paths() {
assert_eq!(entity_identity("Properties.BucketName"), None, "resource-relative dotted path");
assert_eq!(entity_identity("Parameters"), None, "bare section has no entity");
assert_eq!(entity_identity("Description"), None, "bare section has no entity");
assert_eq!(entity_identity("AWSTemplateFormatVersion"), None, "bare section has no entity");
assert_eq!(entity_identity("Globals/Function"), None, "SAM Globals is not a documented section");
assert_eq!(entity_identity(""), None);
assert_eq!(entity_identity("Parameters//Type"), None, "empty entity segment");
}
}