Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support interpolating paths as if they had a Display impl #39

Merged
merged 2 commits into from Nov 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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));
}