quadlet-lens 0.1.2

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_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> {
    assert!(matches!(
        UnitFileName::new("nested/app.container"),
        Err(DocumentSetError::InvalidUnitFileName(_))
    ));
    assert!(matches!(
        UnitFileName::new("application.image"),
        Err(DocumentSetError::UnsupportedUnitFileExtension(_))
    ));

    let network = named_document(QuadletUnitType::Network, 61, "[Network]\n")?;
    assert!(matches!(
        NamedQuadletDocument::new("wrong.volume", network),
        Err(DocumentSetError::UnitTypeMismatch { .. })
    ));
    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()
}