fluidattacks-blends 0.6.0

Blends imperative shell: parsing, AST-graph construction, serialization
Documentation
//! Helm-template custom parser.
//!
//! The shell parses and assembles `Content`; the pure text fixup lives in
//! `blends_domain::content::helm`. Content that already parses is left
//! untouched — flow yaml is handled natively by the ast walker and the
//! syntax readers, not rewritten here.

use blends_domain::content::helm::{fix, HelmInput};

use crate::content::custom_parsers::CustomParser;
use crate::content::Content;
use crate::language::Language;
use crate::parse::parse;

pub const PARSER: CustomParser = CustomParser {
    languages: &[Language::Json, Language::Yaml],
    validator,
    transformer,
};

fn validator(content: &Content) -> bool {
    content
        .path
        .extension()
        .and_then(|extension| extension.to_str())
        .map(str::to_ascii_lowercase)
        .is_some_and(|extension| matches!(extension.as_str(), "json" | "yml" | "yaml"))
}

fn under_templates(content: &Content) -> bool {
    content
        .path
        .components()
        .any(|component| component.as_os_str().to_str() == Some("templates"))
}

fn rebuild(content: &Content, text: String) -> Content {
    Content {
        bytes: text.clone().into_bytes(),
        text,
        language: content.language,
        path: content.path.clone(),
    }
}

fn transformer(content: &Content) -> Option<Content> {
    if parse(content).is_ok() {
        return None;
    }
    try_fix_helm(content)
}

fn try_fix_helm(content: &Content) -> Option<Content> {
    let fixed = rebuild(
        content,
        fix(HelmInput {
            text: &content.text,
            under_templates: under_templates(content),
        })?,
    );

    if parse(&fixed).is_ok() {
        tracing::debug!(path = %content.path.display(), "applied helm template fixup");
        Some(fixed)
    } else {
        tracing::warn!(
            path = %content.path.display(),
            "skipping helm template: still malformed after fixup"
        );
        None
    }
}

#[cfg(test)]
mod tests {
    use super::transformer;
    use crate::content::Content;
    use crate::language::{Language, LanguageExt};
    use crate::parse::parse;
    use std::fs;
    use std::path::Path;

    fn raw_fixture(name: &str) -> Content {
        let path = Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../test/data/helm_parser/templates")
            .join(name);
        let bytes = fs::read(&path).unwrap();
        let text = String::from_utf8_lossy(&bytes).into_owned();
        let language = Language::from_path(&path).unwrap();
        Content {
            bytes,
            text,
            language,
            path,
        }
    }

    fn raw_yaml(source: &str) -> Content {
        Content {
            bytes: source.as_bytes().to_vec(),
            text: source.to_owned(),
            language: Language::Yaml,
            path: Path::new("snippet.yaml").to_path_buf(),
        }
    }

    #[test]
    fn root_flow_yaml_is_left_unchanged() {
        assert!(transformer(&raw_fixture("json_yaml_template.yaml")).is_none());
    }

    #[test]
    fn block_yaml_is_left_unchanged() {
        assert!(transformer(&raw_yaml("version: 2\nmodels:\n  - name: a\n")).is_none());
    }

    #[test]
    fn unparseable_helm_template_is_fixed() {
        let fixed = transformer(&raw_fixture("deployment_with_nested_conditionals.yaml"))
            .expect("templated manifest is fixable");

        assert!(parse(&fixed).is_ok());
    }
}