es-fluent-shared 0.19.0

Shared runtime-safe types and helpers for the es-fluent ecosystem
Documentation
use super::*;
use std::collections::HashSet;

use crate::fluent::FluentDomain;
use crate::namespace::NamespacePathError;
use crate::namespace::ResolvedNamespace;

#[test]
fn fallback_catalog_encodes_only_resolvable_messages() {
    let domain = FluentDomain::try_new("demo").expect("domain");
    let mut catalog = FallbackCatalog::default();
    catalog
        .insert_source(
            &domain,
            "hello = Hello\nattr-only =\n    .label = Label\n-term = Term\n".to_string(),
        )
        .expect("catalog");
    let encoded = catalog.encode();

    assert!(fallback_catalog_contains(&encoded, "demo", "hello"));
    assert!(!fallback_catalog_contains(&encoded, "demo", "attr-only"));
    assert!(!fallback_catalog_contains(&encoded, "demo", "term"));
    assert!(!fallback_catalog_contains(&encoded, "other", "hello"));
    assert!(fallback_catalog_contains(b"demo\thello", "demo", "hello"));
}

#[test]
fn fallback_catalog_rejects_message_term_collisions() {
    let domain = FluentDomain::try_new("demo").expect("domain");
    let mut catalog = FallbackCatalog::default();
    let error = catalog
        .insert_source(&domain, "hello = Hello\n-hello = Term\n".to_string())
        .expect_err("collision");

    assert!(error.to_string().contains("duplicate Fluent entry 'hello'"));
}

#[test]
fn resource_key_explicit_constructors_preserve_key_and_domain() {
    let dynamic = ResourceKey::try_new("demo/ui").expect("dynamic key");
    let static_key = ResourceKey::from_static_path("demo/errors");

    assert_eq!(dynamic.as_str(), "demo/ui");
    assert_eq!(dynamic.domain(), "demo");
    assert_eq!(dynamic.domain_name().as_str(), "demo");
    assert_eq!(dynamic.as_ref(), "demo/ui");
    assert_eq!(static_key.to_string(), "demo/errors");
}

#[test]
fn resource_key_rejects_noncanonical_key_shapes() {
    assert_eq!(
        ResourceKey::try_new("../demo").expect_err("parent segment"),
        ResourceKeyError(NamespacePathError::CurrentOrParentSegment)
    );
    assert_eq!(
        ResourceKey::try_new("demo.ftl").expect_err("file suffix"),
        ResourceKeyError(NamespacePathError::FileExtension)
    );
    assert_eq!(
        ResourceKey::try_new("demo//ui").expect_err("empty segment"),
        ResourceKeyError(NamespacePathError::EmptySegment)
    );
}

#[test]
fn locale_relative_ftl_path_validates_canonical_resource_paths() {
    let path = LocaleRelativeFtlPath::try_new("demo/ui.ftl").expect("path");
    assert_eq!(path.as_str(), "demo/ui.ftl");
    assert_eq!(path.to_string(), "demo/ui.ftl");
    assert_eq!(path, "demo/ui.ftl");
    assert_eq!(
        LocaleRelativeFtlPath::from_static_path("demo/static.ftl").as_str(),
        "demo/static.ftl"
    );

    assert_eq!(
        LocaleRelativeFtlPath::try_new("").expect_err("empty path"),
        LocaleRelativeFtlPathError::Empty
    );
    assert_eq!(
        LocaleRelativeFtlPath::try_new("/demo.ftl").expect_err("absolute path"),
        LocaleRelativeFtlPathError::Absolute
    );
    assert_eq!(
        LocaleRelativeFtlPath::try_new("demo\\ui.ftl").expect_err("backslash path"),
        LocaleRelativeFtlPathError::Backslash
    );
    assert_eq!(
        LocaleRelativeFtlPath::try_new("demo/ui").expect_err("missing suffix"),
        LocaleRelativeFtlPathError::MissingFtlSuffix
    );
    assert!(matches!(
        LocaleRelativeFtlPath::try_new("demo/../ui.ftl"),
        Err(LocaleRelativeFtlPathError::InvalidStem(
            NamespacePathError::CurrentOrParentSegment
        ))
    ));
}

#[test]
fn module_resource_spec_try_new_reports_invalid_parts() {
    assert!(matches!(
        ModuleResourceSpec::try_new("../demo", "demo.ftl", true),
        Err(ResourcePlanError::InvalidResourceKey {
            key,
            details: ResourceKeyError(NamespacePathError::CurrentOrParentSegment),
        }) if key == "../demo"
    ));
    assert!(matches!(
        ModuleResourceSpec::try_new("demo", "demo", true),
        Err(ResourcePlanError::InvalidResourcePath {
            path,
            details: LocaleRelativeFtlPathError::MissingFtlSuffix,
        }) if path == "demo"
    ));
}

