From 2871a9f067848a1f3923ba987abc73909353c54a Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Sat, 3 Oct 2020 03:09:06 -0500 Subject: [PATCH 1/4] Adding CldrPathsDownload to download CLDR JSON from a zip file --- components/cldr-json-data-provider/Cargo.toml | 14 +++ .../cldr-json-data-provider/src/cldr_paths.rs | 41 ++++++- .../src/download/cldr_paths_download.rs | 99 ++++++++++++++++ .../src/download/error.rs | 44 +++++++ .../src/download/io_util.rs | 107 ++++++++++++++++++ .../src/download/mod.rs | 5 + .../cldr-json-data-provider/src/error.rs | 16 +++ components/cldr-json-data-provider/src/lib.rs | 20 +++- .../cldr-json-data-provider/src/support.rs | 8 +- .../src/transform/dates.rs | 6 +- .../src/transform/mod.rs | 12 +- .../src/transform/plurals.rs | 10 +- .../tests/testdata/dummy.pdf | Bin 0 -> 13264 bytes .../tests/testdata/dummy.zip | Bin 0 -> 12480 bytes components/fs-data-provider/Cargo.toml | 8 +- .../src/bin/icu4x-cldr-export.rs | 72 +++++++----- .../src/export/fs_exporter.rs | 9 +- 17 files changed, 406 insertions(+), 65 deletions(-) create mode 100644 components/cldr-json-data-provider/src/download/cldr_paths_download.rs create mode 100644 components/cldr-json-data-provider/src/download/error.rs create mode 100644 components/cldr-json-data-provider/src/download/io_util.rs create mode 100644 components/cldr-json-data-provider/src/download/mod.rs create mode 100644 components/cldr-json-data-provider/tests/testdata/dummy.pdf create mode 100644 components/cldr-json-data-provider/tests/testdata/dummy.zip diff --git a/components/cldr-json-data-provider/Cargo.toml b/components/cldr-json-data-provider/Cargo.toml index d71c4bcc876..8d2a8de176f 100644 --- a/components/cldr-json-data-provider/Cargo.toml +++ b/components/cldr-json-data-provider/Cargo.toml @@ -23,3 +23,17 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde-tuple-vec-map = "1.0" tinystr = "0.3" + +# Dependencies for the download feature +urlencoding = { version = "1.1", optional = true } +reqwest = { version = "0.10.8", features = ["blocking"], optional = true } +unzip = { version = "0.1.0", optional = true } +dirs = { version = "3.0", optional = true } +log = { version = "0.4", optional = true } + +[dev-dependencies] +mktemp = "0.4" + +[features] +# Automatically download CLDR data from a host +download = ["urlencoding", "reqwest", "unzip", "dirs", "log"] diff --git a/components/cldr-json-data-provider/src/cldr_paths.rs b/components/cldr-json-data-provider/src/cldr_paths.rs index 68d7d2a25ca..298418c97b8 100644 --- a/components/cldr-json-data-provider/src/cldr_paths.rs +++ b/components/cldr-json-data-provider/src/cldr_paths.rs @@ -1,21 +1,50 @@ -use crate::error::MissingSourceError; +use crate::error::{Error, MissingSourceError}; use std::default::Default; use std::path::PathBuf; -/// Struct containing filesystem paths to the CLDR JSON resource directories. +/// Trait returning filesystem paths to CLDR JSON resource directories. /// The fields should be Ok if present. They default to Err when not present. +pub trait CldrPaths { + fn cldr_core(&self) -> Result; + fn cldr_dates(&self) -> Result; +} + +/// Implementation of CldrPaths for data directories already downloaded. +/// +/// # Example +/// +/// ``` +/// use icu_cldr_json_data_provider::CldrPathsLocal; +/// use icu_cldr_json_data_provider::CldrJsonDataProvider; +/// use std::path::PathBuf; +/// +/// let mut paths = CldrPathsLocal::default(); +/// paths.cldr_core = Ok(PathBuf::from("/path/to/cldr-core")); +/// // fill in other paths as necessary +/// +/// let data_provider = CldrJsonDataProvider::new(&paths); +/// ``` #[non_exhaustive] #[derive(Debug, PartialEq)] -pub struct CldrPaths { +pub struct CldrPathsLocal { /// Path to checkout of cldr-core: /// https://github.com/unicode-cldr/cldr-core pub cldr_core: Result, pub cldr_dates: Result, } -impl Default for CldrPaths { - fn default() -> CldrPaths { - CldrPaths { +impl CldrPaths for CldrPathsLocal { + fn cldr_core(&self) -> Result { + self.cldr_core.clone().map_err(|e| e.into()) + } + fn cldr_dates(&self) -> Result { + self.cldr_dates.clone().map_err(|e| e.into()) + } +} + +impl Default for CldrPathsLocal { + fn default() -> CldrPathsLocal { + CldrPathsLocal { cldr_core: Err(MissingSourceError { src: "cldr-core" }), cldr_dates: Err(MissingSourceError { src: "cldr-dates" }), } diff --git a/components/cldr-json-data-provider/src/download/cldr_paths_download.rs b/components/cldr-json-data-provider/src/download/cldr_paths_download.rs new file mode 100644 index 00000000000..2b95d65f0e2 --- /dev/null +++ b/components/cldr-json-data-provider/src/download/cldr_paths_download.rs @@ -0,0 +1,99 @@ +use super::io_util; +use crate::error::Error; +use crate::CldrPaths; +use std::path::PathBuf; + +/// Implementation of CldrPaths that downloads CLDR data directories on demand. +/// +/// # Example +/// +/// ``` +/// use icu_cldr_json_data_provider::download::CldrPathsDownload; +/// use icu_cldr_json_data_provider::CldrJsonDataProvider; +/// use std::path::PathBuf; +/// +/// let paths = CldrPathsDownload::from_github_tag("36.0.0"); +/// +/// let data_provider = CldrJsonDataProvider::new(&paths); +/// +/// fn demo<'d>(data_provider: &'d CldrJsonDataProvider<'d, 'd>) { +/// use std::borrow::Cow; +/// use icu_data_provider::prelude::*; +/// use icu_data_provider::structs::plurals::PluralRuleStringsV1; +/// +/// let data: Cow = data_provider +/// .load(&DataRequest { +/// data_entry: DataEntry { +/// langid: "uk".parse().unwrap(), +/// variant: None, +/// }, +/// data_key: icu_data_key!(plurals: ordinal@1), +/// }) +/// .unwrap() +/// .take_payload() +/// .unwrap(); +/// assert_eq!(data.few, Some(Cow::Borrowed("n % 10 = 3 and n % 100 != 13"))); +/// } +/// +/// // Calling demo(&data_provider) will cause the data to actually get downloaded. +/// //demo(&data_provider); +/// ``` +pub struct CldrPathsDownload { + /// Directory where downloaded files are stored. + pub cache_dir: PathBuf, + + pub cldr_core: CldrZipFileInfo, + pub cldr_dates: CldrZipFileInfo, +} + +// TODO(#297): Implement this async. +impl CldrPaths for CldrPathsDownload { + fn cldr_core(&self) -> Result { + self.cldr_core.download_and_unzip(&self) + } + fn cldr_dates(&self) -> Result { + self.cldr_dates.download_and_unzip(&self) + } +} + +impl CldrPathsDownload { + /// Creates a CldrPathsDownload that downloads files to the system cache directory + /// as determined by dirs::cache_dir(). + /// + /// github_tag should be a tag in the CLDR JSON repositories, such as "36.0.0": + /// https://github.com/unicode-cldr/cldr-core/tags + pub fn from_github_tag(github_tag: &str) -> Self { + Self { + cache_dir: dirs::cache_dir().unwrap().join("icu4x").join("cldr"), + cldr_core: CldrZipFileInfo { + url: format!( + "https://github.com/unicode-cldr/cldr-core/archive/{}.zip", + github_tag + ), + top_dir: format!("cldr-core-{}", github_tag), + }, + cldr_dates: CldrZipFileInfo { + url: format!( + "https://github.com/unicode-cldr/cldr-dates-modern/archive/{}.zip", + github_tag + ), + top_dir: format!("cldr-dates-modern-{}", github_tag), + }, + } + } +} + +pub struct CldrZipFileInfo { + /// The URL to the remote zip file + pub url: String, + /// The directory name in the unpacked zip fle + pub top_dir: String, +} + +impl CldrZipFileInfo { + fn download_and_unzip(&self, parent: &CldrPathsDownload) -> Result { + io_util::download_and_unzip(&self.url, &parent.cache_dir) + .map(|p| p.join(&self.top_dir)) + .map_err(|e| e.into()) + } +} diff --git a/components/cldr-json-data-provider/src/download/error.rs b/components/cldr-json-data-provider/src/download/error.rs new file mode 100644 index 00000000000..5b1950bd8bd --- /dev/null +++ b/components/cldr-json-data-provider/src/download/error.rs @@ -0,0 +1,44 @@ +use std::error; +use std::fmt; +use std::io; +use std::path::PathBuf; + +#[derive(Debug)] +pub enum Error { + Io(io::Error, PathBuf), + Reqwest(reqwest::Error), + HttpStatus(reqwest::StatusCode, String), +} + +impl From for Error { + /// Note: Prefer adding the path to Error::Io instead of using this conversion. + fn from(err: io::Error) -> Error { + Error::Io(err, PathBuf::new()) + } +} + +impl From for Error { + fn from(err: reqwest::Error) -> Error { + Error::Reqwest(err) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Io(err, path) => write!(f, "{}: {}", err, path.to_string_lossy()), + Error::Reqwest(err) => err.fmt(f), + Error::HttpStatus(status, url) => write!(f, "HTTP request failed: {}: {}", status, url), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err, _) => Some(err), + Error::Reqwest(err) => Some(err), + _ => None, + } + } +} diff --git a/components/cldr-json-data-provider/src/download/io_util.rs b/components/cldr-json-data-provider/src/download/io_util.rs new file mode 100644 index 00000000000..f10e73d7053 --- /dev/null +++ b/components/cldr-json-data-provider/src/download/io_util.rs @@ -0,0 +1,107 @@ +use super::error::Error; +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; +use std::time::Instant; +use unzip::Unzipper; + +macro_rules! map_io_err { + ($path_ref:ident) => { + |err| Error::Io(err, $path_ref.to_owned()) + }; +} + +#[cfg(test)] +fn assert_files_eq(expected_file_path: &Path, actual_file_path: &Path) { + use std::io::Read; + let mut expected_buf = Vec::new(); + File::open(expected_file_path) + .unwrap() + .read_to_end(&mut expected_buf) + .unwrap(); + let mut actual_buf = Vec::new(); + File::open(&actual_file_path) + .unwrap() + .read_to_end(&mut actual_buf) + .unwrap(); + assert_eq!(expected_buf, actual_buf); +} + +// Synchronously download url and save it to destination. +// TODO(#297): Implement this async. +fn download_sync(url: &str, destination: &Path) -> Result<(), Error> { + log::info!("Downloading: {}", url); + let start = Instant::now(); + let mut response = reqwest::blocking::get(url)?; + if !response.status().is_success() { + return Err(Error::HttpStatus(response.status(), url.to_string())); + } + log::info!("Status: {}", response.status()); + let mut file = File::create(destination).map_err(map_io_err!(destination))?; + response.copy_to(&mut file)?; + log::info!("Finished in {:.2} seconds", start.elapsed().as_secs_f64()); + Ok(()) +} + +#[test] +fn test_download_sync() -> Result<(), Error> { + let temp_file = mktemp::Temp::new_file()?; + download_sync( + "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", + &temp_file, + )?; + assert_files_eq(&PathBuf::from("./tests/testdata/dummy.pdf"), &temp_file); + Ok(()) +} + +/// Synchronously unpack a zip file into a destination directory. +// TODO(#297): Implement this async. +fn unzip_sync(zip_path: &Path, dir_path: &Path) -> Result<(), Error> { + let reader = File::open(zip_path).map_err(map_io_err!(zip_path))?; + log::info!("Unzipping..."); + let start = Instant::now(); + Unzipper::new(reader, dir_path) + .unzip() + .map_err(map_io_err!(dir_path))?; + log::info!("Unzipped in {:.2} seconds", start.elapsed().as_secs_f64()); + Ok(()) +} + +#[test] +fn test_unzip_sync() -> Result<(), Error> { + let temp_dir = mktemp::Temp::new_dir()?; + unzip_sync(&PathBuf::from("./tests/testdata/dummy.zip"), &temp_dir)?; + assert_files_eq( + &PathBuf::from("./tests/testdata/dummy.pdf"), + &temp_dir.to_path_buf().join("dummy.pdf"), + ); + Ok(()) +} + +/// Downloads and unpacks a zip file, returning the path to the unpacked directory. +/// +/// `cache_dir` is a directory where both the zip file and the unpacked directory will be +/// saved. If the zip file has already been downloaded, it will not be downloaded again. +pub fn download_and_unzip(zip_file_url: &str, cache_dir: &Path) -> Result { + fs::create_dir_all(cache_dir).map_err(map_io_err!(cache_dir))?; + + let zip_dir = cache_dir.to_path_buf().join("zips"); + fs::create_dir_all(&zip_dir).map_err(map_io_err!(zip_dir))?; + + let data_dir = cache_dir.to_path_buf().join("data"); + fs::create_dir_all(&data_dir).map_err(map_io_err!(data_dir))?; + + let basename = urlencoding::encode(zip_file_url); + let mut zip_path = zip_dir.join(&basename); + zip_path.set_extension("zip"); + let dir_path = data_dir.join(&basename); + + if !zip_path.exists() { + download_sync(zip_file_url, &zip_path)?; + } + + if !dir_path.exists() { + unzip_sync(&zip_path, &dir_path)?; + } + + Ok(dir_path) +} diff --git a/components/cldr-json-data-provider/src/download/mod.rs b/components/cldr-json-data-provider/src/download/mod.rs new file mode 100644 index 00000000000..ec5b0ef8660 --- /dev/null +++ b/components/cldr-json-data-provider/src/download/mod.rs @@ -0,0 +1,5 @@ +mod cldr_paths_download; +pub mod error; +mod io_util; + +pub use cldr_paths_download::CldrPathsDownload; diff --git a/components/cldr-json-data-provider/src/error.rs b/components/cldr-json-data-provider/src/error.rs index 51a20668a08..0d244bbf6be 100644 --- a/components/cldr-json-data-provider/src/error.rs +++ b/components/cldr-json-data-provider/src/error.rs @@ -1,12 +1,16 @@ use std::error; use std::fmt; +#[cfg(feature = "download")] +use crate::download::error::Error as DownloadError; + #[non_exhaustive] #[derive(Debug)] pub enum Error { JsonError(serde_json::error::Error), IoError(std::io::Error, std::path::PathBuf), MissingSource(MissingSourceError), + Download(Box), PoisonError, } @@ -33,12 +37,23 @@ impl From for Error { } } +#[cfg(feature = "download")] +impl From for Error { + fn from(err: DownloadError) -> Error { + match err { + DownloadError::Io(err, path) => Error::IoError(err, path), + _ => Error::Download(Box::new(err)), + } + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Error::JsonError(err) => write!(f, "{}", err), Error::IoError(err, path) => write!(f, "{}: {}", err, path.to_string_lossy()), Error::MissingSource(err) => err.fmt(f), + Error::Download(err) => err.fmt(f), Error::PoisonError => write!(f, "poisoned lock on CLDR provider"), } } @@ -49,6 +64,7 @@ impl error::Error for Error { match self { Error::JsonError(err) => Some(err), Error::IoError(err, _) => Some(err), + Error::Download(err) => Some(err.as_ref()), _ => None, } } diff --git a/components/cldr-json-data-provider/src/lib.rs b/components/cldr-json-data-provider/src/lib.rs index e967c7bc251..c53e77fbf27 100644 --- a/components/cldr-json-data-provider/src/lib.rs +++ b/components/cldr-json-data-provider/src/lib.rs @@ -1,4 +1,18 @@ -// #![feature(type_alias_impl_trait)] +//! `icu-cldr-json-data-provider` is one of the [`ICU4X`] components. +//! +//! It contains implementations of the [`DataProvider`] interface based on the JSON files +//! shipped by CLDR. You create a CldrPaths and then pass it into CldrJsonDataProvider. +//! +//! This crate contains two implementations of CldrPaths: +//! +//! - `CldrPathsLocal` points to local copies of the CLDR JSON repositories. +//! - `CldrPathsDownload` downloads and caches the CLDR JSON repositories. Requires the +//! "download" feature. +//! +//! **Important:** This data provider implementation is not optimized for production use. +//! It is much more efficient if you use [`FsDataProvider`] instead. +//! +//! [`ICU4X`]: https://github.com/unicode-org/icu4x mod cldr_langid; mod cldr_paths; @@ -8,6 +22,10 @@ mod support; pub mod transform; +#[cfg(feature = "download")] +pub mod download; + pub use cldr_paths::CldrPaths; +pub use cldr_paths::CldrPathsLocal; pub use error::Error as CldrError; pub use transform::CldrJsonDataProvider; diff --git a/components/cldr-json-data-provider/src/support.rs b/components/cldr-json-data-provider/src/support.rs index f047251925c..180bc1d9e99 100644 --- a/components/cldr-json-data-provider/src/support.rs +++ b/components/cldr-json-data-provider/src/support.rs @@ -28,14 +28,14 @@ fn map_poison(_err: E) -> DataError { /// A lazy-initialized CLDR JSON data provider. impl<'b, 'd, T> LazyCldrProvider where - T: DataProvider<'d> + DataKeySupport + DataEntryCollection + TryFrom<&'b CldrPaths>, - >::Error: 'static + std::error::Error, + T: DataProvider<'d> + DataKeySupport + DataEntryCollection + TryFrom<&'b dyn CldrPaths>, + >::Error: 'static + std::error::Error, { /// Call T::load, initializing T if necessary. pub fn try_load( &self, req: &DataRequest, - cldr_paths: &'b CldrPaths, + cldr_paths: &'b dyn CldrPaths, ) -> Result>, DataError> { if T::supports_key(&req.data_key).is_err() { return Ok(None); @@ -57,7 +57,7 @@ where pub fn try_iter( &self, data_key: &DataKey, - cldr_paths: &'b CldrPaths, + cldr_paths: &'b dyn CldrPaths, ) -> Result>>, DataError> { if T::supports_key(data_key).is_err() { return Ok(None); diff --git a/components/cldr-json-data-provider/src/transform/dates.rs b/components/cldr-json-data-provider/src/transform/dates.rs index 17e01c1f9e5..74c78954f8c 100644 --- a/components/cldr-json-data-provider/src/transform/dates.rs +++ b/components/cldr-json-data-provider/src/transform/dates.rs @@ -16,12 +16,12 @@ pub struct DatesProvider<'d> { _phantom: PhantomData<&'d ()>, // placeholder for when we need the lifetime param } -impl TryFrom<&CldrPaths> for DatesProvider<'_> { +impl TryFrom<&dyn CldrPaths> for DatesProvider<'_> { type Error = Error; - fn try_from(cldr_paths: &CldrPaths) -> Result { + fn try_from(cldr_paths: &dyn CldrPaths) -> Result { let mut data = vec![]; - let path = cldr_paths.cldr_dates.clone()?.join("main"); + let path = cldr_paths.cldr_dates()?.join("main"); let locale_dirs = get_subdirectories(&path)?; diff --git a/components/cldr-json-data-provider/src/transform/mod.rs b/components/cldr-json-data-provider/src/transform/mod.rs index 39d0cbc6f95..3114a01234a 100644 --- a/components/cldr-json-data-provider/src/transform/mod.rs +++ b/components/cldr-json-data-provider/src/transform/mod.rs @@ -10,13 +10,13 @@ use icu_data_provider::iter::DataEntryCollection; use icu_data_provider::prelude::*; pub struct CldrJsonDataProvider<'a, 'd> { - pub cldr_paths: &'a CldrPaths, + pub cldr_paths: &'a dyn CldrPaths, plurals: LazyCldrProvider>, dates: LazyCldrProvider>, } impl<'a, 'd> CldrJsonDataProvider<'a, 'd> { - pub fn new(cldr_paths: &'a CldrPaths) -> Self { + pub fn new(cldr_paths: &'a dyn CldrPaths) -> Self { CldrJsonDataProvider { cldr_paths, plurals: Default::default(), @@ -27,10 +27,10 @@ impl<'a, 'd> CldrJsonDataProvider<'a, 'd> { impl<'a, 'd> DataProvider<'d> for CldrJsonDataProvider<'a, 'd> { fn load(&self, req: &DataRequest) -> Result, DataError> { - if let Some(result) = self.plurals.try_load(req, &self.cldr_paths)? { + if let Some(result) = self.plurals.try_load(req, self.cldr_paths)? { return Ok(result); } - if let Some(result) = self.dates.try_load(req, &self.cldr_paths)? { + if let Some(result) = self.dates.try_load(req, self.cldr_paths)? { return Ok(result); } Err(DataError::UnsupportedDataKey(req.data_key)) @@ -42,10 +42,10 @@ impl<'a, 'd> DataEntryCollection for CldrJsonDataProvider<'a, 'd> { &self, data_key: &DataKey, ) -> Result>, DataError> { - if let Some(resp) = self.plurals.try_iter(data_key, &self.cldr_paths)? { + if let Some(resp) = self.plurals.try_iter(data_key, self.cldr_paths)? { return Ok(resp); } - if let Some(resp) = self.dates.try_iter(data_key, &self.cldr_paths)? { + if let Some(resp) = self.dates.try_iter(data_key, self.cldr_paths)? { return Ok(resp); } Err(DataError::UnsupportedDataKey(*data_key)) diff --git a/components/cldr-json-data-provider/src/transform/plurals.rs b/components/cldr-json-data-provider/src/transform/plurals.rs index 49b3102f150..9c0ec76e379 100644 --- a/components/cldr-json-data-provider/src/transform/plurals.rs +++ b/components/cldr-json-data-provider/src/transform/plurals.rs @@ -18,13 +18,12 @@ pub struct PluralsProvider<'d> { _phantom: PhantomData<&'d ()>, // placeholder for when we need the lifetime param } -impl TryFrom<&CldrPaths> for PluralsProvider<'_> { +impl TryFrom<&dyn CldrPaths> for PluralsProvider<'_> { type Error = Error; - fn try_from(cldr_paths: &CldrPaths) -> Result { + fn try_from(cldr_paths: &dyn CldrPaths) -> Result { let cardinal_rules = { let path = cldr_paths - .cldr_core - .clone()? + .cldr_core()? .join("supplemental") .join("plurals.json"); let data: cldr_json::Resource = serde_json::from_reader(open_reader(path)?)?; @@ -32,8 +31,7 @@ impl TryFrom<&CldrPaths> for PluralsProvider<'_> { }; let ordinal_rules = { let path = cldr_paths - .cldr_core - .clone()? + .cldr_core()? .join("supplemental") .join("ordinals.json"); let data: cldr_json::Resource = serde_json::from_reader(open_reader(path)?)?; diff --git a/components/cldr-json-data-provider/tests/testdata/dummy.pdf b/components/cldr-json-data-provider/tests/testdata/dummy.pdf new file mode 100644 index 0000000000000000000000000000000000000000..774c2ea70c55104973794121eae56bcad918da97 GIT binary patch literal 13264 zcmaibWmsIxvUW%|5FkJZ7A&~y%m9Oj;I6>~WPrgfxD$eVfZ*=#?hsspJHa(bATYRn zGueBev(G*EKHr+BrK+pDs^6;aH9u<6Dv3$30@ygwX}fZ|TDt1G($Rqw927PN=I8~c_R69-cY5S*jJE@5Wr0JUS6u!J~3#h`{ZMo=LkbbALoD8vfgB}Fh|2>mhOnfS$3 zNV5}8Ox=$fj;C0=UKy*{myZZPRVS|0mqr-HxZAy;()@wxQ}MN`QWAZTXb3Z&Om9W2 zbnA^OWoQbAW|3W^fw#J;YzDato8*`rHQs+@W70D&SyT{wb`SN*3nI z5G%$wJlq932=n{60Eii*9H8dFih2ks?QY=>nAFL=5g^P@#b{YUEHt0S$D7WbX zx%TzvzIK%zpvzLEd9LNr0ch#LFf_(9 zEGt0C9v~%b54vynAc{~;v&2?S(-sTTft@9CABMNFZHtY1W0-99CEbUNfp_yu{LDBz z@8z^$LPN$wX4Hi+dZQs6K3QiKKF0}Nme@EII;;F}IplC(YvT*C3-Oh#(A}e5pIz01 zyR}D2|ftBF0T=1moHZy}$wS*PSCmSzHQ%x z2tCQQCx4jt7w1cuhY69~eH`31KC4)ZZJ^)f=IabocAkBPa zEeg25yPX&9-i_N(Qiq!I3RDrfx&0t^i)&MSQ1D(w%|%#LTNr>1cPiltAYO;6kBn(B?r11c^Bz~#)z5~~V+*`U)lDFtKbZ|;? z&4wTUtK=KE&uQIWUQv1mDE;LIhXXgx44PMa@%Z<7a& zx45^oYSnei^~%}`?!O-+cgfSmn_c?`=Gmm*Z^I(96ve&$zDs|)r84)IEEiE1kfQ$q zm3km*m1)PjdU9nkk9BTlidI1~M|O~WfP7AUu2T}d>5is9l$<%;7r2&Re06w>W$KM~ zqITBTd=Ln>^crw`_N?{ z;2d_=E0n!*NisQ|XYuX9q3+UcqdA(MC45|>2tz^c6HdZOmXTB?X2Elx@_0f)1z&-gS;UxN`>Ll-kWb0X0 zTrQis=w9sJ(q7k|@|k3SA~DJ@uMXP@4(Mgn+LJC+3F~3NHW71pIzY(aHg~{O+squi zWO_|F>78)L5*gcRXXRD9IzQ(ddSxh}E7(8sC~EYrOz$9BkSMBCkGGO9FuZ{#*mW+h zvwE7d)6Ag=a*R5URs>}qdqb_E6g)kN2Wel;pWe9=hZ)XvRZR!RQg&gxAPGj8J0!gR zrdV<2@MZQ?_Ocbd5@0zI?t>$z3eD80_h^{DI)H5lk`T4lbn8kteH3%fOBH^g26#lLN2&P^s zr&d05GDs)u_8OKzCgNxllk5pLC<2wKmghL{zW%}5^}%S$?d=3OzjaSzT3>uWYikZN z2ZcR7*L|%UMs|u)wMi7#vkN?cxlBcyAM80Tyzzv&zHMF1TH9?Mx5&E57P^)^zE5N| z^foq}!--if$Uj=U6Tc>EM!Pv)e^_SZSdvtQ=@>)(ONejQ!XW8u6>ESl<*s^6cH;Q1 z#n}nL{#|{l}}@td^zNSA;R{`3A&Jjr8L9(3^2FSyZ1W9$%;!XP#N2 z-SAzyRfxtgq^py7_3*GJFO%x_v<`xJ46`~S*IukgQDKfLxzFnS&GYL!1LA{I z!c#{A90{k(b*tUfbgjOH>}{#V;%^O+LUU<*#QkLtWzjho*Kb?Cr&wC38%wxpn}^Wy zG6EpV9x3xioCWA6H6=aE3)%jmZePu#Ji7wy0CmkDZNG`a{J1i-2`Bt&UrFb&<~V$^ zy9i`R1<35M&{mtCz144%v#7LKBTPPApjoV}#W-gDc5cn;A@Mbt#zXUK@J9^vj*ME( zo8(%K{c-KDr8n1-I&Mjn)*i|pF|7l*`fXvo8-z&j{$NOfUPM-xILbX1D29IHp|__B zL*JQ8*7-VrZVY*&$!PiE%zv@osg`qx0M8+w9iy7Az7;HYezs;5NRvrdNM~t@o}5Gc zjagk3Y_>6!Ct;ITqhu3FojJO^(^SG-($M4|frkp?4y-QoSmFcw9Z%(z?eC0kGi9@? zm(vAgXU|%!6_)CrnqYL-Hj@B5hA?#8C3G^cjd?0dMSZ!wbe%O4bWvlIG=nwOEInVj zhjzd`Bry8sXBTfIUr+juZH5JyE#7~UQiwR!gmG@wm}aNyo`13xEo)tzP64MWWG|j8 z8u8a2_=C2FdRZ9(eG&Au`@$mY9vvWldP-@wj5@38H0W2V8wnaQO?!)qoS_J=(ieoI zOvH}mkBRh_p1oTW66+?3u-GH2Ex~c=BQiwpJ zJlF7O2PBaCojRRL_mp44*Iq}vcRFpBD>V9M7do5{w&b;4^<_V~Vr{+O_&hz9k5Sm` zq3|%Z(6B5~wz2k0iH-QlafAa>1%ZebdxkR;6SdA?@dK|4Jf8PIO%64Fpw$6RYG2R# zX>Iq(xf`5Xk)79-@;BAQjlWu|w@Ss3sJv3Ew&%lBu-H?vYsC8XPJD!lkv*A~z_-k= zLOaM?B5}$Sf-KF5BWHoB51WFA{GlweQna618{*tqVn)YKUVq?khU_=QER9uW?N17xgAponbjg0W`=>f;sulH3?st)Y_@k$We2-__a>^{E78lUiI13qq!3# zwxMEl75MK1q`~J>ST#?`mUx#vr%-jwpZ+DV;W!0KNkZmO#sK)zt)H@`EQl6RRWhwb z0&E7|fG~@z)wlK1-RsxN#8Gr)D5=xpv=b}=CWPbwz@(9bIhD0Crd-Q>qEo>~Gh{X7 z77AK5>TfF0wK!?7Nx!<5uDy?D{Qg$SEc_R3J9EuH!Z@qmEJ*QRRHd3BPirM6783nv zAnab$>rhdDJ6pO@%Ox(}BYw{Ba<3|=A%Fg5_Hfxj{%CfzZCFO{?%h&=?%CNBvi&p; z(otqN>+5giLLa^*G?xzN30=IgQrV+r7dW4bX;zKtuD)O$UnwAKC?CpkPt{77nUArH ze-jKcCfRrOlp(Q^b&W}mrgt4n%wikNxeSBBE_n>K-IOIzi6!<)xGRYA)wGgqp^s@d46N#krDHPc#9SOgXhI7Vbj?B z%c6@8dCOGPYBoNE#3N7HD^ihbC9*xGm6chu;?fcuv)s01keHHZ1vXl5D;29O7wZBr zyPzyLZHKMtUI%PK+*X2zTFtaDzU1qn(H=hRRj-SoJw7I5i%4b0u=&InEAKgoae-lp zXk0SkjlJ52HruS*1QykTZ&aCN`PbcKuw$1st{peJ@&aF^aR@~{XA@L&YvK%+VU}G4 ze5iuesu&i6=*#nvHbm_v-ZLr5^Ij#|YSAper4XpsH;0x(2h1-tIobIy;0~2a( z!G($SB!iu#P;;hGeI~C`O=-3|d~zoB0!`*JrU-)Ko_X5#kSpy5o^z49RG;{j#l~45 zF?X9Ih4IdviT(8@+q|`BveLTprbESZ6^2I&ew|V3pDXRe9gSyXT)zzqKQ;gCD;p+( zM)2(;YJ%P5)X(N3ZSn>dn6UIcEcvQOXZBn}uD!7V0yXr$f+d@eTSYoquPit2S8cPW zA8t3dX)Cv{0cKF`@e|PP(xS0|z2_R0(P6)#+kC$0^5- z$7Hs|bOQanE z1oJ;uh(dYiDt}mVmtC3&HaGT6-dY429v#ySHJ7V)C8ow=PSmnEI)=b3_RJsU(S*+J zV$p3>RkK?DFvTc;(-T=h!1u~CP!pE=0eSSu#c@N7S0Z57CPg}!5z{QL#`2v?DJDt^ zCGN{0p-&&=)Sb28Xlo;ZXc^CGdwL9prf30uu$y5aPeWD6WIk4%%~DEhTiwOvy!rS% z&3z#DWo2qBA*=M2xIu=_R0sbrmP;Y?_rRa^k}3WYU6n9H^(})Zi-woMKKXfgbab@J zWx3DUr0MLpdDYk_LO8As}d*Z=x^K+uIv#T&SnY6&C$9 zBn1u`G#TBt+n5b%a;Cr0h^sm5Fl^OdxJ^8IebW);DWATq#Ba=#rggj*wNKy5NMzz& zBm`bk9bcSVPJbC`dHrI>o^=LSvTFpT`VAK`x_naOpvS~*l2$1vIk$avBA!|aeZ+7c z$_9Zzh>fc4$uX&w@-$VORCscG(B)OA@SPj>BNY3gxkkcPgNi9bE=?&3A4`3ekrdsb zn~`M;p8I>4?@@ZI{9Afv(tC@pp@Oe5BYUw-%&J_WaTBGls)&d8q?t$i<<@=_CNfH! z4H!ww7#gkp_^`bxZaJI9@C+A9x7@E1ZRoG5PL?w3GDi>`8Qq%I+0ygfT78%{Zt#mP zqX0CzaHKn@hAOQsv=^8UbfpuyFnT8Ht++Vmmx$~09!e{5t8fMkEjr~tfIxMlIpr4zGwvEIWKC2`Q#C)c7QF9wet?hE zLKoU?t@nqm=iBc` z8_((*(i(g}7z)3{%SJ!uya{?Ir-2^Fiap*VC4pF@N zpL5F*DG+(taLhdu4DbyAP(0&60n@%?G~hHugBI^-X6@_YOu}8UqwbQ8V`2vwDRLMz z)aRFo+r1f?5idT9xRF`cjgx$a-IpH3AH|bs$emw}d23*3aU0hYNh4(D0o-Z+wIX{d zeann?lzjgsAt62`er@<$`G755?i7tl%CHNgXp}#j>j&S1n5wZ;ofNbI>B2*4L1}@3 zq(LzPqn()w{KBsX!5*a&=dv<}t=R%II;TcQatbnKM7S4Q1PQIoT=^$#=>Y(m{mBYtl5W z6}|l4kxikOcJ`C3o{TSxIi?8|N6sH7Lkhq5qttl@uBTA|-cBluU$hU0&xYKvNidrL z4q>|j76}G1Db23Fa|XlFm%W&jW0h#7B$_FD-ZhqJ5#7i!0ZmCrereX z|Jlf`<1zR2akFe|boWv-r=}kM03o|%$mZA7Of2T99u~e56~6sh$P=yk9f!H6msn)n zvFOLF?W?iqi6fK9C)a42Sgt0kz4#M6 z-UY6451Er~=V;ITs1O-q*>}{;bs74MMZ(Z&=Z{5#q+i@cw^vI#0|Dh~-Dh-tn2I(S zTXXp-bLEG{p0#BbIqIcTM|DWZmr`&br8u)jQ`CR*^+g_fIX%=K+)x}F%Oak-Uh$6nIHUavnNV5M7YffU80QPRD%y>T{bIzn<6Rsy zb6cW6`?0EwSn;uJddPn@`?^Cry2s(6ccP1ykKr!kmDg2~zbTJq@+e(z5N>ZNr|8$j zPi-~ofp7E|Xx1#H+f@UR@AS}iLP!}}dRwf{u!avAq-_hNw#uaoOD{2jo*eRn8$~bDK`h1&ssOC6ekGV38+hU!KR z+kpnSzT;y#o|V2h|F?SY4-z1MFxz0;)@Lk`H>Cj zSl@fR%*@F79;HJcsX%L8_d!%TwmQyi$|n&C{oBMJ9~Xm!@@#lZdz(WB9SgJ#NIC%@ zy+~ZnI|4E`7f@W0Y9I@N7UTs1fTPD-ZiU%Lr2MnP+2h8AGh?(WGVf>h@W-_M>jRkD z(KNxvo(UJ7)o+*t%fCcM10;2XM$1NAFKwhp(c917^io_ynn-yv58IFIF*UJUw*2Ma zm?a-a1yp9B?WxpLzap-c^$HKkX_IfT_W8Lqaltl*A%vZSZWAe`Kv}vjz}>Tc;Hw9T zA+Nc49X&{WDmxY~ReV0YceXdL!$9mTL$Q@_vXIW6I{G=`$KR7jFcE&IsHwnKX;KldV#YL z(xwKAB5cFiz+r6m*5iJvo&E)XQqVWjmA}BfyVS&dm9&Y%$Sp^sW!JE3iI0v(kQHdo zmhWk|gC!e@CFKPv4BE*U;mYo0y}J0J-Fhu!c%v+paQf9+3Ed2EkfPt(D7|Ok#t)^PGr3Y)RGfvO=k;@Xry=Cf3fLCQ# zi`%oCt+vyB-t{iEgI&+2dczmnMXj>EOmSpMuuL8Ob`1$D;fc$wM6j2HH4Q$ zqaoj&M$2sLhpptdJMbs!krJId=iOd}HdP4Lt@yf42OZ{pOoQ4_gShz_sMoWYX}yQd zDQ8(tc7UvTt%`0#?9K!C^J>GpucEnBhnsWg102Z=uzOlwez^q^j7nV$krID#wC}A$ zcRfc2)T5Y~({6@1`{yL-Lzs;miT@C9|1SIFBMK7cz*E;v2H|EStZphjfb5mGMpw{q z!pl;Vw772tuvDH4o$;j4u8)@=m+&BIf4Ix(u75P?Q{4Y8^uvpq)mCW(enuQc)hx$B zOY{`_*%~bm%k*x6y;)D8_-yYbMsC8y#1H}89X;M=a#*HT>d*NFf}x$pQ&X?nFtvzA zKH|l8y;frsm|&}<%&*}Yu}Yn0M=Jy8qe%<1qXRR%Nut}Aqr+1pQS*D7Cp`+8Y`RO02p14DyVOmSYlEzZ;9&JzYhtybMZ%e4s zlks=V(+aJ!LK-()3ox`%9c)lx#3#y4{ulL6KpG|&>9`n?Uh#m3G-mZy-3h98Scyja zH^3Pb7?P z+2hAkyvg}g$#)n$Gs2fL19JNOZ|~>Nx(|}lmwesC!>?Y~72mpf4XZ8t^TIwbCk;i0 z+a2ymSZ^=OrtrSH!(y#Vn!8KWk#O7<1-!if+`dDDy18U7wS3k$lIeM}Z0fhYqI)+x zo*o4*S$S|hGf6vL>PaQ(OQ_%eskx-G-FV|dXHbTH<#w@RbeIx9I$d$xqHh`{*&d3y zevlYNk)}w@cuu4A$^DYJsOvO7VBaom@Rx@gb$V5IKJ{Xue16H-1H0j=U0brW-aVRG znWCQRkESBmD^4?a7mB@!jf2>(Hs=Bd-;XX1oEilevb9axB^NhIPLO>jl03S+Rw|fx z&oIsIk(~W!4$zzKF|uSR<@S#;{r;fKup)iDaxz_9JouroY>XHcrN(Mm@UHV?-8bCh zXGfY~7U`rCasv(h-R*ava)^ zF1`BMT*n3xQBTdM?`n&h2Ecf*XXuLo7Zyl_El(v~oh>}mK01$%0a@#uzyiX_g>Bav2XWwH%YekAxU%pBT!p*?%cS#zA zv;^eDC#KZP@7o=^GDc_V8<3w>`*L(+=A#(fcH)dGjqM}Vk_el+c>B`{9xm<>IZ-Zm zLL!-Yf*3nju_(8ZGUd9*K`iofWW+BYFnZF&+a|=yxqV?oUOcG#ulnSR$DMs|e5Tph%WW zVjzE3nMh7+rG!}av)+~;o$#+EHyPX zzOUO?^#)Jh*t^b7pTW+I%f;xy&JMPCO&5RR``BmHX-Mw{qoJp9BjKea$;A9%>-iEZ zvuUBm%0j5UWax~`ue!K6dDdip+zs3f{+qQKqH;9C(1Z@95()-Ew=`BdLh2VS3zI8qYGH&&7m9+vpUc+x8l!i-ATXKhw34XL2;ya_VIQz!OL^)8mtqnb?q=~&^h-$;Zn^HRZ2p(gH z39An;`AWT=i&VP0u&CUe7OYW51Icv=q%Vc7%Zm z_uAp9n}osEUdk2*pV)*i`WRSa-FWtCwGqS-75@K#V0)r;+0(0XVp9vnb7lWiMj!q= z>Zf(ioa@gSwA55Jil$lh)%4U<)$j@HTQU2KwuUUsZA*2O^QTKobak8g0Qb~ROMTW7 zfTF2yF*na6i(lQ*Nq^rPen^0>$$b`K!Kp{FVa-VF`kCiXZg0Vtr}i*rcpny_YOR!} z+?Jiv?dWlT`}o$s9Fxt%%684d7ek-q-Q~jS*I5+8HtvSw+Rp!D=+gVr!gqcYy9K74 z&eClx6f6{1Din;ynjz?XZlJ~W7^A@0wiHIt8$aou;f>MYpU%gUlDwAK*nX0#vHtyl z_C=B+ZkOffY|oR^2>(+IlZCTMFirZMhn>bqzR=38hvJpcM4-@gUYY7_k^G*FW9;5r zc9q4c>C?hd{uS3{MThN*(w!3e05e?bI#SNlo$U&%>((Dz0_JeqbG|}!wI$& z%q2JQ)Vas;i0RYqNXW!CC~QK%u$K$beGI zT2KuzMjus26(zmofK;m2gY%d*o~sHBKA#`RBNc9c*-GLmbgh?*9V;^TBSot2E%~Q5 zl+R!WA_h_JT;+irbJ#Z-tSy-;B^t&&dOSwPV(T!CB)no8Y4sP%k(MD^0P!NL1vK&7 z`3luW2$gkI#Zf>IZT2=m4R&e@d zeo#B=Q|9`w8}%|)f%GBjYO01&Dk5qjm$+#1yia#CE=Sh~88Vdp%|VU}0a6mF@JkhUY&~W3f#rHK-1Qdo z>0*z5?#-hQUY}k^X7~1bkI?($-~3#c3mF4Cl@2%|0@1=ARZ z^qlNaN63&>;O_~mmto}?tAhznb}p;GpyIq1Z^yf<_6Ui~cpbbP;uV7W!+ke>wYG-f zPPz2~%UgSs(>vsKFle%uo=WIDYz;BR!doAy)aQ0QCpE_Wz1XK+3Kpr=V_H8w zqzaizn9ALx#?fo-N)_CtENYH*1|ID|x=xa9d#;9~1Wgrcx^8=evrfky*Xj`269~A;kh^O|ewZnM}=SmM7NX=?h#jjLh&1kIT+A z)If4luYo@s+e_L&eRJ$gw1`)>u#efOq=M0iYIPS$GII0z`T56eNxK@~Y%*^~Q&w$1b)jM9Z~kuRc~YX`6r#ySCskW5cq|#a39s;ZiaL~OdEpgu z1k*sKkLZ&?6fAi=)77yKI1xii%)@DG8r}663xkJcwLTj?s`h{GP@_2}`A|;w7zrzk4QOQ*O$(e|M^<`vLD*1^i>Nr*= z+A`y@f{!zLi)ys9OrFM5`Qw0292Ciyq>zC>8(TkG1O;#UUh?#I08kuwpS_vhufJ0v&p^Yr`=^WG7!qVG(8n9u7=J64fr zQq7B|9rzl7s)I_|8UeVp?=cqGILQ}0O(n+^vJz=vFBU9JmG$=DWzi+qCHw@D0a7`M zA`%pmU8+8W{u0{2*^tg&3;I&i`4`{YJe_n8 z{viTJZL?$}#l9w${3mydrW>Z%nY!WXf$HJv5$Zw4F%7^mXWsZ-s&olv31;C*KlH)j z?j?Eika^cI`l>)WJ*ga?%>0HwJm{%<)OP8pdvwMG@fm;Ca`jfy7ixY-sic42*f&ld zJg3(O0~;=Zsp@cdUj@&Zj~#~LX=F5Ws@!Ik0-~(wlbJO6&)S~s6WrAW9lrQ%6+S03 z&P&xJ{;BC%2s%J#uxZy3=Fc}fkwE9(T}QAK9b{FT!L3^PQ~;#X$T|9v&JFq)ru$h|ls zvPxYyWT}V&Dol3#)t6pVE4nIClEq=r++eGcG-tkOW4{n$Ra~3z?`@_gXRUiR`SrhY4K z#>C+t>pNtm>!Zw*;p^qI0|g<)Ob`r0jaN6asw2ZGLT}bMbHnQ$OH8cR7{Rq?=4%&x z2Qe&O`w$~b%fuo>fkgT`PVx=uto@&SdDpIXL)<da|A*x(b?o zdUj^iN+B9%;2{1URo7=%m@r*RJi3fQNO_`AZY;b#tClm;A}NQF#!Y;pMMdh=^fO@9 z>J>Xv^joKJM>M7x=xh!oSLO3JlxVwTn$DPHdGsnkAvB)9d)IE6ZHgd1vd+Z;W1d682CBy4zti z&6;T6!rzSKIy&zKKfAx9J%7q-=Mac{u-_GIYEaZt*`h25Ne?ch`E_c2{pGA<;nVkx z102u6#||N$g5MhA{!rFwaI(;8$S{1DePGc^L~j6?Q$2QMIO09 zPdma#_kX(|;oOau(pX877ac9V4O8x3g{Mdbr6oS)7 zN0v#H_j!bhUNl;q>GrkeA~){;lCg@&Mg5(z%E1HV`d7{>_}@9JZ(VJn>=HKC4q{My zLpw8D2OD@&E}T?=SV7rE-XI?4H+E(aOI8sZOC$NW=!leE6MG6ycn2;fB4XpB!^#Z= zQ?P=-+!R0#4h{+c2LPbUF6{uZG&6i-ZDI+f;6P`8V{ZtxcA((p;6i6ds6r4x005m` z6k;m{H8U}FK+J;+syaZe)G2u2J;eI(G+`)^0+C~@0#BIzJLi_?-}e8NR15?I|34|k zx>2LneiYApj|7nW4k1sp9h-vz^G);Jq7ONB*clw!(IJ2QT3sYWS)>yb_Ual2Um3r5 zw706UJD48HLY73$&Gm=sl|EYND&Uk>VT!eN_p49f6HS<{TU>u{4&#WYh1dwy^E8il ziH`_=$2m8k)y$Q2yDZQluP+AZbND!Yi7Co@fwHnw2pV1bo*=wGx2n7Urt$y1@imz1&#&nK47Nw zT-dLY@^1NHY?5B#-Qf9?`lA_={@NnLpmwJGQG7&oU}0>) ziZ`GdjY(jIKi2Q?e+d=de}nq3pkP;ZG;lyf$Xh!{=x?qF#2$)p%>NM^W_I=tqNWf# zgv;e1fAtY=)-W@2FtyhKb8%3Bfj|mw00#vR4=)857d&XdU z(4fLD4>dA_AWjHkeJ)-u3LZ|NF1w_ijiW6*A6^xXD#Y5}7O{k(E4!#F{9rhl8A4Sg zMcAb&9N>rx39*a9v4(4~r$8jq|MLt0{*hTPYU2nu0sub&aQG~$!9>qU@%LGVw1{ZAdD5crj3WAdl2KV62-uIT7sX=aUZ*>8aV1F3(c z_P=p-FtxG!8!9*^U<3>RcoByeFaipAK|lhB5)AqaI)n^@hmeEwxOw0OKK@%C0pZ{C z5o^F{FbEE(DEt!$_$B<8DlYiaV7ME855ql#Py+_S#o(c8`L;d6lqRR~$cn(zq-4};(pf)4`xt=`PWS`7YO27?$MdgtpDP{`vCa4 z{2x3Z5bm@8-~oUj5Zv+q!Gl}N`CoDX0N4M*gTIpgb1nb?;)Y)s|FIqb0Ot6gw!m#h zTnhg~j+YZ2)c?r?0yzIm4hZ1=FTFrc;D6}=a`OJeW(PY6{AFi{I1;L6ZcsR+>?$@k z@FNVDLEL!K*2XpzfZwk|I3Y%%Lm?mm76XGtKw?0k2(JV$kO#;s#>p!o!6gRf5#f;l j@(7{-|3%=32kuUL2Z)`+Z(jm{U>-0!Ev>ks1p5C2Hj`#V literal 0 HcmV?d00001 diff --git a/components/cldr-json-data-provider/tests/testdata/dummy.zip b/components/cldr-json-data-provider/tests/testdata/dummy.zip new file mode 100644 index 0000000000000000000000000000000000000000..25c4f53e540d10739b12c79ff1631fa4249e5102 GIT binary patch literal 12480 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 DdZvrF literal 0 HcmV?d00001 diff --git a/components/fs-data-provider/Cargo.toml b/components/fs-data-provider/Cargo.toml index 3a15a380534..8a5953c721c 100644 --- a/components/fs-data-provider/Cargo.toml +++ b/components/fs-data-provider/Cargo.toml @@ -22,14 +22,16 @@ serde_json = "1.0" # Dependencies for the export module erased-serde = { version = "0.3", optional = true } +log = { version = "0.4", optional = true } # Dependencies for the binary clap = { version = "2.33", optional = true } -icu-cldr-json-data-provider = { path = "../cldr-json-data-provider", optional = true } +icu-cldr-json-data-provider = { path = "../cldr-json-data-provider", features = ["download"], optional = true } +simple_logger = { version = "1.10", optional = true } [features] -export = ["erased-serde", "icu-data-provider/invariant"] -export-bin = ["export", "clap", "icu-cldr-json-data-provider"] +export = ["erased-serde", "icu-data-provider/invariant", "log"] +export-bin = ["export", "clap", "icu-cldr-json-data-provider", "simple_logger"] [[bin]] name = "icu4x-cldr-export" diff --git a/components/fs-data-provider/src/bin/icu4x-cldr-export.rs b/components/fs-data-provider/src/bin/icu4x-cldr-export.rs index 7f5c3ae8b4f..c49c8d39d63 100644 --- a/components/fs-data-provider/src/bin/icu4x-cldr-export.rs +++ b/components/fs-data-provider/src/bin/icu4x-cldr-export.rs @@ -1,6 +1,8 @@ use clap::{App, Arg, ArgGroup}; +use icu_cldr_json_data_provider::download::CldrPathsDownload; use icu_cldr_json_data_provider::CldrJsonDataProvider; use icu_cldr_json_data_provider::CldrPaths; +use icu_cldr_json_data_provider::CldrPathsLocal; use icu_data_provider::icu_data_key; use icu_data_provider::iter::IterableDataProvider; use icu_fs_data_provider::export::fs_exporter; @@ -10,8 +12,8 @@ use icu_fs_data_provider::manifest; use std::ffi::OsStr; use std::fmt; use std::path::PathBuf; +use simple_logger::SimpleLogger; -// #[derive(Debug)] enum Error { Unsupported(&'static str), Export(icu_fs_data_provider::FsDataError), @@ -55,7 +57,8 @@ fn main() -> Result<(), Error> { Arg::with_name("VERBOSE") .short("v") .long("verbose") - .help("Enable verbose logging."), + .multiple(true) + .help("Sets the level of verbosity (-v or -vv)"), ) .arg( Arg::with_name("DRY_RUN") @@ -78,19 +81,27 @@ fn main() -> Result<(), Error> { .help("Delete the output directory before writing data."), ) .arg( - Arg::with_name("STYLE") - .long("style") - .takes_value(true) - .possible_value("compact") - .possible_value("pretty") - .help("JSON style when printing files."), + Arg::with_name("PRETTY") + .short("p") + .long("pretty") + .help("Whether to pretty-print the output JSON files."), + ) + .arg( + Arg::with_name("CLDR_TAG") + .long("cldr-tag") + .value_name("TAG") + .help( + "Download CLDR JSON data from this GitHub tag: \n\ + https://github.com/unicode-cldr/cldr-core/tags", + ) + .takes_value(true), ) .arg( Arg::with_name("CLDR_CORE") .long("cldr-core") .value_name("PATH") .help( - "Path to cldr-core JSON: \ + "Path to cldr-core. Ignored if '--cldr-tag' is present. \n\ https://github.com/unicode-cldr/cldr-core", ) .takes_value(true), @@ -100,8 +111,8 @@ fn main() -> Result<(), Error> { .long("cldr-dates") .value_name("PATH") .help( - "Path to cldr-dates JSON: \ - https://github.com/unicode-cldr/cldr-dates", + "Path to cldr-dates. Ignored if '--cldr-tag' is present. \n\ + https://github.com/unicode-cldr/cldr-dates-modern", ) .takes_value(true), ) @@ -149,6 +160,13 @@ fn main() -> Result<(), Error> { ) .get_matches(); + match matches.occurrences_of("VERBOSE") { + 0 => SimpleLogger::from_env().init().unwrap(), + 1 => SimpleLogger::new().with_level(log::LevelFilter::Info).init().unwrap(), + 2 => SimpleLogger::new().with_level(log::LevelFilter::Trace).init().unwrap(), + _ => return Err(Error::Unsupported("Only -v and -vv are supported")) + } + if !matches.is_present("ALL_KEYS") { return Err(Error::Unsupported( "Lists of keys are not yet supported (see #192)", @@ -172,25 +190,24 @@ fn main() -> Result<(), Error> { .unwrap_or_else(|| OsStr::new("/tmp/icu4x_json")), ); - let mut cldr_paths = CldrPaths::default(); - - if let Some(path) = matches.value_of("CLDR_CORE") { - cldr_paths.cldr_core = Ok(path.into()); - } - - if let Some(path) = matches.value_of("CLDR_DATES") { - cldr_paths.cldr_dates = Ok(path.into()); - } + let cldr_paths: Box = if let Some(tag) = matches.value_of("CLDR_TAG") { + Box::new(CldrPathsDownload::from_github_tag(tag)) + } else { + let mut cldr_paths_local = CldrPathsLocal::default(); + if let Some(path) = matches.value_of("CLDR_CORE") { + cldr_paths_local.cldr_core = Ok(path.into()); + } + if let Some(path) = matches.value_of("CLDR_DATES") { + cldr_paths_local.cldr_dates = Ok(path.into()); + } + Box::new(cldr_paths_local) + }; - let provider = CldrJsonDataProvider::new(&cldr_paths); + let provider = CldrJsonDataProvider::new(cldr_paths.as_ref()); let mut options = serializers::JsonSerializerOptions::default(); - if let Some(value) = matches.value_of("STYLE") { - options.style = match value { - "compact" => serializers::StyleOption::Compact, - "pretty" => serializers::StyleOption::Pretty, - _ => unreachable!(), - }; + if matches.is_present("PRETTY") { + options.style = serializers::StyleOption::Pretty; } let json_serializer = Box::new(serializers::JsonSerializer::new(&options)); @@ -206,7 +223,6 @@ fn main() -> Result<(), Error> { if matches.is_present("OVERWRITE") { options.overwrite = fs_exporter::OverwriteOption::RemoveAndReplace } - options.verbose = matches.is_present("VERBOSE"); let mut exporter = FilesystemExporter::try_new(json_serializer, &options)?; for key in keys.iter() { diff --git a/components/fs-data-provider/src/export/fs_exporter.rs b/components/fs-data-provider/src/export/fs_exporter.rs index 77bcecff25a..dda6734603d 100644 --- a/components/fs-data-provider/src/export/fs_exporter.rs +++ b/components/fs-data-provider/src/export/fs_exporter.rs @@ -33,8 +33,6 @@ pub struct ExporterOptions { pub aliasing: AliasOption, /// Option for initializing the output directory. pub overwrite: OverwriteOption, - /// Whether to print progress to stdout. - pub verbose: bool, } impl Default for ExporterOptions { @@ -43,7 +41,6 @@ impl Default for ExporterOptions { root: PathBuf::from("icu4x_data"), aliasing: AliasOption::NoAliases, overwrite: OverwriteOption::CheckEmpty, - verbose: false, } } } @@ -54,7 +51,6 @@ pub struct FilesystemExporter { root: PathBuf, manifest: Manifest, alias_collection: Option>>, - verbose: bool, serializer: Box, } @@ -75,9 +71,7 @@ impl DataExporter for FilesystemExporter { let mut path_buf = self.root.clone(); path_buf.extend(req.data_key.get_components().iter()); path_buf.extend(req.data_entry.get_components().iter()); - if self.verbose { - println!("Initializing: {}", path_buf.to_string_lossy()); - } + log::trace!("Initializing: {}", path_buf.to_string_lossy()); self.write_to_path(path_buf, obj) } } @@ -94,7 +88,6 @@ impl FilesystemExporter { syntax: SyntaxOption::Json, }, alias_collection: None, - verbose: options.verbose, serializer, }; From cf6ae610ab077a7384dd51a96a1b9d364ac52f82 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 5 Oct 2020 05:18:50 -0500 Subject: [PATCH 2/4] Adding locales option to FsDataProvider exporter --- components/data-provider/src/iter.rs | 10 +++- .../tests/fixtures/data/icu4x/manifest.json | 1 + components/fs-data-provider/Cargo.toml | 2 +- .../src/bin/icu4x-cldr-export.rs | 51 +++++++++++++++---- .../src/export/fs_exporter.rs | 26 +++++++--- components/fs-data-provider/src/export/mod.rs | 2 +- components/fs-data-provider/src/manifest.rs | 14 +++++ .../testdata/json_plurals_37/manifest.json | 1 + .../tests/data/json_plurals_37/manifest.json | 1 + 9 files changed, 89 insertions(+), 19 deletions(-) diff --git a/components/data-provider/src/iter.rs b/components/data-provider/src/iter.rs index fbffa858e4b..dbcd01071ce 100644 --- a/components/data-provider/src/iter.rs +++ b/components/data-provider/src/iter.rs @@ -19,11 +19,16 @@ pub trait IterableDataProvider<'d>: DataProvider<'d> + DataEntryCollection { /// Trait for objects capable of persisting serialized data hunks. pub trait DataExporter { + /// Save `obj` corresponding to `req`. fn put( &mut self, req: &DataRequest, obj: &dyn erased_serde::Serialize, ) -> Result<(), Box>; + + /// Whether to load and dump data for the given entry. This function enables the + /// DataExporter to filter out certain data entries. + fn includes(&self, data_entry: &DataEntry) -> bool; } impl<'d, T> IterableDataProvider<'d> for T @@ -32,9 +37,12 @@ where { fn export_key(&self, data_key: &DataKey, sink: &mut dyn DataExporter) -> Result<(), Error> { for data_entry in self.iter_for_key(data_key)? { + if !sink.includes(&data_entry) { + continue; + } let req = DataRequest { data_key: *data_key, - data_entry: data_entry.clone(), + data_entry, }; let response = self.load(&req)?; let payload = response.borrow_as_serialize(); diff --git a/components/datetime/tests/fixtures/data/icu4x/manifest.json b/components/datetime/tests/fixtures/data/icu4x/manifest.json index a68a792a8e3..c0f2b714d99 100644 --- a/components/datetime/tests/fixtures/data/icu4x/manifest.json +++ b/components/datetime/tests/fixtures/data/icu4x/manifest.json @@ -1,4 +1,5 @@ { "aliasing": "NoAliases", + "locales": "IncludeAll", "syntax": "Json" } diff --git a/components/fs-data-provider/Cargo.toml b/components/fs-data-provider/Cargo.toml index 8a5953c721c..b1957dcb62b 100644 --- a/components/fs-data-provider/Cargo.toml +++ b/components/fs-data-provider/Cargo.toml @@ -16,7 +16,7 @@ include = [ [dependencies] icu-data-provider = { path = "../data-provider" } -icu-locale = { path = "../locale" } +icu-locale = { path = "../locale", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/components/fs-data-provider/src/bin/icu4x-cldr-export.rs b/components/fs-data-provider/src/bin/icu4x-cldr-export.rs index c49c8d39d63..9f935da3cfa 100644 --- a/components/fs-data-provider/src/bin/icu4x-cldr-export.rs +++ b/components/fs-data-provider/src/bin/icu4x-cldr-export.rs @@ -1,3 +1,4 @@ +use crate::manifest::LocalesOption; use clap::{App, Arg, ArgGroup}; use icu_cldr_json_data_provider::download::CldrPathsDownload; use icu_cldr_json_data_provider::CldrJsonDataProvider; @@ -9,15 +10,18 @@ use icu_fs_data_provider::export::fs_exporter; use icu_fs_data_provider::export::serializers; use icu_fs_data_provider::export::FilesystemExporter; use icu_fs_data_provider::manifest; +use icu_locale::LanguageIdentifier; +use simple_logger::SimpleLogger; use std::ffi::OsStr; use std::fmt; use std::path::PathBuf; -use simple_logger::SimpleLogger; +use std::str::FromStr; enum Error { Unsupported(&'static str), Export(icu_fs_data_provider::FsDataError), DataProvider(icu_data_provider::DataError), + LocaleParser(icu_locale::ParserError, String), } impl fmt::Display for Error { @@ -26,6 +30,7 @@ impl fmt::Display for Error { Error::Unsupported(message) => write!(f, "Unsupported: {}", message), Error::Export(error) => write!(f, "{}", error), Error::DataProvider(error) => write!(f, "{}", error), + Error::LocaleParser(error, s) => write!(f, "{}: {}", error, s), } } } @@ -117,12 +122,15 @@ fn main() -> Result<(), Error> { .takes_value(true), ) .arg( - Arg::with_name("KEY") + Arg::with_name("KEYS") .short("k") .long("keys") .multiple(true) .takes_value(true) - .help("Include this data key in the output. Also see --key-file."), + .help( + "Include this data key in the output. Accepts multiple arguments. \ + Also see --key-file.", + ), ) .arg( Arg::with_name("KEY_FILE") @@ -141,12 +149,23 @@ fn main() -> Result<(), Error> { .help("Include all keys known to ICU4X."), ) .group( - ArgGroup::with_name("KEYS") - .arg("KEY") + ArgGroup::with_name("KEY_MODE") + .arg("KEYS") .arg("KEY_FILE") .arg("ALL_KEYS") .required(true), ) + .arg( + Arg::with_name("LOCALES") + .short("l") + .long("locales") + .multiple(true) + .takes_value(true) + .help( + "Include this locale in the output. Accepts multiple arguments. \ + Omit this option to include all locales.", + ), + ) .arg( Arg::with_name("OUTPUT") .short("o") @@ -162,9 +181,15 @@ fn main() -> Result<(), Error> { match matches.occurrences_of("VERBOSE") { 0 => SimpleLogger::from_env().init().unwrap(), - 1 => SimpleLogger::new().with_level(log::LevelFilter::Info).init().unwrap(), - 2 => SimpleLogger::new().with_level(log::LevelFilter::Trace).init().unwrap(), - _ => return Err(Error::Unsupported("Only -v and -vv are supported")) + 1 => SimpleLogger::new() + .with_level(log::LevelFilter::Info) + .init() + .unwrap(), + 2 => SimpleLogger::new() + .with_level(log::LevelFilter::Trace) + .init() + .unwrap(), + _ => return Err(Error::Unsupported("Only -v and -vv are supported")), } if !matches.is_present("ALL_KEYS") { @@ -223,7 +248,15 @@ fn main() -> Result<(), Error> { if matches.is_present("OVERWRITE") { options.overwrite = fs_exporter::OverwriteOption::RemoveAndReplace } - let mut exporter = FilesystemExporter::try_new(json_serializer, &options)?; + if let Some(locale_strs) = matches.values_of("LOCALES") { + let locales_vec = locale_strs + .map(|s| { + LanguageIdentifier::from_str(s).map_err(|e| Error::LocaleParser(e, s.to_string())) + }) + .collect::, Error>>()?; + options.locales = LocalesOption::IncludeList(locales_vec.into_boxed_slice()); + } + let mut exporter = FilesystemExporter::try_new(json_serializer, options)?; for key in keys.iter() { let result = provider.export_key(key, &mut exporter); diff --git a/components/fs-data-provider/src/export/fs_exporter.rs b/components/fs-data-provider/src/export/fs_exporter.rs index dda6734603d..f49d0a815e0 100644 --- a/components/fs-data-provider/src/export/fs_exporter.rs +++ b/components/fs-data-provider/src/export/fs_exporter.rs @@ -2,6 +2,7 @@ use super::aliasing::{self, AliasCollection}; use super::serializers::Serializer; use crate::error::Error; use crate::manifest::AliasOption; +use crate::manifest::LocalesOption; use crate::manifest::Manifest; use crate::manifest::SyntaxOption; use crate::manifest::MANIFEST_FILE; @@ -29,6 +30,8 @@ pub enum OverwriteOption { pub struct ExporterOptions { /// Directory in the filesystem to write output. pub root: PathBuf, + /// Strategy for including locales. + pub locales: LocalesOption, /// Strategy for de-duplicating locale data. pub aliasing: AliasOption, /// Option for initializing the output directory. @@ -39,6 +42,7 @@ impl Default for ExporterOptions { fn default() -> Self { Self { root: PathBuf::from("icu4x_data"), + locales: LocalesOption::IncludeAll, aliasing: AliasOption::NoAliases, overwrite: OverwriteOption::CheckEmpty, } @@ -74,17 +78,25 @@ impl DataExporter for FilesystemExporter { log::trace!("Initializing: {}", path_buf.to_string_lossy()); self.write_to_path(path_buf, obj) } + + fn includes(&self, data_entry: &DataEntry) -> bool { + match self.manifest.locales { + LocalesOption::IncludeAll => true, + LocalesOption::IncludeList(ref list) => list.contains(&data_entry.langid), + } + } } impl FilesystemExporter { pub fn try_new( serializer: Box, - options: &ExporterOptions, + options: ExporterOptions, ) -> Result { let result = FilesystemExporter { - root: options.root.to_path_buf(), + root: options.root, manifest: Manifest { aliasing: options.aliasing, + locales: options.locales, syntax: SyntaxOption::Json, }, alias_collection: None, @@ -93,17 +105,17 @@ impl FilesystemExporter { match options.overwrite { OverwriteOption::CheckEmpty => { - if options.root.exists() { - fs::remove_dir(&options.root)?; + if result.root.exists() { + fs::remove_dir(&result.root)?; } } OverwriteOption::RemoveAndReplace => { - if options.root.exists() { - fs::remove_dir_all(&options.root)?; + if result.root.exists() { + fs::remove_dir_all(&result.root)?; } } }; - fs::create_dir_all(&options.root)?; + fs::create_dir_all(&result.root)?; let mut manifest_path = result.root.to_path_buf(); manifest_path.push(MANIFEST_FILE); diff --git a/components/fs-data-provider/src/export/mod.rs b/components/fs-data-provider/src/export/mod.rs index c521d48fbb5..5f64d6ef449 100644 --- a/components/fs-data-provider/src/export/mod.rs +++ b/components/fs-data-provider/src/export/mod.rs @@ -23,7 +23,7 @@ //! let json_serializer = Box::new(serializers::JsonSerializer::new(&options)); //! let mut options = fs_exporter::ExporterOptions::default(); //! options.root = DEMO_PATH.clone(); -//! let mut exporter = fs_exporter::FilesystemExporter::try_new(json_serializer, &options) +//! let mut exporter = fs_exporter::FilesystemExporter::try_new(json_serializer, options) //! .expect("Should successfully initialize data output directory"); //! //! // Export a key diff --git a/components/fs-data-provider/src/manifest.rs b/components/fs-data-provider/src/manifest.rs index 3d8fb571073..8cf954ed59c 100644 --- a/components/fs-data-provider/src/manifest.rs +++ b/components/fs-data-provider/src/manifest.rs @@ -1,3 +1,4 @@ +use icu_locale::LanguageIdentifier; use serde::{Deserialize, Serialize}; /// File name of the manifest. The manifest always uses JSON, even if the serializer isn't JSON. @@ -13,6 +14,15 @@ pub enum AliasOption { // TODO: Alias based on a field in the JSON file } +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum LocalesOption { + /// Include all available locales. + IncludeAll, + /// Include only those locales that have an exact match in the given list. + IncludeList(Box<[LanguageIdentifier]>), +} + #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum SyntaxOption { @@ -36,6 +46,10 @@ impl SyntaxOption { #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub(crate) struct Manifest { + /// Strategy for de-duplicating locale data. pub aliasing: AliasOption, + /// Configuration for including locales in this data provider. + pub locales: LocalesOption, + /// Which data serialization file format is used. pub syntax: SyntaxOption, } diff --git a/components/fs-data-provider/tests/testdata/json_plurals_37/manifest.json b/components/fs-data-provider/tests/testdata/json_plurals_37/manifest.json index a68a792a8e3..c0f2b714d99 100644 --- a/components/fs-data-provider/tests/testdata/json_plurals_37/manifest.json +++ b/components/fs-data-provider/tests/testdata/json_plurals_37/manifest.json @@ -1,4 +1,5 @@ { "aliasing": "NoAliases", + "locales": "IncludeAll", "syntax": "Json" } diff --git a/components/pluralrules/tests/data/json_plurals_37/manifest.json b/components/pluralrules/tests/data/json_plurals_37/manifest.json index a68a792a8e3..c0f2b714d99 100644 --- a/components/pluralrules/tests/data/json_plurals_37/manifest.json +++ b/components/pluralrules/tests/data/json_plurals_37/manifest.json @@ -1,4 +1,5 @@ { "aliasing": "NoAliases", + "locales": "IncludeAll", "syntax": "Json" } From 6737ac826348ea2ebd356336e987f6752edf77c5 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 5 Oct 2020 16:29:38 -0500 Subject: [PATCH 3/4] Making from_github_tag fallible. --- .../src/download/cldr_paths_download.rs | 17 +++++++---- .../src/download/error.rs | 30 ++++++++++--------- .../src/download/io_util.rs | 19 +++++++----- .../src/download/mod.rs | 3 +- .../cldr-json-data-provider/src/error.rs | 2 +- .../src/bin/icu4x-cldr-export.rs | 10 ++++++- 6 files changed, 51 insertions(+), 30 deletions(-) diff --git a/components/cldr-json-data-provider/src/download/cldr_paths_download.rs b/components/cldr-json-data-provider/src/download/cldr_paths_download.rs index 2b95d65f0e2..7cdd552f53a 100644 --- a/components/cldr-json-data-provider/src/download/cldr_paths_download.rs +++ b/components/cldr-json-data-provider/src/download/cldr_paths_download.rs @@ -1,9 +1,12 @@ +use super::error::DownloadError; use super::io_util; use crate::error::Error; use crate::CldrPaths; use std::path::PathBuf; /// Implementation of CldrPaths that downloads CLDR data directories on demand. +/// The download artifacts are saved in the user's cache directory; see +/// https://docs.rs/dirs/3.0.0/dirs/fn.cache_dir.html /// /// # Example /// @@ -12,7 +15,8 @@ use std::path::PathBuf; /// use icu_cldr_json_data_provider::CldrJsonDataProvider; /// use std::path::PathBuf; /// -/// let paths = CldrPathsDownload::from_github_tag("36.0.0"); +/// let paths = CldrPathsDownload::try_from_github_tag("36.0.0") +/// .expect("Cache directory not found"); /// /// let data_provider = CldrJsonDataProvider::new(&paths); /// @@ -62,9 +66,12 @@ impl CldrPathsDownload { /// /// github_tag should be a tag in the CLDR JSON repositories, such as "36.0.0": /// https://github.com/unicode-cldr/cldr-core/tags - pub fn from_github_tag(github_tag: &str) -> Self { - Self { - cache_dir: dirs::cache_dir().unwrap().join("icu4x").join("cldr"), + pub fn try_from_github_tag(github_tag: &str) -> Result { + Ok(Self { + cache_dir: dirs::cache_dir() + .ok_or(DownloadError::NoCacheDir)? + .join("icu4x") + .join("cldr"), cldr_core: CldrZipFileInfo { url: format!( "https://github.com/unicode-cldr/cldr-core/archive/{}.zip", @@ -79,7 +86,7 @@ impl CldrPathsDownload { ), top_dir: format!("cldr-dates-modern-{}", github_tag), }, - } + }) } } diff --git a/components/cldr-json-data-provider/src/download/error.rs b/components/cldr-json-data-provider/src/download/error.rs index 5b1950bd8bd..3a3bd60e7fe 100644 --- a/components/cldr-json-data-provider/src/download/error.rs +++ b/components/cldr-json-data-provider/src/download/error.rs @@ -4,40 +4,42 @@ use std::io; use std::path::PathBuf; #[derive(Debug)] -pub enum Error { +pub enum DownloadError { Io(io::Error, PathBuf), Reqwest(reqwest::Error), HttpStatus(reqwest::StatusCode, String), + NoCacheDir, } -impl From for Error { +impl From for DownloadError { /// Note: Prefer adding the path to Error::Io instead of using this conversion. - fn from(err: io::Error) -> Error { - Error::Io(err, PathBuf::new()) + fn from(err: io::Error) -> Self { + Self::Io(err, PathBuf::new()) } } -impl From for Error { - fn from(err: reqwest::Error) -> Error { - Error::Reqwest(err) +impl From for DownloadError { + fn from(err: reqwest::Error) -> Self { + Self::Reqwest(err) } } -impl fmt::Display for Error { +impl fmt::Display for DownloadError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Error::Io(err, path) => write!(f, "{}: {}", err, path.to_string_lossy()), - Error::Reqwest(err) => err.fmt(f), - Error::HttpStatus(status, url) => write!(f, "HTTP request failed: {}: {}", status, url), + Self::Io(err, path) => write!(f, "{}: {}", err, path.to_string_lossy()), + Self::Reqwest(err) => err.fmt(f), + Self::HttpStatus(status, url) => write!(f, "HTTP request failed: {}: {}", status, url), + Self::NoCacheDir => write!(f, "dirs::cache_dir() returned None"), } } } -impl error::Error for Error { +impl error::Error for DownloadError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - Error::Io(err, _) => Some(err), - Error::Reqwest(err) => Some(err), + Self::Io(err, _) => Some(err), + Self::Reqwest(err) => Some(err), _ => None, } } diff --git a/components/cldr-json-data-provider/src/download/io_util.rs b/components/cldr-json-data-provider/src/download/io_util.rs index f10e73d7053..dc9414b8d07 100644 --- a/components/cldr-json-data-provider/src/download/io_util.rs +++ b/components/cldr-json-data-provider/src/download/io_util.rs @@ -1,4 +1,4 @@ -use super::error::Error; +use super::error::DownloadError; use std::fs::{self, File}; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -6,7 +6,7 @@ use unzip::Unzipper; macro_rules! map_io_err { ($path_ref:ident) => { - |err| Error::Io(err, $path_ref.to_owned()) + |err| DownloadError::Io(err, $path_ref.to_owned()) }; } @@ -28,12 +28,15 @@ fn assert_files_eq(expected_file_path: &Path, actual_file_path: &Path) { // Synchronously download url and save it to destination. // TODO(#297): Implement this async. -fn download_sync(url: &str, destination: &Path) -> Result<(), Error> { +fn download_sync(url: &str, destination: &Path) -> Result<(), DownloadError> { log::info!("Downloading: {}", url); let start = Instant::now(); let mut response = reqwest::blocking::get(url)?; if !response.status().is_success() { - return Err(Error::HttpStatus(response.status(), url.to_string())); + return Err(DownloadError::HttpStatus( + response.status(), + url.to_string(), + )); } log::info!("Status: {}", response.status()); let mut file = File::create(destination).map_err(map_io_err!(destination))?; @@ -43,7 +46,7 @@ fn download_sync(url: &str, destination: &Path) -> Result<(), Error> { } #[test] -fn test_download_sync() -> Result<(), Error> { +fn test_download_sync() -> Result<(), DownloadError> { let temp_file = mktemp::Temp::new_file()?; download_sync( "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", @@ -55,7 +58,7 @@ fn test_download_sync() -> Result<(), Error> { /// Synchronously unpack a zip file into a destination directory. // TODO(#297): Implement this async. -fn unzip_sync(zip_path: &Path, dir_path: &Path) -> Result<(), Error> { +fn unzip_sync(zip_path: &Path, dir_path: &Path) -> Result<(), DownloadError> { let reader = File::open(zip_path).map_err(map_io_err!(zip_path))?; log::info!("Unzipping..."); let start = Instant::now(); @@ -67,7 +70,7 @@ fn unzip_sync(zip_path: &Path, dir_path: &Path) -> Result<(), Error> { } #[test] -fn test_unzip_sync() -> Result<(), Error> { +fn test_unzip_sync() -> Result<(), DownloadError> { let temp_dir = mktemp::Temp::new_dir()?; unzip_sync(&PathBuf::from("./tests/testdata/dummy.zip"), &temp_dir)?; assert_files_eq( @@ -81,7 +84,7 @@ fn test_unzip_sync() -> Result<(), Error> { /// /// `cache_dir` is a directory where both the zip file and the unpacked directory will be /// saved. If the zip file has already been downloaded, it will not be downloaded again. -pub fn download_and_unzip(zip_file_url: &str, cache_dir: &Path) -> Result { +pub fn download_and_unzip(zip_file_url: &str, cache_dir: &Path) -> Result { fs::create_dir_all(cache_dir).map_err(map_io_err!(cache_dir))?; let zip_dir = cache_dir.to_path_buf().join("zips"); diff --git a/components/cldr-json-data-provider/src/download/mod.rs b/components/cldr-json-data-provider/src/download/mod.rs index ec5b0ef8660..22c83e8a225 100644 --- a/components/cldr-json-data-provider/src/download/mod.rs +++ b/components/cldr-json-data-provider/src/download/mod.rs @@ -1,5 +1,6 @@ mod cldr_paths_download; -pub mod error; +mod error; mod io_util; pub use cldr_paths_download::CldrPathsDownload; +pub use error::DownloadError; diff --git a/components/cldr-json-data-provider/src/error.rs b/components/cldr-json-data-provider/src/error.rs index 0d244bbf6be..045561722d3 100644 --- a/components/cldr-json-data-provider/src/error.rs +++ b/components/cldr-json-data-provider/src/error.rs @@ -2,7 +2,7 @@ use std::error; use std::fmt; #[cfg(feature = "download")] -use crate::download::error::Error as DownloadError; +use crate::download::DownloadError; #[non_exhaustive] #[derive(Debug)] diff --git a/components/fs-data-provider/src/bin/icu4x-cldr-export.rs b/components/fs-data-provider/src/bin/icu4x-cldr-export.rs index 9f935da3cfa..20fb4dc7179 100644 --- a/components/fs-data-provider/src/bin/icu4x-cldr-export.rs +++ b/components/fs-data-provider/src/bin/icu4x-cldr-export.rs @@ -22,6 +22,7 @@ enum Error { Export(icu_fs_data_provider::FsDataError), DataProvider(icu_data_provider::DataError), LocaleParser(icu_locale::ParserError, String), + Setup(Box), } impl fmt::Display for Error { @@ -31,6 +32,7 @@ impl fmt::Display for Error { Error::Export(error) => write!(f, "{}", error), Error::DataProvider(error) => write!(f, "{}", error), Error::LocaleParser(error, s) => write!(f, "{}: {}", error, s), + Error::Setup(error) => write!(f, "{}", error), } } } @@ -53,6 +55,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: icu_cldr_json_data_provider::download::DownloadError) -> Error { + Error::Setup(Box::from(err)) + } +} + fn main() -> Result<(), Error> { let matches = App::new("ICU4X Data Exporter") .version("0.0.1") @@ -216,7 +224,7 @@ fn main() -> Result<(), Error> { ); let cldr_paths: Box = if let Some(tag) = matches.value_of("CLDR_TAG") { - Box::new(CldrPathsDownload::from_github_tag(tag)) + Box::new(CldrPathsDownload::try_from_github_tag(tag)?) } else { let mut cldr_paths_local = CldrPathsLocal::default(); if let Some(path) = matches.value_of("CLDR_CORE") { From 5647849d2d5e102d399b7401d65e15e68a242172 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 5 Oct 2020 16:43:39 -0500 Subject: [PATCH 4/4] Use DownloadError and block out enum variant --- components/cldr-json-data-provider/src/error.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/cldr-json-data-provider/src/error.rs b/components/cldr-json-data-provider/src/error.rs index 045561722d3..4764800c1fa 100644 --- a/components/cldr-json-data-provider/src/error.rs +++ b/components/cldr-json-data-provider/src/error.rs @@ -10,7 +10,8 @@ pub enum Error { JsonError(serde_json::error::Error), IoError(std::io::Error, std::path::PathBuf), MissingSource(MissingSourceError), - Download(Box), + #[cfg(feature = "download")] + Download(DownloadError), PoisonError, } @@ -42,7 +43,7 @@ impl From for Error { fn from(err: DownloadError) -> Error { match err { DownloadError::Io(err, path) => Error::IoError(err, path), - _ => Error::Download(Box::new(err)), + _ => Error::Download(err), } } } @@ -53,6 +54,7 @@ impl fmt::Display for Error { Error::JsonError(err) => write!(f, "{}", err), Error::IoError(err, path) => write!(f, "{}: {}", err, path.to_string_lossy()), Error::MissingSource(err) => err.fmt(f), + #[cfg(feature = "download")] Error::Download(err) => err.fmt(f), Error::PoisonError => write!(f, "poisoned lock on CLDR provider"), } @@ -64,7 +66,8 @@ impl error::Error for Error { match self { Error::JsonError(err) => Some(err), Error::IoError(err, _) => Some(err), - Error::Download(err) => Some(err.as_ref()), + #[cfg(feature = "download")] + Error::Download(err) => Some(err), _ => None, } }