blob: e7fe27eb19232e4be8bfbb24b6448ef09bc470b2 [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.
//! Traits for mergeable structs and collections of font metadata.
use {
anyhow::Error,
itertools::Itertools,
std::{fmt::Debug, hash::Hash},
thiserror::Error,
};
/// Indicates that multiple items that implement this trait should be merged if they all have the
/// same key.
///
/// See [crate::font_catalog::Family] for example.
pub trait TryMerge: Clone + Debug + Eq + Hash + Sync + Send + Sized + 'static {
type Key: Clone + Hash + Eq + PartialEq + Ord + PartialOrd + 'static;
/// The key by which to group the items.
fn key(&self) -> Self::Key;
/// Returns `true` for two items about to be merged if the fields are expected to match up
/// between them do in fact match up.
///
/// The default implementation just delegates to `==`, but this can be made narrower.
///
/// Usually, overriders will want to exclude the `key` field and any collection fields that will
/// be merged.
fn has_matching_fields(&self, other: &Self) -> bool {
self == other
}
/// Merges a group of items into one. At this point, it has already been confirmed that all the
/// items in the group have matching fields, as defined by [`has_matching_fields`].
fn try_merge_group(group: Vec<Self>) -> Result<Self, Error>;
/// Perform validation on the entire set of merged groups.
///
/// The default implementation is a no-op.
fn post_validate(groups: Vec<Self>) -> Result<Vec<Self>, MergeError<Self>> {
Ok(groups)
}
}
/// Try to merge multiple items into a single one. If there are any inconsistencies in the
/// fields that are expected to be identical, an error will be returned.
fn try_match_fields_and_merge_group<T>(group: impl IntoIterator<Item = T>) -> Result<T, Error>
where
T: TryMerge,
{
let group: Vec<T> = group.into_iter().collect();
if !all_have_matching_fields(&group) {
Err(MergeError::Conflict(group).into())
} else {
TryMerge::try_merge_group(group)
}
}
/// Returns true if all the items in the given group return `true` for
/// [`TryMerge::has_matching_fields`] when compared to the first item in the group.
fn all_have_matching_fields<T>(group: &Vec<T>) -> bool
where
T: TryMerge,
{
let mut iter = group.iter();
let first = iter.next();
if first.is_none() {
true
} else {
let first = first.unwrap();
iter.all(|item| item.has_matching_fields(first))
}
}
/// Import this trait to allow the use of [`TryMergeGroups::try_merge_groups`] on an `Iterator`.
pub trait TryMergeGroups
where
Self: Iterator + Sized,
Self::Item: TryMerge,
{
/// For an iterator over a list of items with possible duplicates or mergeable items, removes
/// duplicates, attempts to merge overlapping items, and sorts by the items' keys.
fn try_merge_groups(self) -> Result<Vec<Self::Item>, Error> {
let merged: Result<Vec<Self::Item>, _> = self
.unique()
.map(|item| (item.key(), item))
.into_group_map()
.into_iter()
.map(|(_key, items)| try_match_fields_and_merge_group(items))
.collect();
let mut merged = merged?;
merged.sort_by_key(|item| item.key());
TryMerge::post_validate(merged).map_err(|e| e.into())
}
}
/// Blanket implementation of `TryMergeGroups`.
impl<T, V> TryMergeGroups for T
where
T: Iterator<Item = V>,
V: TryMerge,
{
}
/// Errors while merging a group of items.
#[derive(Debug, Error)]
pub enum MergeError<V>
where
V: Debug + Send + Sync + 'static,
{
#[error("Conflict when attempting to merge {:?}", _0)]
Conflict(Vec<V>),
#[error("Post validation failed with [{}] on list {:?}", _0, _1)]
PostInvalid(String, Vec<V>),
}