#[test]
fn resource_plan_for_handles_base_and_namespaced_resources() {
    let base_plan = resource_plan_for("demo", &[]);
    assert_eq!(base_plan.len(), 1);
    assert_eq!(base_plan[0].key.as_str(), "demo");
    assert_eq!(base_plan[0].locale_relative_path, "demo.ftl");
    let en_us = "en-US".parse().expect("language id");
    assert_eq!(base_plan[0].locale_path(&en_us), "en-US/demo.ftl");
    assert!(base_plan[0].required);

    let namespaced_plan = resource_plan_for("demo", &["ui", "ui", "errors"]);
    let keys: Vec<_> = namespaced_plan
        .iter()
        .map(|spec| spec.key.as_str())
        .collect();
    assert_eq!(keys, vec!["demo", "demo/ui", "demo/errors"]);
    assert!(!namespaced_plan[0].required);
    assert_eq!(namespaced_plan[0].locale_relative_path, "demo.ftl");
    assert!(namespaced_plan[1].required);
    assert!(
        namespaced_plan
            .iter()
            .all(|spec| spec.locale_relative_path.ends_with(".ftl"))
    );
}

#[test]
fn resource_plan_api_exposes_specs_and_sparse_plans() {
    let plan = ResourcePlan::for_domain("demo", &["ui"]).expect("resource plan");
    assert_eq!(plan.specs()[0], ModuleResourceSpec::base("demo", false));
    assert_eq!(
        plan.specs()[1].key,
        ResourceKey::from_static_path("demo/ui")
    );

    let namespace = ResolvedNamespace::new("errors/forms").expect("namespace");
    let sparse = ResourcePlan::sparse_for_domain("demo", true, &[namespace], false);
    let specs = sparse.into_specs();

    assert_eq!(
        specs,
        vec![
            ModuleResourceSpec::base("demo", false),
            ModuleResourceSpec::new(
                ResourceKey::from_static_path("demo/errors/forms"),
                LocaleRelativeFtlPath::from_static_path("demo/errors/forms.ftl"),
                true
            ),
        ]
    );
}

#[test]
fn sparse_from_assets_discovers_canonical_language_resource_plans() {
    let temp = tempfile::tempdir().expect("tempdir");
    let assets = temp.path();
    std::fs::create_dir_all(assets.join("en-US/demo")).expect("create en assets");
    std::fs::create_dir_all(assets.join("fr/demo/forms")).expect("create fr assets");
    std::fs::write(assets.join("en-US/demo.ftl"), "hello = Hello").expect("write en base");
    std::fs::write(assets.join("en-US/demo/ui.ftl"), "title = UI").expect("write en ui");
    std::fs::write(assets.join("fr/demo/forms/button.ftl"), "button = Bouton")
        .expect("write fr namespace");
    std::fs::write(assets.join("en-US/other-domain.ftl"), "other = Other")
        .expect("write host-provided domain");
    std::fs::create_dir_all(assets.join("fr/other-domain")).expect("create other domain");
    std::fs::write(assets.join("fr/other-domain/ui.ftl"), "other-ui = Autre")
        .expect("write host-provided namespace");
    std::fs::write(assets.join("fr/demo/ignore.txt"), "ignored").expect("write ignored");

    let plans = ResourcePlan::sparse_from_assets("demo", assets).expect("plans");

    assert_eq!(
        plans
            .languages()
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>(),
        vec!["en-US", "fr"]
    );
    assert_eq!(
        plans
            .namespaces()
            .iter()
            .map(ResolvedNamespace::as_str)
            .collect::<Vec<_>>(),
        vec!["forms/button", "ui"]
    );
    let specs_by_language = plans
        .resource_specs_by_language()
        .iter()
        .map(|(language, specs)| (language.to_string(), specs.clone()))
        .collect::<Vec<_>>();
    assert_eq!(
        specs_by_language,
        vec![
            (
                "en-US".to_string(),
                vec![
                    ModuleResourceSpec::base("demo", false),
                    ModuleResourceSpec::namespaced(
                        "demo",
                        &ResolvedNamespace::new("ui").expect("ui namespace"),
                        true
                    ),
                ]
            ),
            (
                "fr".to_string(),
                vec![ModuleResourceSpec::namespaced(
                    "demo",
                    &ResolvedNamespace::new("forms/button").expect("forms namespace"),
                    true
                ),]
            ),
        ]
    );
}

