use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use crate::LanguageIdentifier;
use crate::namespace::ResolvedNamespace;
use super::{ModuleResourceSpec, ResourcePlan, SparseAssetResourcePlanError};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SparseAssetResourcePlans {
languages: Vec<LanguageIdentifier>,
namespaces: Vec<ResolvedNamespace>,
resource_specs_by_language: Vec<(LanguageIdentifier, Vec<ModuleResourceSpec>)>,
}
impl SparseAssetResourcePlans {
pub fn languages(&self) -> &[LanguageIdentifier] {
&self.languages
}
pub fn namespaces(&self) -> &[ResolvedNamespace] {
&self.namespaces
}
pub fn resource_specs_by_language(&self) -> &[(LanguageIdentifier, Vec<ModuleResourceSpec>)] {
&self.resource_specs_by_language
}
pub fn into_parts(
self,
) -> (
Vec<LanguageIdentifier>,
Vec<ResolvedNamespace>,
Vec<(LanguageIdentifier, Vec<ModuleResourceSpec>)>,
) {
(
self.languages,
self.namespaces,
self.resource_specs_by_language,
)
}
}
impl ResourcePlan {
pub fn sparse_from_assets(
domain: &str,
assets_root: &Path,
) -> Result<SparseAssetResourcePlans, SparseAssetResourcePlanError> {
let entries = std::fs::read_dir(assets_root).map_err(|source| {
SparseAssetResourcePlanError::ReadAssetsRoot {
path: assets_root.to_path_buf(),
source,
}
})?;
let mut namespaces = BTreeSet::new();
let mut languages_with_base_file = BTreeSet::new();
let mut discovered_languages = BTreeSet::new();
let mut namespaces_by_language: BTreeMap<LanguageIdentifier, BTreeSet<ResolvedNamespace>> =
BTreeMap::new();
for entry in entries {
let entry =
entry.map_err(|source| SparseAssetResourcePlanError::ReadAssetsRootEntry {
path: assets_root.to_path_buf(),
source,
})?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let raw_name = path
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| SparseAssetResourcePlanError::NonUtf8LocaleDirectory {
path: path.clone(),
})?;
let canonical_lang =
crate::parse_canonical_language_identifier(raw_name).map_err(|details| {
SparseAssetResourcePlanError::InvalidLocaleDirectory {
raw_name: raw_name.to_string(),
path: path.clone(),
details,
}
})?;
let base_path = path.join(format!("{domain}.ftl"));
let namespace_root = path.join(domain);
let has_base_file = base_path.exists();
let discovered_namespaces = if namespace_root.is_dir() {
discover_namespaces(domain, &namespace_root)?
} else {
BTreeSet::new()
};
if has_base_file || !discovered_namespaces.is_empty() {
discovered_languages.insert(canonical_lang.clone());
}
if has_base_file {
languages_with_base_file.insert(canonical_lang.clone());
}
for namespace in discovered_namespaces {
namespaces.insert(namespace.clone());
namespaces_by_language
.entry(canonical_lang.clone())
.or_default()
.insert(namespace);
}
}
let namespaces: Vec<ResolvedNamespace> = namespaces.into_iter().collect();
let languages: Vec<LanguageIdentifier> = discovered_languages.into_iter().collect();
let mut resource_specs_by_language = Vec::with_capacity(languages.len());
for lang in &languages {
if namespaces.is_empty() {
let plan = Self::sparse_for_domain(domain, true, &[], true);
resource_specs_by_language.push((lang.clone(), plan.into_specs()));
continue;
}
let resolved_namespaces = namespaces_by_language
.get(lang)
.into_iter()
.flat_map(|entries| entries.iter())
.cloned()
.collect::<Vec<_>>();
let plan = Self::sparse_for_domain(
domain,
languages_with_base_file.contains(lang),
&resolved_namespaces,
false,
);
resource_specs_by_language.push((lang.clone(), plan.into_specs()));
}
Ok(SparseAssetResourcePlans {
languages,
namespaces,
resource_specs_by_language,
})
}
}
fn namespace_from_relative_ftl_path(
domain: &str,
namespace_root: &Path,
path: &Path,
) -> Result<Option<ResolvedNamespace>, SparseAssetResourcePlanError> {
if !path.is_file() {
return Ok(None);
}
if path.extension().and_then(|ext| ext.to_str()) != Some("ftl") {
return Ok(None);
}
let relative_path = path.strip_prefix(namespace_root).map_err(|source| {
SparseAssetResourcePlanError::NamespaceRelativePath {
path: path.to_path_buf(),
root: namespace_root.to_path_buf(),
source,
}
})?;
let relative_without_extension = relative_path.with_extension("");
let mut components = Vec::new();
for component in relative_without_extension.components() {
let value = component.as_os_str().to_str().ok_or_else(|| {
SparseAssetResourcePlanError::NonUtf8NamespacePath {
path: relative_without_extension.clone(),
}
})?;
components.push(value.to_string());
}
if components.is_empty() {
return Ok(None);
}
let namespace = components.join("/");
ResolvedNamespace::new(namespace.clone())
.map(Some)
.map_err(|details| SparseAssetResourcePlanError::InvalidNamespace {
namespace,
domain: domain.to_string(),
details,
})
}
fn discover_namespaces(
domain: &str,
namespace_root: &Path,
) -> Result<BTreeSet<ResolvedNamespace>, SparseAssetResourcePlanError> {
let mut namespaces = BTreeSet::new();
let mut pending = vec![namespace_root.to_path_buf()];
while let Some(current_dir) = pending.pop() {
let entries = std::fs::read_dir(¤t_dir).map_err(|source| {
SparseAssetResourcePlanError::ReadNamespaceDirectory {
path: current_dir.clone(),
source,
}
})?;
for entry in entries {
let entry = entry.map_err(|source| {
SparseAssetResourcePlanError::ReadNamespaceDirectoryEntry {
path: current_dir.clone(),
source,
}
})?;
let path = entry.path();
if path.is_dir() {
pending.push(path);
continue;
}
if let Some(namespace) =
namespace_from_relative_ftl_path(domain, namespace_root, &path)?
{
namespaces.insert(namespace);
}
}
}
Ok(namespaces)
}