Skip to main content

es_fluent_shared/
path_utils.rs

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