#[test]
fn sparse_from_assets_requires_base_when_no_namespaces_exist() {
    let temp = tempfile::tempdir().expect("tempdir");
    let assets = temp.path();
    std::fs::create_dir_all(assets.join("en")).expect("create en assets");
    std::fs::write(assets.join("en/demo.ftl"), "hello = Hello").expect("write en base");

    let plans = ResourcePlan::sparse_from_assets("demo", assets).expect("plans");

    let specs_by_language = plans
        .resource_specs_by_language()
        .iter()
        .map(|(language, specs)| (language.to_string(), specs.clone()))
        .collect::<Vec<_>>();
    assert_eq!(
        specs_by_language,
        vec![(
            "en".to_string(),
            vec![ModuleResourceSpec::base("demo", true)]
        )]
    );
}

#[test]
fn sparse_from_assets_rejects_noncanonical_locale_directories() {
    let temp = tempfile::tempdir().expect("tempdir");
    let assets = temp.path();
    std::fs::create_dir_all(assets.join("en-us")).expect("create invalid locale");
    std::fs::write(assets.join("en-us/demo.ftl"), "hello = Hello").expect("write base");

    let error = ResourcePlan::sparse_from_assets("demo", assets).expect_err("invalid locale");

    assert!(matches!(
        error,
        SparseAssetResourcePlanError::InvalidLocaleDirectory { ref raw_name, .. }
            if raw_name == "en-us"
    ));
    assert!(
        error
            .to_string()
            .contains("must use canonical BCP-47 form 'en-US'")
    );
}

#[test]
fn sparse_from_assets_rejects_invalid_namespaces() {
    let temp = tempfile::tempdir().expect("tempdir");
    let assets = temp.path();
    std::fs::create_dir_all(assets.join("en/demo")).expect("create en assets");
    std::fs::write(assets.join("en/demo/bad.ftl.ftl"), "bad = Bad")
        .expect("write invalid namespace");

    let error = ResourcePlan::sparse_from_assets("demo", assets).expect_err("invalid namespace");

    assert!(matches!(
        error,
        SparseAssetResourcePlanError::InvalidNamespace {
            namespace,
            domain,
            details: NamespacePathError::FileExtension,
        } if namespace == "bad.ftl" && domain == "demo"
    ));
}

#[test]
#[should_panic(expected = "resource_plan_for received invalid namespace")]
fn resource_plan_for_rejects_invalid_namespaces() {
    let _ = resource_plan_for("demo", &["../outside"]);
}

#[test]
fn try_resource_plan_for_reports_invalid_namespaces() {
    let err =
        try_resource_plan_for("demo", &["../outside"]).expect_err("invalid namespace should fail");

    assert_eq!(
        err,
        ResourcePlanError::InvalidNamespace {
            namespace: "../outside".to_string(),
            details: NamespacePathError::CurrentOrParentSegment
        }
    );
}

#[test]
fn try_resource_plan_for_reports_invalid_domain_without_panicking() {
    let err = try_resource_plan_for("../demo", &[]).expect_err("invalid domain should fail");

    assert!(matches!(
        err,
        ResourcePlanError::InvalidDomain { domain, .. } if domain == "../demo"
    ));
}

#[test]
fn resource_plan_uses_resolved_namespace_keys() {
    let plan = resource_plan_for("demo", &["ui/button"]);

    assert_eq!(plan[1].key, ResourceKey::from_static_path("demo/ui/button"));
    assert_eq!(plan[1].locale_relative_path, "demo/ui/button.ftl");
}

#[test]
fn required_and_optional_keys_reflect_plan_membership() {
    let plan = vec![
        ModuleResourceSpec::new(
            ResourceKey::from_static_path("demo"),
            LocaleRelativeFtlPath::from_static_path("demo.ftl"),
            true,
        ),
        ModuleResourceSpec::new(
            ResourceKey::from_static_path("demo/optional"),
            LocaleRelativeFtlPath::from_static_path("demo/optional.ftl"),
            false,
        ),
    ];

    let required = required_resource_keys_from_plan(&plan);
    let optional = optional_resource_keys_from_plan(&plan);

    assert!(required.contains(&ResourceKey::from_static_path("demo")));
    assert!(!required.contains(&ResourceKey::from_static_path("demo/optional")));
    assert!(optional.contains(&ResourceKey::from_static_path("demo/optional")));
    assert!(!optional.contains(&ResourceKey::from_static_path("demo")));

    let loaded = HashSet::from([ResourceKey::from_static_path("demo")]);
    assert!(locale_is_ready(&required, &loaded));

    let unloaded = HashSet::new();
    assert!(!locale_is_ready(&required, &unloaded));
}