quadlet-lens 0.1.13

Loss-aware parsing, validation, and rendering of version-aware Quadlet documents
Documentation
//! Named Quadlet document sets and exact native dependency resolution.

use quadlet_lens::model::{
    DocumentSetError, NamedQuadletDocument, QuadletDocument, QuadletDocumentSet, QuadletUnitType, ReferenceResolution,
    UnitFileName, UnitReferenceKind,
};
use quadlet_lens::source::SourceId;

const APP: &str = include_str!("../fixtures/typed-model/document-set-resolution/app.container");
const PROXY: &str = include_str!("../fixtures/typed-model/document-set-resolution/proxy.container");
const POD: &str = include_str!("../fixtures/typed-model/document-set-resolution/application.pod");
const NETWORK: &str = include_str!("../fixtures/typed-model/document-set-resolution/frontend.network");
const VOLUME: &str = include_str!("../fixtures/typed-model/document-set-resolution/cache.volume");

#[test]
fn document_set_resolves_container_pod_network_and_volume_dependencies() -> Result<(), String> {
    let set = QuadletDocumentSet::new([
        named("app.container", QuadletUnitType::Container, 51, APP)?,
        named("proxy.container", QuadletUnitType::Container, 52, PROXY)?,
        named("application.pod", QuadletUnitType::Pod, 53, POD)?,
        named("frontend.network", QuadletUnitType::Network, 54, NETWORK)?,
        named("cache.volume", QuadletUnitType::Volume, 55, VOLUME)?,
    ])
    .map_err(|error| error.to_string())?;

    assert!(set.is_valid(), "{:#?}", set.diagnostics());
    assert!(set.graph().is_complete());
    assert_eq!(set.graph().references().len(), 5);
    assert_eq!(set.graph().edges().len(), 5);
    assert_eq!(
        set.document("application.pod")
            .map(|document| document.document().unit_type()),
        Some(QuadletUnitType::Pod)
    );

    let edges: Vec<_> = set
        .graph()
        .edges()
        .iter()
        .map(|edge| {
            (
                set.documents()[edge.source_document()].name().as_str(),
                set.documents()[edge.target_document()].name().as_str(),
                edge.kind(),
            )
        })
        .collect();
    assert_eq!(
        edges,
        [
            ("app.container", "cache.volume", UnitReferenceKind::Volume),
            ("app.container", "application.pod", UnitReferenceKind::Pod),
            ("proxy.container", "frontend.network", UnitReferenceKind::Network),
            ("application.pod", "frontend.network", UnitReferenceKind::Network),
            ("application.pod", "cache.volume", UnitReferenceKind::Volume),
        ]
    );
    Ok(())
}

#[test]
fn document_set_resolves_a_container_image_to_an_exact_build_unit() -> Result<(), String> {
    let set = QuadletDocumentSet::new([
        named(
            "application.container",
            QuadletUnitType::Container,
            181,
            "[Container]\nImage=application.build\n",
        )?,
        named(
            "application.build",
            QuadletUnitType::Build,
            182,
            "[Build]\nImageTag=localhost/application:latest\nTarget=build-stage\nSetWorkingDirectory=unit\n",
        )?,
    ])
    .map_err(|error| error.to_string())?;
    assert!(set.is_valid(), "{:#?}", set.diagnostics());
    assert_eq!(set.graph().references().len(), 1);
    assert_eq!(set.graph().edges().len(), 1);
    assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Build);
    assert_eq!(
        set.graph().references()[0].resolution(),
        ReferenceResolution::Resolved { document_index: 1 }
    );
    assert_eq!(set.graph().edges()[0].target_document(), 1);
    assert_eq!(
        set.document("application.build")
            .map(|document| document.document().unit_type()),
        Some(QuadletUnitType::Build)
    );
    Ok(())
}

#[test]
fn document_set_resolves_a_volume_image_to_an_exact_build_unit() -> Result<(), String> {
    let set = QuadletDocumentSet::new([
        named(
            "cache.volume",
            QuadletUnitType::Volume,
            183,
            "[Volume]\nImage=application.build\n",
        )?,
        named(
            "application.build",
            QuadletUnitType::Build,
            184,
            "[Build]\nImageTag=localhost/application:latest\nTarget=build-stage\nSetWorkingDirectory=unit\n",
        )?,
    ])
    .map_err(|error| error.to_string())?;
    assert!(set.is_valid(), "{:#?}", set.diagnostics());
    assert_eq!(set.graph().references().len(), 1);
    assert_eq!(set.graph().edges().len(), 1);
    assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Build);
    assert_eq!(set.graph().references()[0].target_name(), "application.build");
    Ok(())
}

