use core::{
error::Error,
fmt::{self, Display, Formatter},
marker::PhantomData,
};
use alloc::boxed::Box;
use derive_where::derive_where;
use crate::with_context::{Colon, WithContext};
use crate::{AsDisplay, Format};
use super::ManyErrors;
#[derive_where(Clone, PartialEq, Eq, Hash, Debug; C, E, GroupContext)]
pub struct Subgroup<C, E, GroupContext = C, F = Colon, GroupFormat = AsDisplay> {
pub context: GroupContext,
pub errors: Box<ManyErrors<C, E, GroupContext, F, GroupFormat>>,
#[derive_where(skip(Debug))]
_label: PhantomData<fn() -> GroupFormat>,
}
impl<C, E, GroupContext, F, GroupFormat> Subgroup<C, E, GroupContext, F, GroupFormat> {
pub fn new(
context: GroupContext,
errors: ManyErrors<C, E, GroupContext, F, GroupFormat>,
) -> Self {
Self {
context,
errors: Box::new(errors),
_label: PhantomData,
}
}
pub fn with_formats<NewF, NewGF>(self) -> Subgroup<C, E, GroupContext, NewF, NewGF>
where
NewF: Format<WithContext<C, E, NewF>>,
NewGF: Format<GroupContext>,
{
Subgroup {
context: self.context,
errors: Box::new(self.errors.with_formats()),
_label: PhantomData,
}
}
}
impl<C, E, GroupContext, F, GroupFormat> Display for Subgroup<C, E, GroupContext, F, GroupFormat>
where
E: Error + 'static,
F: Format<WithContext<C, E, F>>,
GroupFormat: Format<GroupContext>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
GroupFormat::fmt(&self.context, f)?;
write!(f, " (")?;
Display::fmt(&*self.errors, f)?;
write!(f, ")")
}
}
impl<C, E, GroupContext, F, GroupFormat> Error for Subgroup<C, E, GroupContext, F, GroupFormat>
where
C: fmt::Debug,
GroupContext: fmt::Debug,
E: Error + 'static,
F: Format<WithContext<C, E, F>>,
GroupFormat: Format<GroupContext>,
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
#[derive_where(Clone, PartialEq, Eq, Hash, Debug; C, E, GroupContext)]
pub enum Node<C, E, GroupContext = C, F = Colon, GroupFormat = AsDisplay> {
Leaf(WithContext<C, E, F>),
Group(Subgroup<C, E, GroupContext, F, GroupFormat>),
}
impl<C, E, GroupContext, F, GroupFormat> From<WithContext<C, E, F>>
for Node<C, E, GroupContext, F, GroupFormat>
{
fn from(w: WithContext<C, E, F>) -> Self {
Node::Leaf(w)
}
}
impl<C, E, GroupContext, F, GroupFormat> From<(C, E)> for Node<C, E, GroupContext, F, GroupFormat> {
fn from((context, error): (C, E)) -> Self {
Node::Leaf(WithContext::new(context, error))
}
}
impl<C, E, GroupContext, F, GroupFormat> From<Subgroup<C, E, GroupContext, F, GroupFormat>>
for Node<C, E, GroupContext, F, GroupFormat>
{
fn from(group: Subgroup<C, E, GroupContext, F, GroupFormat>) -> Self {
Node::Group(group)
}
}
impl<C, E, GroupContext, F, GroupFormat> Node<C, E, GroupContext, F, GroupFormat> {
pub fn is_leaf(&self) -> bool {
matches!(self, Node::Leaf(_))
}
pub fn as_leaf(&self) -> Option<&WithContext<C, E, F>> {
match self {
Node::Leaf(w) => Some(w),
Node::Group(_) => None,
}
}
pub fn as_group(&self) -> Option<&Subgroup<C, E, GroupContext, F, GroupFormat>> {
match self {
Node::Group(w) => Some(w),
Node::Leaf(_) => None,
}
}
pub fn with_formats<NewF, NewGF>(self) -> Node<C, E, GroupContext, NewF, NewGF>
where
NewF: Format<WithContext<C, E, NewF>>,
NewGF: Format<GroupContext>,
{
match self {
Node::Leaf(w) => Node::Leaf(w.with_format()),
Node::Group(group) => Node::Group(group.with_formats()),
}
}
}
impl<C, E, GroupContext, F, GroupFormat> Display for Node<C, E, GroupContext, F, GroupFormat>
where
E: Error + 'static,
F: Format<WithContext<C, E, F>>,
GroupFormat: Format<GroupContext>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Node::Leaf(w) => Display::fmt(w, f),
Node::Group(group) => Display::fmt(group, f),
}
}
}
impl<C, E, GroupContext, F, GroupFormat> Error for Node<C, E, GroupContext, F, GroupFormat>
where
C: fmt::Debug,
GroupContext: fmt::Debug,
E: Error + 'static,
F: Format<WithContext<C, E, F>>,
GroupFormat: Format<GroupContext>,
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Node::Leaf(w) => Error::source(w),
Node::Group(_) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::Inner;
type N = Node<&'static str, Inner, &'static str, Colon, AsDisplay>;
#[test]
fn test_leaf_from_with_context() {
let w = WithContext::<_, _, Colon>::new("ctx", Inner::A);
let node: N = Node::from(w);
assert!(node.is_leaf());
assert_eq!(node.as_leaf().unwrap().context, "ctx");
}
#[test]
fn test_leaf_from_tuple() {
let node: N = Node::from(("ctx", Inner::A));
assert!(node.is_leaf());
assert_eq!(node.as_leaf().unwrap().context, "ctx");
}
#[test]
fn test_accessor_mismatch_is_none() {
let leaf: N = Node::from(("ctx", Inner::A));
assert!(leaf.as_group().is_none());
let group: N = Node::Group(Subgroup::new("region", ManyErrors::new()));
assert!(group.as_leaf().is_none());
}
#[test]
fn test_group_context() {
let node: N = Node::Group(Subgroup::new("region", ManyErrors::new()));
assert!(!node.is_leaf());
assert_eq!(node.as_group().unwrap().context, "region");
}
#[test]
fn test_clone_leaf() {
let node: N = Node::from(("ctx", Inner::A));
let cloned = node.clone();
assert_eq!(node, cloned);
}
#[test]
fn test_clone_group() {
let node: N = Node::Group(Subgroup::new("grp", ManyErrors::new()));
let cloned = node.clone();
assert_eq!(node, cloned);
}
#[test]
fn test_debug_leaf() {
let node: N = Node::from(("ctx", Inner::A));
let s = format!("{node:?}");
assert!(s.contains("Leaf"));
assert!(s.contains("ctx"));
}
#[test]
fn test_debug_group() {
let node: N = Node::Group(Subgroup::new("grp", ManyErrors::new()));
let s = format!("{node:?}");
assert!(s.contains("Group"));
assert!(s.contains("grp"));
}
#[test]
fn test_node_from_subgroup() {
let node: N = Subgroup::new("region", ManyErrors::new()).into();
assert!(!node.is_leaf());
assert_eq!(node.as_group().unwrap().context, "region");
}
#[test]
fn test_node_display() {
use crate::tests::Mid;
let leaf: Node<&str, Mid> = Node::from(("ctx", Mid::Inner(Inner::A)));
assert_eq!(leaf.to_string(), "ctx: mid");
let mut inner = ManyErrors::<&str, Mid>::new();
inner.push("x", Mid::Inner(Inner::A));
let group: Node<&str, Mid> = Subgroup::new("g", inner).into();
assert_eq!(group.to_string(), "g (x: mid)");
}
#[test]
fn test_node_error_source() {
use crate::FormatError as _;
use crate::tests::Mid;
let leaf: Node<&str, Mid> = Node::from(("ctx", Mid::Inner(Inner::A)));
assert_eq!(
leaf.source().expect("leaf has a source").to_string(),
"InnerA"
);
assert_eq!(leaf.one_line().to_string(), "ctx: mid: InnerA");
let group: Node<&str, Mid> = Subgroup::new("g", ManyErrors::new()).into();
assert!(group.source().is_none());
}
#[test]
fn test_subgroup_error_source_is_none() {
let mut inner = ManyErrors::<&str, Inner>::new();
inner.push("x", Inner::A);
let group = Subgroup::new("g", inner);
assert!(group.source().is_none());
assert_eq!(group.to_string(), "g (x: InnerA)");
}
#[test]
fn test_with_formats_rebuilds_tree() {
use crate::tests::WcArrow;
struct Bracket;
impl<GC: core::fmt::Display> crate::Format<GC> for Bracket {
fn fmt(label: &GC, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "[{label}]")
}
}
let mut inner = ManyErrors::<&str, Inner>::new();
inner.push("x", Inner::A);
let mut outer = ManyErrors::<&str, Inner>::new();
outer.push("leaf", Inner::B);
outer.push_group("region", inner);
assert_eq!(
outer.to_string(),
"2 errors: leaf: InnerB; region (x: InnerA)"
);
let swapped: ManyErrors<&str, Inner, &str, WcArrow, Bracket> = outer.with_formats();
assert_eq!(
swapped.to_string(),
"2 errors: leaf -> InnerB; [region] (x -> InnerA)"
);
}
#[test]
fn test_group_display_is_lossless() {
let mut inner = ManyErrors::<&str, Inner>::new();
inner.push("x", Inner::A);
inner.push("y", Inner::B);
let mut outer = ManyErrors::<&str, Inner>::new();
outer.push_group("region", inner);
let group = outer.iter().next().unwrap().as_group().unwrap();
assert_eq!(group.to_string(), "region (2 errors: x: InnerA; y: InnerB)");
}
}