Skip to main content

es_fluent_shared/
path_utils.rs

1//! Common path utilities for the es-fluent ecosystem.
2
3use crate::CanonicalLanguageIdentifierError;
4use crate::error::{EsFluentError, EsFluentResult};
5use std::path::Path;
6
7/// Parse a directory entry as a language identifier.
8///
9/// Returns `Ok(None)` if the entry is not a directory.
10pub fn parse_language_entry(
11    entry: std::fs::DirEntry,
12) -> EsFluentResult<Option<unic_langid::LanguageIdentifier>> {
13    if !entry.file_type()?.is_dir() {
14        return Ok(None);
15    }
16
17    let raw_name = entry.file_name();
18    let name = raw_name.into_string().map_err(|raw| {
19        EsFluentError::IoError(std::io::Error::new(
20            std::io::ErrorKind::InvalidData,
21            format!("Assets directory contains a non UTF-8 entry: {:?}", raw),
22        ))
23    })?;
24
25    let lang = crate::parse_canonical_language_identifier(&name).map_err(|err| match err {
26        CanonicalLanguageIdentifierError::Invalid { source, .. } => {
27            EsFluentError::invalid_language_identifier(&name, format!("Parse error: {}", source))
28        },
29        CanonicalLanguageIdentifierError::IcuInvalid { details, .. } => {
30            EsFluentError::invalid_language_identifier(&name, format!("ICU parse error: {details}"))
31        },
32        CanonicalLanguageIdentifierError::NonCanonical { canonical, .. } => {
33            EsFluentError::invalid_language_identifier(
34                &name,
35                format!(
36                    "Locale directory must use canonical BCP-47 casing '{}'",
37                    canonical
38                ),
39            )
40        },
41    })?;
42
43    Ok(Some(lang))
44}
45
46/// Validate that assets directory exists and is a directory.
47pub fn validate_assets_dir(assets_dir: &Path) -> EsFluentResult<()> {
48    if !assets_dir.exists() {
49        return Err(EsFluentError::assets_not_found(assets_dir));
50    }
51
52    if !assets_dir.is_dir() {
53        return Err(EsFluentError::IoError(std::io::Error::new(
54            std::io::ErrorKind::InvalidInput,
55            format!("Assets path '{}' is not a directory", assets_dir.display()),
56        )));
57    }
58
59    Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    fn read_dir_entry_by_name(parent: &Path, name: &str) -> std::fs::DirEntry {
67        std::fs::read_dir(parent)
68            .expect("read_dir")
69            .filter_map(|entry| entry.ok())
70            .find(|entry| entry.file_name() == name)
71            .expect("entry not found")
72    }
73
74    #[test]
75    fn parse_language_entry_handles_non_directory_and_valid_directory() {
76        let temp = tempfile::tempdir().expect("tempdir");
77        std::fs::write(temp.path().join("file.txt"), "not a directory").expect("write");
78        std::fs::create_dir_all(temp.path().join("en-US")).expect("create lang");
79
80        let file_entry = read_dir_entry_by_name(temp.path(), "file.txt");
81        assert_eq!(parse_language_entry(file_entry).expect("parse file"), None);
82
83        let dir_entry = read_dir_entry_by_name(temp.path(), "en-US");
84        let parsed = parse_language_entry(dir_entry)
85            .expect("parse dir")
86            .expect("language id");
87        assert_eq!(
88            parsed,
89            "en-US"
90                .parse::<unic_langid::LanguageIdentifier>()
91                .expect("language")
92        );
93    }
94
95    #[test]
96    fn parse_language_entry_rejects_invalid_identifiers_and_accepts_variants() {
97        let temp = tempfile::tempdir().expect("tempdir");
98        std::fs::create_dir_all(temp.path().join("not-a-lang!")).expect("create invalid");
99        std::fs::create_dir_all(temp.path().join("de-DE-1901")).expect("create variant");
100
101        let invalid_entry = read_dir_entry_by_name(temp.path(), "not-a-lang!");
102        let invalid_err = parse_language_entry(invalid_entry).expect_err("should fail");
103        assert!(matches!(
104            invalid_err,
105            EsFluentError::InvalidLanguageIdentifier { .. }
106        ));
107
108        let variant_entry = read_dir_entry_by_name(temp.path(), "de-DE-1901");
109        let variant_lang = parse_language_entry(variant_entry)
110            .expect("variant locale should parse")
111            .expect("language id");
112        assert_eq!(
113            variant_lang,
114            "de-DE-1901"
115                .parse::<unic_langid::LanguageIdentifier>()
116                .expect("language")
117        );
118    }
119
120    #[test]
121    fn parse_language_entry_rejects_noncanonical_locale_casing() {
122        let temp = tempfile::tempdir().expect("tempdir");
123        std::fs::create_dir_all(temp.path().join("en-us")).expect("create noncanonical");
124
125        let entry = read_dir_entry_by_name(temp.path(), "en-us");
126        let err = parse_language_entry(entry).expect_err("noncanonical locale should fail");
127        assert!(matches!(
128            err,
129            EsFluentError::InvalidLanguageIdentifier { .. }
130        ));
131        assert!(err.to_string().contains("en-US"));
132    }
133
134    #[test]
135    fn validate_assets_dir_checks_missing_file_and_directory() {
136        let temp = tempfile::tempdir().expect("tempdir");
137        let missing = temp.path().join("missing");
138        let file = temp.path().join("file.txt");
139        let dir = temp.path().join("assets");
140        std::fs::write(&file, "x").expect("write");
141        std::fs::create_dir_all(&dir).expect("mkdir");
142
143        let missing_err = validate_assets_dir(&missing).expect_err("missing should fail");
144        assert!(matches!(missing_err, EsFluentError::AssetsNotFound { .. }));
145
146        let file_err = validate_assets_dir(&file).expect_err("file should fail");
147        assert!(matches!(file_err, EsFluentError::IoError(_)));
148
149        validate_assets_dir(&dir).expect("directory should validate");
150    }
151}