#[test]
fn document_set_resolves_exact_container_and_volume_image_references_to_an_image_unit() -> Result<(), String> {
    let set = QuadletDocumentSet::new([
        named(
            "application.container",
            QuadletUnitType::Container,
            185,
            "[Container]\nImage=application.image\n",
        )?,
        named(
            "cache.volume",
            QuadletUnitType::Volume,
            186,
            "[Volume]\nImage=application.image\n",
        )?,
        named(
            "application.image",
            QuadletUnitType::Image,
            187,
            "[Image]\nImage=example.invalid/application:latest\n",
        )?,
    ])
    .map_err(|error| error.to_string())?;
    assert!(set.is_valid(), "{:#?}", set.diagnostics());
    assert_eq!(set.graph().references().len(), 2);
    assert_eq!(set.graph().edges().len(), 2);
    assert!(set.graph().references().iter().all(|reference| {
        reference.kind() == UnitReferenceKind::Image
            && reference.target_name() == "application.image"
            && reference.resolution() == ReferenceResolution::Resolved { document_index: 2 }
    }));
    assert_eq!(
        set.document("application.image")
            .map(|document| document.document().unit_type()),
        Some(QuadletUnitType::Image)
    );
    Ok(())
}

#[test]
fn image_tag_does_not_create_or_mutate_document_set_edges() -> Result<(), String> {
    let set = QuadletDocumentSet::new([
        named(
            "application.container",
            QuadletUnitType::Container,
            449,
            "[Container]\nImage=application.image\n",
        )?,
        named(
            "application.image",
            QuadletUnitType::Image,
            450,
            "[Image]\nImage=example.invalid/application:latest\nImageTag=other.image\nServiceName=other.service\nAllTags=true\nArch=arm64\nAuthFile=/placeholder/quadlet-lens-auth.json\nCertDir=/placeholder/quadlet-lens-certs\nContainersConfModule=one.conf\nContainersConfModule=two.conf\n",
        )?,
    ])
    .map_err(|error| error.to_string())?;
    assert!(set.is_valid(), "{:#?}", set.diagnostics());
    assert_eq!(set.graph().references().len(), 1);
    assert_eq!(set.graph().edges().len(), 1);
    assert_eq!(set.graph().references()[0].target_name(), "application.image");
    assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Image);
    Ok(())
}

#[test]
fn document_set_resolves_build_volume_source_prefix_without_mutating_identities() -> Result<(), String> {
    let set = QuadletDocumentSet::new([
        named(
            "application.build",
            QuadletUnitType::Build,
            220,
            "[Build]\nImageTag=localhost/application\nVolume=cache.volume:/var/cache:Z\n",
        )?,
        named("cache.volume", QuadletUnitType::Volume, 221, "[Volume]\n")?,
    ])
    .map_err(|error| error.to_string())?;
    assert!(set.is_valid(), "{:#?}", set.diagnostics());
    assert_eq!(set.graph().references().len(), 1);
    assert_eq!(set.graph().references()[0].target_name(), "cache.volume");
    assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Volume);
    assert_eq!(set.graph().edges().len(), 1);
    assert_eq!(
        set.documents()[set.graph().edges()[0].target_document()]
            .name()
            .as_str(),
        "cache.volume"
    );
    Ok(())
}

#[test]
fn document_set_resolves_an_exact_build_network_reference() -> Result<(), String> {
    let set = QuadletDocumentSet::new([
        named(
            "application.build",
            QuadletUnitType::Build,
            183,
            "[Build]\nNetwork=frontend.network\nNetwork=frontend.network:ip=192.0.2.10\nNetwork=frontend.container\n",
        )?,
        named("frontend.network", QuadletUnitType::Network, 184, "[Network]\n")?,
    ])
    .map_err(|error| error.to_string())?;
    assert!(set.is_valid(), "{:#?}", set.diagnostics());
    assert_eq!(set.graph().references().len(), 1);
    assert_eq!(set.graph().edges().len(), 1);
    assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Network);
    assert_eq!(set.graph().references()[0].target_name(), "frontend.network");
    assert_eq!(
        set.graph().references()[0].resolution(),
        ReferenceResolution::Resolved { document_index: 1 }
    );
    assert_eq!(set.graph().edges()[0].target_document(), 1);
    Ok(())
}

