blob: bcb968d1cc4e6ccc7fd8443af3e48d21b0ece602 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Deserialization for `.font_pkgs.json` files.
use {
crate::{
merge::{TryMerge, TryMergeGroups},
serde_ext::{self, LoadError},
},
anyhow::Error,
rayon::prelude::*,
serde::Deserialize,
std::{
collections::BTreeMap,
path::{Path, PathBuf},
},
};
/// Possible versions of [FontPackageListing].
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(tag = "version")]
enum FontPackageListingWrapper {
#[serde(rename = "1")]
Version1(FontPackageListingInternal),
}
/// A listing of font files and information about the GN packages to which they belong.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
struct FontPackageListingInternal {
pub packages: Vec<FontPackageEntry>,
}
impl FontPackageListingInternal {
/// Loads and merges multiple listings from files.
fn load_from_paths<T, P>(paths: T) -> Result<Self, Error>
where
T: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let paths: Vec<PathBuf> =
paths.into_iter().map(|path_ref| path_ref.as_ref().into()).collect();
let font_pkgs: Result<Vec<Self>, _> =
paths.par_iter().map(|path| Self::load_from_path(path)).collect();
Self::try_merge(font_pkgs?)
}
/// Loads a single listing from a file.
fn load_from_path<T: AsRef<Path>>(path: T) -> Result<Self, LoadError> {
match serde_ext::load_from_path(path) {
Ok(FontPackageListingWrapper::Version1(listing)) => Ok(listing),
Err(err) => Err(err),
}
}
/// Merges multiple listings into a single one, alphabetically sorted by file name.
/// Exact duplicates are fine, but if there are any inconsistent definitions under the same
/// file name, an error will result.
fn try_merge<T>(listings: T) -> Result<Self, Error>
where
T: IntoIterator<Item = Self>,
{
let merged_entries: Vec<FontPackageEntry> =
listings.into_iter().flat_map(|listing| listing.packages).try_merge_groups()?;
Ok(FontPackageListingInternal { packages: merged_entries })
}
}
/// An in-memory representation of a ".font_pkgs.json" file, with some indexing niceties.
pub struct FontPackageListing {
by_name: BTreeMap<String, FontPackageEntry>,
}
impl FontPackageListing {
/// Loads multiple .font_pkgs.json files and merges them.
pub fn load_from_paths<T, P>(paths: T) -> Result<Self, Error>
where
T: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let internal = FontPackageListingInternal::load_from_paths(paths)?;
let mut by_name = BTreeMap::new();
for package in internal.packages {
by_name.insert(package.file_name.clone(), package);
}
Ok(Self::new(by_name))
}
/// Creates a new `FontPackageListing` from a map of font file names to `FontPackageEntry`,
/// as generated in [`FontPackageListing::load_from_paths`].
///
/// _Visible for tests only._
pub(crate) fn new(by_name: BTreeMap<String, FontPackageEntry>) -> Self {
Self { by_name }
}
pub fn get(&self, asset_name: &str) -> Option<&FontPackageEntry> {
self.by_name.get(asset_name)
}
pub fn get_safe_name(&self, asset_name: &str) -> Option<&str> {
self.get(asset_name).map(|entry| &*entry.safe_name)
}
pub fn get_path_prefix(&self, asset_name: &str) -> Option<&Path> {
self.get(asset_name).map(|entry| Path::new(&entry.path_prefix))
}
}
/// A single entry in the font package listing.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)]
pub struct FontPackageEntry {
/// Name of the font file, e.g. `"Roboto-Black.ttf"`
pub file_name: String,
/// Transformed name of the font file for use in Fuchsia package names, e.g.
/// `"roboto-black-ttf"`.
pub safe_name: String,
/// Prefix of the path the font relative to the root font directory, e.g. `""` or
/// `"roboto/"`.
pub path_prefix: String,
}
impl FontPackageEntry {
#[cfg(test)]
pub fn new(
file_name: impl AsRef<str>,
safe_name: impl AsRef<str>,
path_prefix: impl AsRef<str>,
) -> FontPackageEntry {
FontPackageEntry {
file_name: file_name.as_ref().to_string(),
safe_name: safe_name.as_ref().to_string(),
path_prefix: path_prefix.as_ref().to_string(),
}
}
}
impl TryMerge for FontPackageEntry {
type Key = String;
fn key(&self) -> Self::Key {
self.file_name.clone()
}
fn has_matching_fields(&self, other: &Self) -> bool {
return self.safe_name == other.safe_name && self.path_prefix == other.path_prefix;
}
fn try_merge_group(mut group: Vec<Self>) -> Result<Self, Error> {
// They should all be identical, so just take the last one.
Ok(group.pop().unwrap())
}
}
#[cfg(test)]
mod tests {
use {super::*, pretty_assertions::assert_eq};
#[test]
fn test_merge() -> Result<(), Error> {
let group = vec![
FontPackageListingInternal {
packages: vec![
FontPackageEntry::new("a.ttf", "a-ttf", ""),
FontPackageEntry::new("d.ttf", "d-ttf", ""),
FontPackageEntry::new("e.ttf", "e-ttf", ""),
],
},
FontPackageListingInternal {
packages: vec![
FontPackageEntry::new("h.ttf", "h-ttf", ""),
FontPackageEntry::new("c.ttf", "c-ttf", ""),
FontPackageEntry::new("j.ttf", "j-ttf", ""),
FontPackageEntry::new("a.ttf", "a-ttf", ""),
],
},
];
let expected = FontPackageListingInternal {
packages: vec![
FontPackageEntry::new("a.ttf", "a-ttf", ""),
FontPackageEntry::new("c.ttf", "c-ttf", ""),
FontPackageEntry::new("d.ttf", "d-ttf", ""),
FontPackageEntry::new("e.ttf", "e-ttf", ""),
FontPackageEntry::new("h.ttf", "h-ttf", ""),
FontPackageEntry::new("j.ttf", "j-ttf", ""),
],
};
assert_eq!(FontPackageListingInternal::try_merge(group)?, expected);
Ok(())
}
#[test]
fn test_merge_collision() {
let group = vec![
FontPackageListingInternal {
packages: vec![
FontPackageEntry::new("a.ttf", "a-ttf", ""),
FontPackageEntry::new("d.ttf", "d-ttf", ""),
FontPackageEntry::new("e.ttf", "e-ttf", ""),
],
},
FontPackageListingInternal {
packages: vec![
FontPackageEntry::new("h.ttf", "h-ttf", ""),
FontPackageEntry::new("a.ttf", "aa-ttf", ""),
FontPackageEntry::new("j.ttf", "j-ttf", ""),
],
},
];
assert!(FontPackageListingInternal::try_merge(group).is_err());
}
}