Skip to content

Commit

Permalink
Merge pull request #39 from dtolnay/path
Browse files Browse the repository at this point in the history
Support interpolating paths as if they had a Display impl
  • Loading branch information
dtolnay committed Nov 10, 2019
2 parents 6f3c40e + f1eb7a8 commit 2e19391
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -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"

Expand Down
2 changes: 2 additions & 0 deletions impl/src/attr.rs
Expand Up @@ -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> {
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions impl/src/expand.rs
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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,)*
Expand Down
11 changes: 9 additions & 2 deletions impl/src/fmt.rs
Expand Up @@ -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];
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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());
Expand All @@ -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 changes: 28 additions & 0 deletions src/display.rs
@@ -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()
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Expand Up @@ -136,11 +136,13 @@
//! [`anyhow`]: https://github.com/dtolnay/anyhow

mod aserror;
mod display;

pub use thiserror_impl::*;

// Not public API.
#[doc(hidden)]
pub mod private {
pub use crate::aserror::AsDynError;
pub use crate::display::{DisplayAsDisplay, PathAsDisplay};
}
37 changes: 37 additions & 0 deletions tests/test_path.rs
@@ -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));
}

0 comments on commit 2e19391

Please sign in to comment.