es_fluent_shared/resource/
discovery.rs1use std::collections::{BTreeMap, BTreeSet};
2use std::path::Path;
3
4use crate::LanguageIdentifier;
5use crate::namespace::ResolvedNamespace;
6
7use super::{ModuleResourceSpec, ResourcePlan, SparseAssetResourcePlanError};
8
9#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct SparseAssetResourcePlans {
12 languages: Vec<LanguageIdentifier>,
13 namespaces: Vec<ResolvedNamespace>,
14 resource_specs_by_language: Vec<(LanguageIdentifier, Vec<ModuleResourceSpec>)>,
15}
16
17impl SparseAssetResourcePlans {
18 pub fn languages(&self) -> &[LanguageIdentifier] {
20 &self.languages
21 }
22
23 pub fn namespaces(&self) -> &[ResolvedNamespace] {
25 &self.namespaces
26 }
27
28 pub fn resource_specs_by_language(&self) -> &[(LanguageIdentifier, Vec<ModuleResourceSpec>)] {
30 &self.resource_specs_by_language
31 }
32
33 pub fn into_parts(
35 self,
36 ) -> (
37 Vec<LanguageIdentifier>,
38 Vec<ResolvedNamespace>,
39 Vec<(LanguageIdentifier, Vec<ModuleResourceSpec>)>,
40 ) {
41 (
42 self.languages,
43 self.namespaces,
44 self.resource_specs_by_language,
45 )
46 }
47}
48
49impl ResourcePlan {
50 pub fn sparse_from_assets(
56 domain: &str,
57 assets_root: &Path,
58 ) -> Result<SparseAssetResourcePlans, SparseAssetResourcePlanError> {
59 let entries = std::fs::read_dir(assets_root).map_err(|source| {
60 SparseAssetResourcePlanError::ReadAssetsRoot {
61 path: assets_root.to_path_buf(),
62 source,
63 }
64 })?;
65
66 let mut namespaces = BTreeSet::new();
67 let mut languages_with_base_file = BTreeSet::new();
68 let mut discovered_languages = BTreeSet::new();
69 let mut namespaces_by_language: BTreeMap<LanguageIdentifier, BTreeSet<ResolvedNamespace>> =
70 BTreeMap::new();
71
72 for entry in entries {
73 let entry =
74 entry.map_err(|source| SparseAssetResourcePlanError::ReadAssetsRootEntry {
75 path: assets_root.to_path_buf(),
76 source,
77 })?;
78 let path = entry.path();
79 if !path.is_dir() {
80 continue;
81 }
82
83 let raw_name = path
84 .file_name()
85 .and_then(|value| value.to_str())
86 .ok_or_else(|| SparseAssetResourcePlanError::NonUtf8LocaleDirectory {
87 path: path.clone(),
88 })?;
89 let canonical_lang =
90 crate::parse_canonical_language_identifier(raw_name).map_err(|details| {
91 SparseAssetResourcePlanError::InvalidLocaleDirectory {
92 raw_name: raw_name.to_string(),
93 path: path.clone(),
94 details,
95 }
96 })?;
97
98 let base_path = path.join(format!("{domain}.ftl"));
99 let namespace_root = path.join(domain);
100 let has_base_file = base_path.exists();
101 let discovered_namespaces = if namespace_root.is_dir() {
102 discover_namespaces(domain, &namespace_root)?
103 } else {
104 BTreeSet::new()
105 };
106
107 if has_base_file || !discovered_namespaces.is_empty() {
108 discovered_languages.insert(canonical_lang.clone());
109 }
110 if has_base_file {
111 languages_with_base_file.insert(canonical_lang.clone());
112 }
113 for namespace in discovered_namespaces {
114 namespaces.insert(namespace.clone());
115 namespaces_by_language
116 .entry(canonical_lang.clone())
117 .or_default()
118 .insert(namespace);
119 }
120 }
121
122 let namespaces: Vec<ResolvedNamespace> = namespaces.into_iter().collect();
123 let languages: Vec<LanguageIdentifier> = discovered_languages.into_iter().collect();
124 let mut resource_specs_by_language = Vec::with_capacity(languages.len());
125
126 for lang in &languages {
127 if namespaces.is_empty() {
128 let plan = Self::sparse_for_domain(domain, true, &[], true);
129 resource_specs_by_language.push((lang.clone(), plan.into_specs()));
130 continue;
131 }
132
133 let resolved_namespaces = namespaces_by_language
134 .get(lang)
135 .into_iter()
136 .flat_map(|entries| entries.iter())
137 .cloned()
138 .collect::<Vec<_>>();
139
140 let plan = Self::sparse_for_domain(
141 domain,
142 languages_with_base_file.contains(lang),
143 &resolved_namespaces,
144 false,
145 );
146 resource_specs_by_language.push((lang.clone(), plan.into_specs()));
147 }
148
149 Ok(SparseAssetResourcePlans {
150 languages,
151 namespaces,
152 resource_specs_by_language,
153 })
154 }
155}
156
157fn namespace_from_relative_ftl_path(
158 domain: &str,
159 namespace_root: &Path,
160 path: &Path,
161) -> Result<Option<ResolvedNamespace>, SparseAssetResourcePlanError> {
162 if !path.is_file() {
163 return Ok(None);
164 }
165
166 if path.extension().and_then(|ext| ext.to_str()) != Some("ftl") {
167 return Ok(None);
168 }
169
170 let relative_path = path.strip_prefix(namespace_root).map_err(|source| {
171 SparseAssetResourcePlanError::NamespaceRelativePath {
172 path: path.to_path_buf(),
173 root: namespace_root.to_path_buf(),
174 source,
175 }
176 })?;
177 let relative_without_extension = relative_path.with_extension("");
178 let mut components = Vec::new();
179
180 for component in relative_without_extension.components() {
181 let value = component.as_os_str().to_str().ok_or_else(|| {
182 SparseAssetResourcePlanError::NonUtf8NamespacePath {
183 path: relative_without_extension.clone(),
184 }
185 })?;
186 components.push(value.to_string());
187 }
188
189 if components.is_empty() {
190 return Ok(None);
191 }
192
193 let namespace = components.join("/");
194 ResolvedNamespace::new(namespace.clone())
195 .map(Some)
196 .map_err(|details| SparseAssetResourcePlanError::InvalidNamespace {
197 namespace,
198 domain: domain.to_string(),
199 details,
200 })
201}
202
203fn discover_namespaces(
204 domain: &str,
205 namespace_root: &Path,
206) -> Result<BTreeSet<ResolvedNamespace>, SparseAssetResourcePlanError> {
207 let mut namespaces = BTreeSet::new();
208 let mut pending = vec![namespace_root.to_path_buf()];
209
210 while let Some(current_dir) = pending.pop() {
211 let entries = std::fs::read_dir(¤t_dir).map_err(|source| {
212 SparseAssetResourcePlanError::ReadNamespaceDirectory {
213 path: current_dir.clone(),
214 source,
215 }
216 })?;
217
218 for entry in entries {
219 let entry = entry.map_err(|source| {
220 SparseAssetResourcePlanError::ReadNamespaceDirectoryEntry {
221 path: current_dir.clone(),
222 source,
223 }
224 })?;
225 let path = entry.path();
226
227 if path.is_dir() {
228 pending.push(path);
229 continue;
230 }
231
232 if let Some(namespace) =
233 namespace_from_relative_ftl_path(domain, namespace_root, &path)?
234 {
235 namespaces.insert(namespace);
236 }
237 }
238 }
239
240 Ok(namespaces)
241}