cloudformation-validate-data-source 1.9.0-beta

AWS CloudFormation Validate
Documentation
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

pub const SOURCE_VERSIONS_FILE: &str = "source_versions.json";
pub const CFN_LINT_SOURCE: &str = "https://github.com/aws-cloudformation/cfn-lint";
pub const RESOURCE_SCHEMA_SOURCE: &str = "https://github.com/aws-cloudformation/resource-provider-enhanced-schemas";

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SourceVersions {
    pub cfn_lint_version: String,
    pub resource_schema_version: String,
}

impl SourceVersions {
    pub fn new(cfn_lint_version: String, resource_schema_version: String) -> Result<Self, String> {
        let versions = Self { cfn_lint_version, resource_schema_version };
        versions.validate()?;
        Ok(versions)
    }

    pub fn read(path: &Path) -> Result<Self, String> {
        let contents =
            fs::read_to_string(path).map_err(|error| format!("failed to read {}: {error}", path.display()))?;
        Self::from_json(&contents).map_err(|error| format!("invalid {}: {error}", path.display()))
    }

    pub fn from_json(contents: &str) -> Result<Self, String> {
        let versions: Self = serde_json::from_str(contents).map_err(|error| error.to_string())?;
        versions.validate()?;
        Ok(versions)
    }

    fn validate(&self) -> Result<(), String> {
        validate_source_version("cfn_lint_version", &self.cfn_lint_version, CFN_LINT_SOURCE)?;
        validate_source_version("resource_schema_version", &self.resource_schema_version, RESOURCE_SCHEMA_SOURCE)
    }
}

fn validate_source_version(field: &str, value: &str, source: &str) -> Result<(), String> {
    let prefix = format!("{source}@");
    let Some(version) = value.strip_prefix(&prefix) else {
        return Err(format!("{field} must start with {prefix}"));
    };
    if version.trim().is_empty() {
        return Err(format!("{field} must include a nonblank version"));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_complete_manifest() {
        let versions = SourceVersions::from_json(
            r#"{"cfn_lint_version":"https://github.com/aws-cloudformation/cfn-lint@1.54.0","resource_schema_version":"https://github.com/aws-cloudformation/resource-provider-enhanced-schemas@2026-08-07T18:20:13Z"}"#,
        )
        .expect("manifest should parse");

        assert_eq!(versions.cfn_lint_version, "https://github.com/aws-cloudformation/cfn-lint@1.54.0");
        assert_eq!(
            versions.resource_schema_version,
            "https://github.com/aws-cloudformation/resource-provider-enhanced-schemas@2026-08-07T18:20:13Z"
        );
    }

    #[test]
    fn serializes_complete_manifest() {
        let versions = SourceVersions::new(
            format!("{CFN_LINT_SOURCE}@1.54.0"),
            format!("{RESOURCE_SCHEMA_SOURCE}@2026-08-07T18:20:13Z"),
        )
        .expect("source versions should be valid");

        let json = serde_json::to_value(versions).expect("manifest should serialize");
        assert_eq!(json.as_object().expect("manifest should be an object").len(), 2);
        assert_eq!(json["cfn_lint_version"], format!("{CFN_LINT_SOURCE}@1.54.0"));
        assert_eq!(json["resource_schema_version"], format!("{RESOURCE_SCHEMA_SOURCE}@2026-08-07T18:20:13Z"));
    }

    #[test]
    fn missing_field_is_rejected() {
        let manifest = r#"{"resource_schema_version":"https://github.com/aws-cloudformation/resource-provider-enhanced-schemas@2026-08-07T18:20:13Z"}"#;
        assert!(SourceVersions::from_json(manifest).is_err());
    }

    #[test]
    fn blank_version_suffix_is_rejected() {
        let error = SourceVersions::new(
            format!("{CFN_LINT_SOURCE}@  "),
            format!("{RESOURCE_SCHEMA_SOURCE}@2026-08-07T18:20:13Z"),
        )
        .expect_err("blank version must fail");
        assert!(error.contains("cfn_lint_version must include a nonblank version"));
    }

    #[test]
    fn unqualified_version_is_rejected() {
        let error = SourceVersions::new("1.54.0".to_string(), format!("{RESOURCE_SCHEMA_SOURCE}@2026-08-07T18:20:13Z"))
            .expect_err("unqualified version must fail");
        assert!(error.contains(CFN_LINT_SOURCE));
    }

    #[test]
    fn unknown_field_is_rejected() {
        let manifest = r#"{
            "cfn_lint_version":"https://github.com/aws-cloudformation/cfn-lint@1.54.0",
            "resource_schema_version":"https://github.com/aws-cloudformation/resource-provider-enhanced-schemas@2026-08-07T18:20:13Z",
            "unexpected":"value"
        }"#;
        assert!(SourceVersions::from_json(manifest).is_err());
    }
}