tame/tamer/src/fmt.rs

655 lines
20 KiB
Rust

// TAMER formatting helpers
//
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
// This file is part of TAME.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Typed formatting helpers.
//!
//! These types create composable formatters for use with [`Display`].
//! Whereas [`Display`] operates on data owned by the type implementing it,
//! these formatters compose into functions that operate on data provided
//! _to_ it.
//! Consequently,
//! formatters are simply types,
//! and writes can be streamed just as they are with [`Display`].
//!
//! There are two types of wrappers:
//!
//! - [`DisplayWrapper`] formats objects as atoms; and
//! - [`ListDisplayWrapper`] maps a [`DisplayWrapper`] to each of its
//! items as atoms,
//! where the specific wrapper used depends on the position of the
//! item within the list and the properties of the list itself.
//!
//! [`DisplayWrapper::wrap`] and [`ListDisplayWrapper::wrap`] can be used to
//! associate wrappers with data,
//! effectively creating a custom [`Display`] implementation controlled
//! by the caller.
//! This is _not_ a substitute for canonical object representations that own
//! their own [`Display`] implementation,
//! but is suitable where display is context-dependent.
//!
//! For example:
//!
//! ```
//! # use tamer::{fmt::*, xir::fmt::*};
//! let attrs = ["foo", "bar", "baz"];
//!
//! assert_eq!(
//! AndQualConjList::<"attribute", "attributes", Tt<XmlAttr>>::wrap(&attrs)
//! .to_string(),
//! "attributes `@foo`, `@bar`, and `@baz`",
//! );
//!
//! assert_eq!(
//! AndQualConjList::<"attribute", "attributes", Tt<XmlAttr>>::wrap(&attrs[0..1])
//! .to_string(),
//! "attribute `@foo`",
//! );
//!
//! assert_eq!(
//! AndConjList::<Raw>::wrap(&["toil", "trouble"]).to_string(),
//! "toil and trouble",
//! );
//! ```
//!
//! This is in contrast to the approach taken by (for example)
//! the [diagnostic system](crate::diagnose),
//! which is to produce a data structure representing the data to be
//! formatted.
//! This system is not useful as,
//! and is not intended to be,
//! an IR for more sophisticated manipulation of data prior to output.
use std::{
fmt::{Display, Formatter, Result},
marker::PhantomData,
};
/// Wrapper for a [`Display`]-able type.
///
/// See the [module-level documentation](super) for more information.
pub trait DisplayWrapper {
/// Transform inner data and output using the provided [`Formatter`].
///
/// If a [`Formatter`] is not available,
/// [`ListDisplayWrapper::wrap`] may be used to produce a
/// [`Display`]-able object instead.
fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result;
/// Associate data with a [`DisplayWrapper`] for rendering using
/// [`Display`].
///
/// This has the effect of creating an arbitrary [`Display`]
/// implementation for the wrapped object,
/// which will work well with [`format!`] and anything else that
/// does not have access to an explicit [`Formatter`].
fn wrap<T: Display>(inner: T) -> Wrap<Self, T> {
Wrap {
inner,
_phantom: Default::default(),
}
}
}
/// Wrapper with associated data.
///
/// This has the effect of creating an arbitrary [`Display`] implementation
/// for the wrapped data,
/// which will work well with [`format!`] and anything else that does
/// not have access to an explicit [`Formatter`].
pub struct Wrap<W: DisplayWrapper + ?Sized, T: Display> {
inner: T,
_phantom: PhantomData<W>,
}
impl<W: DisplayWrapper, T: Display> Display for Wrap<W, T> {
fn fmt(&self, f: &mut Formatter) -> Result {
W::fmt(&self.inner, f)
}
}
/// Echo data as-is without any wrapping.
///
/// This is primarily used at the root of a wrapper composition.
pub struct Raw;
impl DisplayWrapper for Raw {
fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result {
inner.fmt(f)
}
}
/// Prefix data with a static [`str`].
///
/// See also [`Suffix`] and [`Delim`].
pub struct Prefix<const PREFIX: &'static str, W: DisplayWrapper>(
PhantomData<W>,
);
impl<const PREFIX: &'static str, W: DisplayWrapper> DisplayWrapper
for Prefix<PREFIX, W>
{
fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result {
f.write_str(PREFIX)?;
W::fmt(inner, f)
}
}
/// Suffix data with a static [`str`].
///
/// See also [`Prefix`] and [`Delim`].
pub struct Suffix<const SUFFIX: &'static str, W: DisplayWrapper>(
PhantomData<W>,
);
impl<const SUFFIX: &'static str, W: DisplayWrapper> DisplayWrapper
for Suffix<SUFFIX, W>
{
fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result {
W::fmt(inner, f)?;
f.write_str(SUFFIX)
}
}
/// Surround a value in delimiters.
///
/// See also [`Prefix`] and [`Suffix`].
pub type Delim<const LEFT: &'static str, const RIGHT: &'static str, W> =
Prefix<LEFT, Suffix<RIGHT, W>>;
/// Denote text that would conventionally be delimited in a teletypewriter
/// font.
///
/// This produces a markdown-style quote using backticks.
///
/// NB: This does not defend against nested quotes,
/// so this is _not_ safe against format escapes.
pub type Tt<W> = Delim<"`", "`", W>;
/// Quote text that would conventionally be delimited in a teletypewriter
/// font.
///
/// This is a more terse alternative to [`Tt`] when formatter composition is
/// unneeded.
pub type TtQuote = Tt<Raw>;
/// Prefix with a single space.
pub type Sp<W> = Prefix<" ", W>;
/// Wrapper for a list that maps each element to a context-specific
/// [`DisplayWrapper`].
///
/// This uses the slice API for wrapping since [`Display`] takes objects by
/// non-mutable reference,
/// and so we cannot consume an iterator.
///
/// The associated types define the wrappers to use for items in various
/// positions depending on the length of the list.
/// They were chosen to be somewhat intuitive given the necessary use cases.
///
/// See the [module-level documentation](super) for examples.
pub trait ListDisplayWrapper {
/// Wrapper to use when the list contains only a single item.
type Single: DisplayWrapper = Raw;
/// Wrapper for the first item in a multi-item list.
type First: DisplayWrapper = Raw;
/// Wrapper for all but the first and last items in a multi-item list.
type Middle: DisplayWrapper = Raw;
/// Wrapper for the last item of a list containing a pair of items.
type LastOfPair: DisplayWrapper = Raw;
/// Wrapper for the last item of a list containing more than two items.
type LastOfMany: DisplayWrapper = Raw;
/// Format a slice using the provided wrappers.
///
/// If a [`Formatter`] is not available,
/// [`ListDisplayWrapper::wrap`] may be used to produce a
/// [`Display`]-able object instead.
fn fmt<T: Display>(list: &[T], f: &mut Formatter) -> Result {
let lasti = list.len().saturating_sub(1);
// This can be further abstracted away using the above primitives,
// if ever we have a use.
for next in list.iter().enumerate() {
match next {
(i, x) => Self::fmt_nth(lasti, i, x, f)?,
};
}
Ok(())
}
/// Format an item as if it were the `i`th value of a list of length
/// `lasti+1`.
///
/// This allows for generating list-like output without the expense of
/// actually producing a list.
/// This may be useful when values are stored in different memory
/// location,
/// so that the displaying of those values is a problem of invoking
/// this method on them in the right order,
/// rather than collecting them just for the sake of display.
/// If Rust supports `const` array/Vec functions in the future,
/// this may not be necessary anymore,
/// unless we also don't want the space cost of such a
/// precomputation
/// (but it may come with performance benefits from locality).
#[inline]
fn fmt_nth<T: Display>(
lasti: usize,
i: usize,
item: &T,
f: &mut Formatter,
) -> Result {
match (i, item) {
(0, x) if lasti == 0 => {
Self::Single::fmt(x, f)?;
}
(0, x) => {
Self::First::fmt(x, f)?;
}
(i, x) if lasti == i => {
if i == 1 {
Self::LastOfPair::fmt(x, f)?;
} else {
Self::LastOfMany::fmt(x, f)?;
}
}
(_, x) => {
Self::Middle::fmt(x, f)?;
}
}
Ok(())
}
/// Associate data with a [`ListDisplayWrapper`] for rendering using
/// [`Display`].
///
/// This has the effect of creating an arbitrary [`Display`]
/// implementation for the wrapped slice,
/// which will work well with [`format!`] and anything else that
/// does not have access to an explicit [`Formatter`].
fn wrap<T: Display>(list: &[T]) -> ListWrap<Self, T> {
ListWrap {
list,
_phantom: Default::default(),
}
}
}
/// Format each item of a slice using a [`DisplayWrapper`] formatter,
/// outputting an English list with a serial comma and conjunctive term.
///
/// No formatting is done to a single item,
/// and the serial comma is omitted for only two items.
/// It is assumed that the items are not complete sentences,
/// and nested lists are not expected
/// (in such a case you'd want to replace the inner list with
/// semicolons and a more robust abstraction is needed).
///
/// The use of the serial comma (also known as the Oxford comma) is the
/// preference of the author.
///
/// For example:
/// If we have a slice `[1, 2, 3]`,
/// this will output "1, 2, and 3".
/// If we have a slice `[1, 2]`,
/// it will omit the serial comma and output "1 and 2",
/// since no person would write "1, and 2" unless they wished to
/// place particularly dramatic emphasis on the first item.
///
/// To output a qualifier before the list,
/// see [`QualConjList`].
pub struct ConjList<const CONJ: &'static str, W: DisplayWrapper>(
PhantomData<W>,
);
impl<const CONJ: &'static str, W: DisplayWrapper> ListDisplayWrapper
for ConjList<CONJ, W>
{
type Single = W;
type First = W;
type Middle = Prefix<", ", W>;
type LastOfPair = Sp<Prefix<CONJ, Sp<W>>>;
// Comma after the penultimate term (serial/Oxford comma).
type LastOfMany = Prefix<", ", Prefix<CONJ, Sp<W>>>;
}
/// A list of values with a serial comma and the term "and" as a
/// conjunction between the penultimate and final items.
pub type AndConjList<W> = ConjList<"and", W>;
/// A list of values with a serial comma and the term "or" as a
/// conjunction between the penultimate and final items.
///
/// Terminology note:
/// English refers to the term "or" here as a conjunction between words,
/// which differs from "or" in logic as a disjunction.
pub type OrConjList<W> = ConjList<"or", W>;
/// Format each item of a slice using a [`DisplayWrapper`] formatter,
/// outputting an English list with a serial comma and conjunctive term,
/// qualified by a term that is singular or plural depending on the length
/// of the list.
///
/// See [`ConjList`] for more information;
/// this operates identically but with the addition of the qualifier.
pub struct QualConjList<
const QUAL_ONE: &'static str,
const QUAL_MANY: &'static str,
const CONJ: &'static str,
W: DisplayWrapper,
>(PhantomData<W>);
impl<
const QUAL_ONE: &'static str,
const QUAL_MANY: &'static str,
const CONJ: &'static str,
W: DisplayWrapper,
> ListDisplayWrapper for QualConjList<QUAL_ONE, QUAL_MANY, CONJ, W>
{
type Single =
Prefix<QUAL_ONE, Sp<<ConjList<CONJ, W> as ListDisplayWrapper>::Single>>;
type First =
Prefix<QUAL_MANY, Sp<<ConjList<CONJ, W> as ListDisplayWrapper>::First>>;
type Middle = <ConjList<CONJ, W> as ListDisplayWrapper>::Middle;
type LastOfPair = <ConjList<CONJ, W> as ListDisplayWrapper>::LastOfPair;
type LastOfMany = <ConjList<CONJ, W> as ListDisplayWrapper>::LastOfMany;
}
/// A list of values with a serial comma and the term "and" as a
/// conjunction between the penultimate and final items.
/// prefixed with a singular or plural qualifier term depending on the
/// length of the list.
pub type AndQualConjList<
const QUAL_ONE: &'static str,
const QUAL_MANY: &'static str,
W,
> = QualConjList<QUAL_ONE, QUAL_MANY, "and", W>;
/// A list of values with a serial comma and the term "or" as a
/// conjunction between the penultimate and final items.
/// prefixed with a singular or plural qualifier term depending on the
/// length of the list.
///
/// Terminology note:
/// English refers to the term "or" here as a conjunction between words,
/// which differs from "or" in logic as a disjunction.
pub type OrQualConjList<
const QUAL_ONE: &'static str,
const QUAL_MANY: &'static str,
W,
> = QualConjList<QUAL_ONE, QUAL_MANY, "or", W>;
/// A list of values separated by a delimiter.
///
/// This is analogous to the typical `join` operation on lists.
/// Single-element lists will have no delimiter,
/// but lists of multiple elements will have each element delimited by the
/// same delimiter.
/// This is in contrast with wrappers that follow more sophisticated
/// typographical conventions,
/// such as [`ConjList`] and [`QualConjList`].
pub struct JoinListWrap<const DELIM: &'static str, W: DisplayWrapper>(
PhantomData<W>,
);
impl<const DELIM: &'static str, W: DisplayWrapper> ListDisplayWrapper
for JoinListWrap<DELIM, W>
{
type Single = W;
type First = W;
type Middle = Prefix<DELIM, W>;
type LastOfPair = Prefix<DELIM, W>;
type LastOfMany = Prefix<DELIM, W>;
}
/// List wrapper with associated data.
///
/// This has the effect of creating an arbitrary [`Display`] implementation
/// for the wrapped list,
/// which will work well with [`format!`] and anything else that does
/// not have access to an explicit [`Formatter`].
pub struct ListWrap<'a, W: ListDisplayWrapper + ?Sized, T: Display> {
list: &'a [T],
_phantom: PhantomData<W>,
}
impl<'a, W: ListDisplayWrapper + ?Sized, T: Display> Display
for ListWrap<'a, W, T>
{
fn fmt(&self, f: &mut Formatter) -> Result {
W::fmt(self.list, f)
}
}
/// Wrap a `fmt`-like function to be used as [`Display::fmt`] for this
/// object.
///
/// This works around the problem of having a function expecting a
/// [`Formatter`],
/// but not having a [`Formatter`] to call it with.
/// It also allows for arbitrary (compatible) functions to be used as
/// [`Display`].
pub struct DisplayFn<F: Fn(&mut Formatter) -> Result>(pub F);
impl<F: Fn(&mut Formatter) -> Result> Display for DisplayFn<F> {
fn fmt(&self, f: &mut Formatter) -> Result {
(self.0)(f)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn prefix() {
assert_eq!(Prefix::<"@!", Raw>::wrap("foo").to_string(), "@!foo",);
assert_eq!(
Prefix::<"1", Prefix::<"2", Raw>>::wrap("bar").to_string(),
"12bar",
);
}
#[test]
fn suffix() {
assert_eq!(Suffix::<"!@", Raw>::wrap("foo").to_string(), "foo!@",);
assert_eq!(
Suffix::<"1", Suffix::<"2", Raw>>::wrap("bar").to_string(),
"bar21",
);
}
#[test]
fn delimit() {
assert_eq!(Delim::<"[", "]", Raw>::wrap("foo").to_string(), "[foo]",);
assert_eq!(
Delim::<"(", ")", Delim::<"|", "|", Raw>>::wrap("bar").to_string(),
"(|bar|)",
);
}
// Certain types are trivially verifiable by their definition,
// and so have no tests.
#[test]
fn conj_list_single() {
assert_eq!(
ConjList::<"unused", Raw>::wrap(&["single"]).to_string(),
"single",
);
// Ensure that we're actually mapping.
assert_eq!(
ConjList::<"unused", Delim<"(", ")", Raw>>::wrap(&["single"])
.to_string(),
"(single)",
);
}
#[test]
fn conj_list_double() {
assert_eq!(
ConjList::<"and", Raw>::wrap(&["first", "second"]).to_string(),
"first and second",
);
// Ensure that we're actually mapping.
assert_eq!(
ConjList::<"or", Delim<"(", ")", Raw>>::wrap(&["first", "second"])
.to_string(),
"(first) or (second)",
);
}
#[test]
fn conj_list_many() {
assert_eq!(
ConjList::<"and", Raw>::wrap(&["first", "second", "third"])
.to_string(),
"first, second, and third",
);
// Ensure that we're actually mapping.
assert_eq!(
ConjList::<"or", Delim<"[", "]", Raw>>::wrap(&[
"first", "second", "third"
])
.to_string(),
"[first], [second], or [third]",
);
}
#[test]
fn qualified_conj_list_single() {
assert_eq!(
QualConjList::<"thing", "things", "unused", Raw>::wrap(&["a"])
.to_string(),
"thing a",
);
// Ensure that we're actually mapping.
assert_eq!(
QualConjList::<"thing", "things", "unused", Delim<"(", ")", Raw>>::wrap(&["a"])
.to_string(),
"thing (a)",
);
}
#[test]
fn qualified_conj_list_double() {
assert_eq!(
QualConjList::<"thing", "things", "and", Raw>::wrap(&["a", "b"])
.to_string(),
"things a and b",
);
// Ensure that we're actually mapping.
assert_eq!(
QualConjList::<"thing", "things", "or", Delim<"(", ")", Raw>>::wrap(&["a", "b"])
.to_string(),
"things (a) or (b)",
);
}
#[test]
fn qualified_conj_list_many() {
assert_eq!(
QualConjList::<"thing", "things", "and", Raw>::wrap(&[
"a", "b", "c"
])
.to_string(),
"things a, b, and c",
);
// Ensure that we're actually mapping.
assert_eq!(
QualConjList::<"thing", "things", "or", Delim<"(", ")", Raw>>::wrap(&["a", "b", "c"])
.to_string(),
"things (a), (b), or (c)",
);
}
#[test]
fn display_fn() {
assert_eq!(
DisplayFn(|f| write!(f, "test fmt")).to_string(),
"test fmt",
);
}
// `fmt_nth` is used by the above tests,
// but that's an implementation detail;
// we expose it as a public API so it ought to be tested too.
#[test]
fn fmt_nth() {
type Sut = QualConjList<"thing", "things", "or", Raw>;
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(0, 0, &"foo", f)).to_string(),
"thing foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(1, 0, &"foo", f)).to_string(),
"things foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(1, 1, &"foo", f)).to_string(),
" or foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(2, 0, &"foo", f)).to_string(),
"things foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(2, 1, &"foo", f)).to_string(),
", foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(2, 2, &"foo", f)).to_string(),
", or foo",
);
}
#[test]
fn delim_list() {
assert_eq!(
JoinListWrap::<"::", Raw>::wrap(&["one", "two", "three"])
.to_string(),
"one::two::three",
);
assert_eq!(
JoinListWrap::<" -> ", TtQuote>::wrap(&["foo", "bar"]).to_string(),
"`foo` -> `bar`",
);
}
}