#[test]
fn document_set_reports_missing_ambiguous_and_duplicate_identities() -> Result<(), String> {
    let missing = QuadletDocumentSet::new([named(
        "missing.container",
        QuadletUnitType::Container,
        56,
        "[Container]\nImage=example.invalid/app\nNetwork=absent.network\n",
    )?])
    .map_err(|error| error.to_string())?;
    assert!(!missing.is_valid());
    assert_eq!(diagnostic_codes(&missing), ["QLG0001"]);
    assert_eq!(
        missing.graph().references()[0].resolution(),
        ReferenceResolution::Missing
    );

    let ambiguous = QuadletDocumentSet::new([
        named(
            "ambiguous.container",
            QuadletUnitType::Container,
            57,
            "[Container]\nImage=example.invalid/app\nNetwork=duplicate.network\n",
        )?,
        named("duplicate.network", QuadletUnitType::Network, 58, "[Network]\n")?,
        named("duplicate.network", QuadletUnitType::Network, 59, "[Network]\n")?,
    ])
    .map_err(|error| error.to_string())?;
    assert!(!ambiguous.is_valid());
    assert_eq!(diagnostic_codes(&ambiguous), ["QLG0003", "QLG0002"]);
    assert_eq!(
        ambiguous.graph().references()[0].resolution(),
        ReferenceResolution::Ambiguous { candidates: 2 }
    );
    assert!(ambiguous.graph().edges().is_empty());
    assert!(ambiguous.document("duplicate.network").is_none());

    let duplicate_source = QuadletDocumentSet::new([
        named("one.network", QuadletUnitType::Network, 60, "[Network]\n")?,
        named("two.network", QuadletUnitType::Network, 60, "[Network]\n")?,
    ]);
    assert!(matches!(duplicate_source, Err(DocumentSetError::DuplicateSourceId(id)) if id == SourceId::new(60)));
    Ok(())
}

#[test]
fn unit_file_names_are_basenames_with_matching_supported_suffixes() -> Result<(), String> {
    for invalid in [
        "",
        "application",
        ".container",
        "application.",
        "nested/app.container",
        "nested\\app.container",
    ] {
        assert!(
            matches!(
                UnitFileName::new(invalid),
                Err(DocumentSetError::InvalidUnitFileName(_))
            ),
            "unexpectedly accepted {invalid:?}"
        );
    }
    assert!(matches!(
        UnitFileName::new("application.service"),
        Err(DocumentSetError::UnsupportedUnitFileExtension(_))
    ));

    let image = UnitFileName::new("application.image").map_err(|error| error.to_string())?;
    assert_eq!(image.as_str(), "application.image");
    assert_eq!(image.to_string(), "application.image");
    assert_eq!(image.unit_type(), QuadletUnitType::Image);

    assert_eq!(
        UnitFileName::new("application.build")
            .map_err(|error| error.to_string())?
            .unit_type(),
        QuadletUnitType::Build
    );

    let network = named_document(QuadletUnitType::Network, 61, "[Network]\n")?;
    let mismatch = NamedQuadletDocument::new("wrong.volume", network);
    assert!(matches!(mismatch, Err(DocumentSetError::UnitTypeMismatch { .. })));
    assert_eq!(
        mismatch.err().map(|error| error.to_string()),
        Some("Quadlet filename `wrong.volume` implies Volume, but the document is Network".to_owned())
    );
    Ok(())
}

#[test]
fn document_set_accessors_preserve_names_sources_and_reference_spans() -> Result<(), String> {
    let source = "[Container]\nImage=example.invalid/app\nNetwork=frontend.network\n";
    let set = QuadletDocumentSet::new([
        named("app.container", QuadletUnitType::Container, 62, source)?,
        named("frontend.network", QuadletUnitType::Network, 63, "[Network]\n")?,
    ])
    .map_err(|error| error.to_string())?;

    let reference = &set.graph().references()[0];
    let edge = set.graph().edges()[0];
    let expected_start = source
        .find("frontend.network")
        .ok_or_else(|| "test source must contain its reference".to_owned())?;
    assert_eq!(reference.source_document(), 0);
    assert_eq!(reference.span().source_id(), SourceId::new(62));
    assert_eq!(
        (reference.span().start(), reference.span().end()),
        (expected_start, source.len() - 1)
    );
    assert_eq!(edge.source_document(), 0);
    assert_eq!(edge.target_document(), 1);
    assert_eq!(edge.kind(), UnitReferenceKind::Network);
    assert_eq!(edge.span(), reference.span());

    let decomposed = named("standalone.network", QuadletUnitType::Network, 64, "[Network]\n")?;
    let (name, document) = decomposed.into_parts();
    assert_eq!(name.as_str(), "standalone.network");
    assert_eq!(document.source_id(), SourceId::new(64));
    Ok(())
}

fn named(name: &str, unit_type: QuadletUnitType, source_id: u32, source: &str) -> Result<NamedQuadletDocument, String> {
    let document = named_document(unit_type, source_id, source)?;
    NamedQuadletDocument::new(name, document).map_err(|error| error.to_string())
}

fn named_document(unit_type: QuadletUnitType, source_id: u32, source: &str) -> Result<QuadletDocument, String> {
    let result =
        QuadletDocument::parse(unit_type, SourceId::new(source_id), source).map_err(|error| error.to_string())?;
    if !result.is_valid() {
        return Err(format!(
            "test source has diagnostics: {:#?}",
            result.model_diagnostics()
        ));
    }
    let (_, document, _) = result.into_parts();
    Ok(document)
}

fn diagnostic_codes(set: &QuadletDocumentSet) -> Vec<&str> {
    set.diagnostics()
        .iter()
        .map(|diagnostic| diagnostic.code().as_str())
        .collect()
}