Merge pull request #39 from dtolnay/path

Support interpolating paths as if they had a Display impl
This commit is contained in:
David Tolnay 2019-11-09 18:34:53 -08:00 committed by GitHub
commit 2e1939158a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 103 additions and 2 deletions

View File

@ -15,6 +15,7 @@ thiserror-impl = { version = "=1.0.4", path = "impl" }
[dev-dependencies]
anyhow = "1.0"
ref-cast = "1.0"
rustversion = "1.0"
trybuild = "1.0"

View File

@ -20,6 +20,7 @@ pub struct Display<'a> {
pub fmt: LitStr,
pub args: TokenStream,
pub was_shorthand: bool,
pub has_bonus_display: bool,
}
pub fn get(input: &[Attribute]) -> Result<Attrs> {
@ -74,6 +75,7 @@ fn parse_display(attr: &Attribute) -> Result<Display> {
fmt: input.parse()?,
args: parse_token_expr(input, false)?,
was_shorthand: false,
has_bonus_display: false,
};
display.expand_shorthand();
Ok(display)

View File

@ -77,10 +77,19 @@ fn impl_struct(input: Struct) -> TokenStream {
});
let display_impl = input.attrs.display.as_ref().map(|display| {
let use_as_display = if display.has_bonus_display {
Some(quote! {
#[allow(unused_imports)]
use thiserror::private::{DisplayAsDisplay, PathAsDisplay};
})
} else {
None
};
let pat = fields_pat(&input.fields);
quote! {
impl #impl_generics std::fmt::Display for #ty #ty_generics #where_clause {
fn fmt(&self, __formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
#use_as_display
#[allow(unused_variables)]
let Self #pat = self;
#display
@ -215,6 +224,20 @@ fn impl_enum(input: Enum) -> TokenStream {
};
let display_impl = if input.has_display() {
let use_as_display = if input.variants.iter().any(|v| {
v.attrs
.display
.as_ref()
.expect(valid::CHECKED)
.has_bonus_display
}) {
Some(quote! {
#[allow(unused_imports)]
use thiserror::private::{DisplayAsDisplay, PathAsDisplay};
})
} else {
None
};
let void_deref = if input.variants.is_empty() {
Some(quote!(*))
} else {
@ -231,6 +254,7 @@ fn impl_enum(input: Enum) -> TokenStream {
Some(quote! {
impl #impl_generics std::fmt::Display for #ty #ty_generics #where_clause {
fn fmt(&self, __formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
#use_as_display
#[allow(unused_variables)]
match #void_deref self {
#(#arms,)*

View File

@ -15,6 +15,7 @@ impl Display<'_> {
let mut read = fmt.as_str();
let mut out = String::new();
let mut args = TokenStream::new();
let mut has_bonus_display = false;
while let Some(brace) = read.find('{') {
out += &read[..=brace];
@ -35,12 +36,17 @@ impl Display<'_> {
};
let ident = Ident::new(&var, span);
args.extend(quote_spanned!(span=> , #ident));
if read.starts_with('}') {
has_bonus_display = true;
args.extend(quote_spanned!(span=> .as_display()));
}
}
out += read;
self.fmt = LitStr::new(&out, self.fmt.span());
self.args = args;
self.was_shorthand = true;
self.has_bonus_display = has_bonus_display;
}
}
@ -85,6 +91,7 @@ mod tests {
fmt: LitStr::new(input, Span::call_site()),
args: TokenStream::new(),
was_shorthand: false,
has_bonus_display: false,
};
display.expand_shorthand();
assert_eq!(fmt, display.fmt.value());
@ -93,12 +100,12 @@ mod tests {
#[test]
fn test_expand() {
assert("error {var}", "error {}", ", var");
assert("error {var}", "error {}", ", var . as_display ( )");
assert("fn main() {{ }}", "fn main() {{ }}", "");
assert(
"{v} {v:?} {0} {0:?}",
"{} {:?} {} {:?}",
", v , v , _0 , _0",
", v . as_display ( ) , v , _0 . as_display ( ) , _0",
);
}
}

28
src/display.rs Normal file
View File

@ -0,0 +1,28 @@
use std::fmt::Display;
use std::path::{self, Path, PathBuf};
pub trait DisplayAsDisplay {
fn as_display(&self) -> Self;
}
impl<T: Display> DisplayAsDisplay for &T {
fn as_display(&self) -> Self {
self
}
}
pub trait PathAsDisplay {
fn as_display(&self) -> path::Display<'_>;
}
impl PathAsDisplay for Path {
fn as_display(&self) -> path::Display<'_> {
self.display()
}
}
impl PathAsDisplay for PathBuf {
fn as_display(&self) -> path::Display<'_> {
self.display()
}
}

View File

@ -136,6 +136,7 @@
//! [`anyhow`]: https://github.com/dtolnay/anyhow
mod aserror;
mod display;
pub use thiserror_impl::*;
@ -143,4 +144,5 @@ pub use thiserror_impl::*;
#[doc(hidden)]
pub mod private {
pub use crate::aserror::AsDynError;
pub use crate::display::{DisplayAsDisplay, PathAsDisplay};
}

37
tests/test_path.rs Normal file
View File

@ -0,0 +1,37 @@
use ref_cast::RefCast;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
#[error("failed to read '{file}'")]
struct StructPathBuf {
file: PathBuf,
}
#[derive(Error, Debug, RefCast)]
#[repr(C)]
#[error("failed to read '{file}'")]
struct StructPath {
file: Path,
}
#[derive(Error, Debug)]
enum EnumPathBuf {
#[error("failed to read '{0}'")]
Read(PathBuf),
}
fn assert<T: Display>(expected: &str, value: T) {
assert_eq!(expected, value.to_string());
}
#[test]
fn test_display() {
let path = Path::new("/thiserror");
let file = path.to_owned();
assert("failed to read '/thiserror'", StructPathBuf { file });
let file = path.to_owned();
assert("failed to read '/thiserror'", EnumPathBuf::Read(file));
assert("failed to read '/thiserror'", StructPath::ref_cast(path));
}