mod.rs (15968B)
1 mod fetcher; 2 pub use fetcher::FileFetcher; 3 pub use fluent_fallback::types::{ResourceId, ToResourceId}; 4 5 use crate::env::ErrorReporter; 6 use crate::errors::L10nRegistryError; 7 use crate::fluent::FluentResource; 8 9 use std::{ 10 borrow::Borrow, 11 cell::RefCell, 12 fmt, 13 hash::{Hash, Hasher}, 14 pin::Pin, 15 rc::Rc, 16 task::Poll, 17 }; 18 19 use futures::{future::Shared, Future, FutureExt}; 20 use rustc_hash::FxHashMap; 21 use unic_langid::LanguageIdentifier; 22 23 pub type RcResource = Rc<FluentResource>; 24 25 /// An option type whose None variant is either optional or required. 26 /// 27 /// This behaves similarly to the standard-library [`Option`] type 28 /// except that there are two [`None`]-like variants: 29 /// [`ResourceOption::MissingOptional`] and [`ResourceOption::MissingRequired`]. 30 #[derive(Clone, Debug)] 31 pub enum ResourceOption { 32 /// An available resource. 33 Some(RcResource), 34 /// A missing optional resource. 35 MissingOptional, 36 /// A missing required resource. 37 MissingRequired, 38 } 39 40 impl ResourceOption { 41 /// Creates a resource option that is either [`ResourceOption::MissingRequired`] 42 /// or [`ResourceOption::MissingOptional`] based on whether the given [`ResourceId`] 43 /// is required or optional. 44 pub fn missing_resource(resource_id: &ResourceId) -> Self { 45 if resource_id.is_required() { 46 Self::MissingRequired 47 } else { 48 Self::MissingOptional 49 } 50 } 51 52 /// Returns [`true`] if this option contains a recource, otherwise [`false`]. 53 pub fn is_some(&self) -> bool { 54 matches!(self, Self::Some(_)) 55 } 56 57 /// Resource [`true`] if this option is missing a resource of any type, otherwise [`false`]. 58 pub fn is_none(&self) -> bool { 59 matches!(self, Self::MissingOptional | Self::MissingRequired) 60 } 61 62 /// Returns [`true`] if this option is missing a required resource, otherwise [`false`]. 63 pub fn is_required_and_missing(&self) -> bool { 64 matches!(self, Self::MissingRequired) 65 } 66 } 67 68 impl From<ResourceOption> for Option<RcResource> { 69 fn from(other: ResourceOption) -> Self { 70 match other { 71 ResourceOption::Some(id) => Some(id), 72 _ => None, 73 } 74 } 75 } 76 77 pub type ResourceFuture = Shared<Pin<Box<dyn Future<Output = ResourceOption>>>>; 78 79 #[derive(Debug, Clone)] 80 pub enum ResourceStatus { 81 /// The resource is missing. Don't bother trying to fetch. 82 MissingRequired, 83 MissingOptional, 84 /// The resource is loading and future will deliver the result. 85 Loading(ResourceFuture), 86 /// The resource is loaded and parsed. 87 Loaded(RcResource), 88 } 89 90 impl From<ResourceOption> for ResourceStatus { 91 fn from(input: ResourceOption) -> Self { 92 match input { 93 ResourceOption::Some(res) => Self::Loaded(res), 94 ResourceOption::MissingOptional => Self::MissingOptional, 95 ResourceOption::MissingRequired => Self::MissingRequired, 96 } 97 } 98 } 99 100 impl Future for ResourceStatus { 101 type Output = ResourceOption; 102 103 fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> { 104 use ResourceStatus::*; 105 106 let this = &mut *self; 107 108 match this { 109 MissingRequired => ResourceOption::MissingRequired.into(), 110 MissingOptional => ResourceOption::MissingOptional.into(), 111 Loaded(res) => ResourceOption::Some(res.clone()).into(), 112 Loading(res) => Pin::new(res).poll(cx), 113 } 114 } 115 } 116 117 /// `FileSource` provides a generic fetching and caching of fluent resources. 118 /// The user of `FileSource` provides a [`FileFetcher`](trait.FileFetcher.html) 119 /// implementation and `FileSource` takes care of the rest. 120 #[derive(Clone)] 121 pub struct FileSource { 122 /// Name of the FileSource, e.g. "browser" 123 pub name: String, 124 /// Pre-formatted path for the FileSource, e.g. "/browser/data/locale/{locale}/" 125 pub pre_path: String, 126 /// Metasource name for the FileSource, e.g. "app", "langpack" 127 /// Only sources from the same metasource are passed into the solver. 128 pub metasource: String, 129 /// The locales for which data is present in the FileSource, e.g. ["en-US", "pl"] 130 locales: Vec<LanguageIdentifier>, 131 shared: Rc<Inner>, 132 index: Option<Vec<String>>, 133 pub options: FileSourceOptions, 134 } 135 136 struct Inner { 137 fetcher: Box<dyn FileFetcher>, 138 error_reporter: Option<RefCell<Box<dyn ErrorReporter>>>, 139 entries: RefCell<FxHashMap<String, ResourceStatus>>, 140 } 141 142 impl fmt::Display for FileSource { 143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 144 write!(f, "{}", self.name) 145 } 146 } 147 148 impl PartialEq<FileSource> for FileSource { 149 fn eq(&self, other: &Self) -> bool { 150 self.name == other.name && self.metasource == other.metasource 151 } 152 } 153 154 impl Eq for FileSource {} 155 156 impl Hash for FileSource { 157 fn hash<H: Hasher>(&self, state: &mut H) { 158 self.name.hash(state) 159 } 160 } 161 162 #[derive(PartialEq, Clone, Debug, Default)] 163 pub struct FileSourceOptions { 164 pub allow_override: bool, 165 } 166 167 impl FileSource { 168 /// Create a `FileSource` using the provided [`FileFetcher`](../trait.FileFetcher.html). 169 pub fn new( 170 name: String, 171 metasource: Option<String>, 172 locales: Vec<LanguageIdentifier>, 173 pre_path: String, 174 options: FileSourceOptions, 175 fetcher: impl FileFetcher + 'static, 176 ) -> Self { 177 FileSource { 178 name, 179 metasource: metasource.unwrap_or_default(), 180 pre_path, 181 locales, 182 index: None, 183 shared: Rc::new(Inner { 184 entries: RefCell::new(FxHashMap::default()), 185 fetcher: Box::new(fetcher), 186 error_reporter: None, 187 }), 188 options, 189 } 190 } 191 192 pub fn new_with_index( 193 name: String, 194 metasource: Option<String>, 195 locales: Vec<LanguageIdentifier>, 196 pre_path: String, 197 options: FileSourceOptions, 198 fetcher: impl FileFetcher + 'static, 199 index: Vec<String>, 200 ) -> Self { 201 FileSource { 202 name, 203 metasource: metasource.unwrap_or_default(), 204 pre_path, 205 locales, 206 index: Some(index), 207 shared: Rc::new(Inner { 208 entries: RefCell::new(FxHashMap::default()), 209 fetcher: Box::new(fetcher), 210 error_reporter: None, 211 }), 212 options, 213 } 214 } 215 216 pub fn set_reporter(&mut self, reporter: impl ErrorReporter + 'static) { 217 let shared = Rc::get_mut(&mut self.shared).unwrap(); 218 shared.error_reporter = Some(RefCell::new(Box::new(reporter))); 219 } 220 } 221 222 fn calculate_pos_in_source(source: &str, idx: usize) -> (usize, usize) { 223 let mut ptr = 0; 224 let mut result = (1, 1); 225 for line in source.lines() { 226 let bytes = line.as_bytes().len(); 227 if ptr + bytes < idx { 228 ptr += bytes + 1; 229 result.0 += 1; 230 } else { 231 result.1 = idx - ptr + 1; 232 break; 233 } 234 } 235 result 236 } 237 238 impl FileSource { 239 fn get_path(&self, locale: &LanguageIdentifier, resource_id: &ResourceId) -> String { 240 format!( 241 "{}{}", 242 self.pre_path.replace("{locale}", &locale.to_string()), 243 resource_id.value, 244 ) 245 } 246 247 fn fetch_sync(&self, resource_id: &ResourceId) -> ResourceOption { 248 self.shared 249 .fetcher 250 .fetch_sync(resource_id) 251 .ok() 252 .map(|source| match FluentResource::try_new(source) { 253 Ok(res) => ResourceOption::Some(Rc::new(res)), 254 Err((res, errors)) => { 255 if let Some(reporter) = &self.shared.error_reporter { 256 reporter.borrow().report_errors( 257 errors 258 .into_iter() 259 .map(|e| L10nRegistryError::FluentError { 260 resource_id: resource_id.clone(), 261 loc: Some(calculate_pos_in_source(res.source(), e.pos.start)), 262 error: e.into(), 263 }) 264 .collect(), 265 ); 266 } 267 ResourceOption::Some(Rc::new(res)) 268 } 269 }) 270 .unwrap_or_else(|| ResourceOption::missing_resource(resource_id)) 271 } 272 273 /// Attempt to synchronously fetch resource for the combination of `locale` 274 /// and `path`. Returns `Some(ResourceResult)` if the resource is available, 275 /// else `None`. 276 pub fn fetch_file_sync( 277 &self, 278 locale: &LanguageIdentifier, 279 resource_id: &ResourceId, 280 overload: bool, 281 ) -> ResourceOption { 282 use ResourceStatus::*; 283 284 if self.has_file(locale, resource_id) == Some(false) { 285 return ResourceOption::missing_resource(resource_id); 286 } 287 288 let full_path_id = self 289 .get_path(locale, resource_id) 290 .to_resource_id(resource_id.resource_type); 291 292 let res = self.shared.lookup_resource(full_path_id.clone(), || { 293 self.fetch_sync(&full_path_id).into() 294 }); 295 296 match res { 297 MissingRequired => ResourceOption::MissingRequired, 298 MissingOptional => ResourceOption::MissingOptional, 299 Loaded(res) => ResourceOption::Some(res), 300 Loading(..) if overload => { 301 // A sync load has been requested for the same resource that has 302 // a pending async load in progress. How do we handle this? 303 // 304 // Ideally, we would sync load and resolve all the pending 305 // futures with the result. With the current Futures and 306 // combinators, it's unclear how to proceed. One potential 307 // solution is to store a oneshot::Sender and 308 // Shared<oneshot::Receiver>. When the async loading future 309 // resolves it would check that the state is still `Loading`, 310 // and if so, send the result. The sync load would do the same 311 // send on the oneshot::Sender. 312 // 313 // For now, we warn and return the resource, paying the cost of 314 // duplication of the resource. 315 self.fetch_sync(&full_path_id) 316 } 317 Loading(..) => { 318 panic!("[l10nregistry] Attempting to synchronously load file {} while it's being loaded asynchronously.", &full_path_id.value); 319 } 320 } 321 } 322 323 /// Attempt to fetch resource for the combination of `locale` and `path`. 324 /// Returns [`ResourceStatus`](enum.ResourceStatus.html) which is 325 /// a `Future` that can be polled. 326 pub fn fetch_file( 327 &self, 328 locale: &LanguageIdentifier, 329 resource_id: &ResourceId, 330 ) -> ResourceStatus { 331 use ResourceStatus::*; 332 333 if self.has_file(locale, resource_id) == Some(false) { 334 return ResourceOption::missing_resource(resource_id).into(); 335 } 336 337 let full_path_id = self 338 .get_path(locale, resource_id) 339 .to_resource_id(resource_id.resource_type); 340 341 self.shared.lookup_resource(full_path_id.clone(), || { 342 let shared = self.shared.clone(); 343 Loading(read_resource(full_path_id, shared).boxed_local().shared()) 344 }) 345 } 346 347 /// Determine if the `FileSource` has a loaded resource for the combination 348 /// of `locale` and `path`. Returns `Some(true)` if the file is loaded, else 349 /// `Some(false)`. `None` is returned if there is an outstanding async fetch 350 /// pending and the status is yet to be determined. 351 pub fn has_file<L: Borrow<LanguageIdentifier>>( 352 &self, 353 locale: L, 354 path: &ResourceId, 355 ) -> Option<bool> { 356 let locale = locale.borrow(); 357 if !self.locales.contains(locale) { 358 Some(false) 359 } else { 360 let full_path = self.get_path(locale, path); 361 if let Some(index) = &self.index { 362 return Some(index.iter().any(|p| p == &full_path)); 363 } 364 self.shared.has_file(&full_path) 365 } 366 } 367 368 pub fn locales(&self) -> &[LanguageIdentifier] { 369 &self.locales 370 } 371 372 pub fn get_index(&self) -> Option<&Vec<String>> { 373 self.index.as_ref() 374 } 375 } 376 377 impl std::fmt::Debug for FileSource { 378 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { 379 if let Some(index) = &self.index { 380 f.debug_struct("FileSource") 381 .field("name", &self.name) 382 .field("metasource", &self.metasource) 383 .field("locales", &self.locales) 384 .field("pre_path", &self.pre_path) 385 .field("index", index) 386 .finish() 387 } else { 388 f.debug_struct("FileSource") 389 .field("name", &self.name) 390 .field("metasource", &self.metasource) 391 .field("locales", &self.locales) 392 .field("pre_path", &self.pre_path) 393 .finish() 394 } 395 } 396 } 397 398 impl Inner { 399 fn lookup_resource<F>(&self, resource_id: ResourceId, f: F) -> ResourceStatus 400 where 401 F: FnOnce() -> ResourceStatus, 402 { 403 let mut lock = self.entries.borrow_mut(); 404 lock.entry(resource_id.value).or_insert_with(f).clone() 405 } 406 407 fn update_resource(&self, resource_id: ResourceId, resource: ResourceOption) -> ResourceOption { 408 let mut lock = self.entries.borrow_mut(); 409 let entry = lock.get_mut(&resource_id.value); 410 match entry { 411 Some(entry) => *entry = resource.clone().into(), 412 _ => panic!("Expected "), 413 } 414 resource 415 } 416 417 pub fn has_file(&self, full_path: &str) -> Option<bool> { 418 match self.entries.borrow().get(full_path) { 419 Some(ResourceStatus::MissingRequired) => Some(false), 420 Some(ResourceStatus::MissingOptional) => Some(false), 421 Some(ResourceStatus::Loaded(_)) => Some(true), 422 Some(ResourceStatus::Loading(_)) | None => None, 423 } 424 } 425 } 426 427 async fn read_resource(resource_id: ResourceId, shared: Rc<Inner>) -> ResourceOption { 428 let resource = shared 429 .fetcher 430 .fetch(&resource_id) 431 .await 432 .ok() 433 .map(|source| match FluentResource::try_new(source) { 434 Ok(res) => ResourceOption::Some(Rc::new(res)), 435 Err((res, errors)) => { 436 if let Some(reporter) = &shared.error_reporter { 437 reporter.borrow().report_errors( 438 errors 439 .into_iter() 440 .map(|e| L10nRegistryError::FluentError { 441 resource_id: resource_id.clone(), 442 loc: Some(calculate_pos_in_source(res.source(), e.pos.start)), 443 error: e.into(), 444 }) 445 .collect(), 446 ); 447 } 448 ResourceOption::Some(Rc::new(res)) 449 } 450 }) 451 .unwrap_or_else(|| ResourceOption::missing_resource(&resource_id)); 452 // insert the resource into the cache 453 shared.update_resource(resource_id, resource) 454 } 455 456 #[cfg(test)] 457 mod tests { 458 use super::*; 459 460 #[test] 461 fn calculate_source_pos() { 462 let source = r#" 463 key = Value 464 465 key2 = Value 2 466 "# 467 .trim(); 468 let result = calculate_pos_in_source(source, 0); 469 assert_eq!(result, (1, 1)); 470 471 let result = calculate_pos_in_source(source, 1); 472 assert_eq!(result, (1, 2)); 473 474 let result = calculate_pos_in_source(source, 12); 475 assert_eq!(result, (2, 1)); 476 477 let result = calculate_pos_in_source(source, 13); 478 assert_eq!(result, (3, 1)); 479 } 480 }