From 07b6f1501b815813da17fefa846edb49370be44d Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Mon, 17 Jun 2024 00:12:01 +0200 Subject: [PATCH 01/12] Refs: #2768 Add an Attachment type to axum-extra --- axum-extra/Cargo.toml | 1 + axum-extra/src/response/attachment.rs | 112 ++++++++++++++++++++++++++ axum-extra/src/response/mod.rs | 8 ++ 3 files changed, 121 insertions(+) create mode 100644 axum-extra/src/response/attachment.rs diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index 48c6556ccc..fbfb2b3719 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -15,6 +15,7 @@ version = "0.9.3" default = ["tracing"] async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"] +attachment = [] cookie = ["dep:cookie"] cookie-private = ["cookie", "cookie?/private"] cookie-signed = ["cookie", "cookie?/signed"] diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs new file mode 100644 index 0000000000..cceeb743b2 --- /dev/null +++ b/axum-extra/src/response/attachment.rs @@ -0,0 +1,112 @@ +use axum::response::IntoResponse; +use http::{header, HeaderMap, HeaderValue}; + +/// A file attachment response type +/// +/// This type will set the `Content-Disposition` header for this response. In response a webbrowser +/// will download the contents localy. +/// +/// Use the `filename` and `content_type` methods to set the filename or content-type of the +/// attachment. If these values are not set they will not be sent. +/// +/// +/// # Example +/// +/// ```rust +/// use axum::{http::StatusCode, routing::get, Router}; +/// use axum_extra::response::Attachment; +/// +/// async fn cargo_toml() -> Result, (StatusCode, String)> { +/// let file_contents = tokio::fs::read_to_string("Cargo.toml") +/// .await +/// .map_err(|err| (StatusCode::NOT_FOUND, format!("File not found: {err}")))?; +/// Ok(Attachment::new(file_contents) +/// .filename("Cargo.toml") +/// .content_type("text/x-toml")) +/// } +/// +/// let app = Router::new().route("/Cargo.toml", get(cargo_toml)); +/// let _: Router = app; +/// ``` +/// +/// Hyper will set the `Content-Length` header if it knows the length. To manually set this header +/// use the [`Attachment`] type in a tuple. +/// +/// # Note +/// If the content length is known and this header is manualy set to a different length hyper +/// panics. +/// +/// ```rust +/// async fn with_content_length() -> impl IntoResponse { +/// ( +/// [(header::CONTENT_LENGTH, 3)], +/// Attachment::new([0, 0, 0]) +/// .filename("Cargo.toml") +/// .content_type("text/x-toml"), +/// ) +/// } +/// ``` +#[derive(Debug)] +pub struct Attachment { + inner: T, + filename: Option, + content_type: Option, +} + +impl Attachment { + /// Creates a new [`Attachment`]. + pub fn new(inner: T) -> Self { + Self { + inner, + filename: None, + content_type: None, + } + } + + /// Sets the filename of the [`Attachment`] + /// + /// This updates the `Content-Disposition` header to add a filename. + pub fn filename>(mut self, value: H) -> Self { + // TODO: change + self.filename = value.try_into().ok(); + self + } + + /// Sets the content-type of the [`Attachment`] + pub fn content_type>(mut self, value: H) -> Self { + // TODO: change + self.content_type = value.try_into().ok(); + self + } +} + +impl IntoResponse for Attachment +where + T: IntoResponse, +{ + fn into_response(self) -> axum::response::Response { + let mut headers = HeaderMap::new(); + + if let Some(content_type) = self.content_type { + headers.append(header::CONTENT_TYPE, content_type); + } + + if let Some(filename) = self.filename { + let mut bytes = b"attachment; filename=\"".to_vec(); + bytes.extend_from_slice(filename.as_bytes()); + bytes.push(b'\"'); + + let content_disposition = HeaderValue::from_bytes(&bytes) + .expect("This was a HeaderValue so this can not fail"); + + headers.append(header::CONTENT_DISPOSITION, content_disposition); + } else { + headers.append( + header::CONTENT_DISPOSITION, + HeaderValue::from_static("attachment"), + ); + } + + (headers, self.inner).into_response() + } +} diff --git a/axum-extra/src/response/mod.rs b/axum-extra/src/response/mod.rs index dda382cf02..be4dcfc5cf 100644 --- a/axum-extra/src/response/mod.rs +++ b/axum-extra/src/response/mod.rs @@ -3,6 +3,10 @@ #[cfg(feature = "erased-json")] mod erased_json; +#[cfg(feature = "attachment")] +mod attachment; + + #[cfg(feature = "erased-json")] pub use erased_json::ErasedJson; @@ -10,6 +14,10 @@ pub use erased_json::ErasedJson; #[doc(no_inline)] pub use crate::json_lines::JsonLines; + +#[cfg(feature = "attachment")] +pub use attachment::Attachment; + macro_rules! mime_response { ( $(#[$m:meta])* From 0a9b52ad4c68091946cd7f9d697327dadd49a7af Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Mon, 17 Jun 2024 16:20:34 +0200 Subject: [PATCH 02/12] Refs: #2768 log Attachment errors using tracing --- axum-extra/Cargo.toml | 2 +- axum-extra/src/response/attachment.rs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index fbfb2b3719..4ba0732a61 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -15,7 +15,7 @@ version = "0.9.3" default = ["tracing"] async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"] -attachment = [] +attachment = ["dep:tracing"] cookie = ["dep:cookie"] cookie-private = ["cookie", "cookie?/private"] cookie-signed = ["cookie", "cookie?/signed"] diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index cceeb743b2..f00aa38e2a 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -1,5 +1,6 @@ use axum::response::IntoResponse; use http::{header, HeaderMap, HeaderValue}; +use tracing::trace; /// A file attachment response type /// @@ -67,15 +68,21 @@ impl Attachment { /// /// This updates the `Content-Disposition` header to add a filename. pub fn filename>(mut self, value: H) -> Self { - // TODO: change - self.filename = value.try_into().ok(); + if let Some(filename) = value.try_into().ok() { + self.filename = Some(filename); + } else { + trace!("Attachment filename contains invalid characters"); + } self } /// Sets the content-type of the [`Attachment`] pub fn content_type>(mut self, value: H) -> Self { - // TODO: change - self.content_type = value.try_into().ok(); + if let Some(content_type) = value.try_into().ok() { + self.content_type = Some(content_type); + } else { + trace!("Attachment content-type contains invalid characters"); + } self } } From 0be6dac218877f926a780bc081d7c7a8215b71aa Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Mon, 17 Jun 2024 17:12:31 +0200 Subject: [PATCH 03/12] Fix typos in Attachment docs --- axum-extra/src/response/attachment.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index f00aa38e2a..fead4e4c8e 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -5,7 +5,7 @@ use tracing::trace; /// A file attachment response type /// /// This type will set the `Content-Disposition` header for this response. In response a webbrowser -/// will download the contents localy. +/// will download the contents locally. /// /// Use the `filename` and `content_type` methods to set the filename or content-type of the /// attachment. If these values are not set they will not be sent. @@ -34,7 +34,7 @@ use tracing::trace; /// use the [`Attachment`] type in a tuple. /// /// # Note -/// If the content length is known and this header is manualy set to a different length hyper +/// If the content length is known and this header is manually set to a different length hyper /// panics. /// /// ```rust From 82a13a6a96766637b42b00bd32ad5c8698f3cd71 Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Mon, 17 Jun 2024 20:45:24 +0200 Subject: [PATCH 04/12] Apply suggestions from code review Co-authored-by: Jonas Platte --- axum-extra/src/response/attachment.rs | 32 +++++++++++++-------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index fead4e4c8e..d67c1fb297 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -2,10 +2,10 @@ use axum::response::IntoResponse; use http::{header, HeaderMap, HeaderValue}; use tracing::trace; -/// A file attachment response type +/// A file attachment response. /// -/// This type will set the `Content-Disposition` header for this response. In response a webbrowser -/// will download the contents locally. +/// This type will set the `Content-Disposition` header to `attachment`. In response a webbrowser +/// will offer to download the file instead of displaying it directly. /// /// Use the `filename` and `content_type` methods to set the filename or content-type of the /// attachment. If these values are not set they will not be sent. @@ -64,15 +64,16 @@ impl Attachment { } } - /// Sets the filename of the [`Attachment`] + /// Sets the filename of the [`Attachment`]. /// /// This updates the `Content-Disposition` header to add a filename. pub fn filename>(mut self, value: H) -> Self { - if let Some(filename) = value.try_into().ok() { - self.filename = Some(filename); + self.filename = if let Ok(filename) = value.try_into() { + Some(filename) } else { trace!("Attachment filename contains invalid characters"); - } + None + }; self } @@ -98,21 +99,18 @@ where headers.append(header::CONTENT_TYPE, content_type); } - if let Some(filename) = self.filename { + let content_disposition = if let Some(filename) = self.filename { let mut bytes = b"attachment; filename=\"".to_vec(); bytes.extend_from_slice(filename.as_bytes()); bytes.push(b'\"'); - let content_disposition = HeaderValue::from_bytes(&bytes) - .expect("This was a HeaderValue so this can not fail"); - - headers.append(header::CONTENT_DISPOSITION, content_disposition); + HeaderValue::from_bytes(&bytes) + .expect("This was a HeaderValue so this can not fail") } else { - headers.append( - header::CONTENT_DISPOSITION, - HeaderValue::from_static("attachment"), - ); - } + HeaderValue::from_static("attachment") + }; + + headers.append(header::CONTENT_DISPOSITION, content_disposition) (headers, self.inner).into_response() } From 336ef99eb637cc352c87679369e1ac1a36747fe5 Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Mon, 17 Jun 2024 20:53:17 +0200 Subject: [PATCH 05/12] Change docs about content-length with Attachment --- axum-extra/src/response/attachment.rs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index d67c1fb297..037b8d447c 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -30,23 +30,10 @@ use tracing::trace; /// let _: Router = app; /// ``` /// -/// Hyper will set the `Content-Length` header if it knows the length. To manually set this header -/// use the [`Attachment`] type in a tuple. -/// /// # Note -/// If the content length is known and this header is manually set to a different length hyper -/// panics. /// -/// ```rust -/// async fn with_content_length() -> impl IntoResponse { -/// ( -/// [(header::CONTENT_LENGTH, 3)], -/// Attachment::new([0, 0, 0]) -/// .filename("Cargo.toml") -/// .content_type("text/x-toml"), -/// ) -/// } -/// ``` +/// When using this type in Axum with hyper, hyper will set the `Content-Length` if it is known. +/// #[derive(Debug)] pub struct Attachment { inner: T, From 312c5e8d6bc8928e2c51e608af50f6362ec62c46 Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Mon, 17 Jun 2024 23:45:23 +0200 Subject: [PATCH 06/12] Add semicolon --- axum-extra/src/response/attachment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index 037b8d447c..ef55f18ae2 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -97,7 +97,7 @@ where HeaderValue::from_static("attachment") }; - headers.append(header::CONTENT_DISPOSITION, content_disposition) + headers.append(header::CONTENT_DISPOSITION, content_disposition); (headers, self.inner).into_response() } From 739f9153145f516100c81a5f249ac46abec72f11 Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Mon, 17 Jun 2024 23:50:49 +0200 Subject: [PATCH 07/12] Fix clippy warning --- axum-extra/src/response/attachment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index ef55f18ae2..6f039d9ba0 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -66,7 +66,7 @@ impl Attachment { /// Sets the content-type of the [`Attachment`] pub fn content_type>(mut self, value: H) -> Self { - if let Some(content_type) = value.try_into().ok() { + if let Ok(content_type) = value.try_into() { self.content_type = Some(content_type); } else { trace!("Attachment content-type contains invalid characters"); From b5e3efaca69d8857a6c71e7b92145ed987ccaa43 Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Tue, 18 Jun 2024 00:23:54 +0200 Subject: [PATCH 08/12] fix indentation --- axum/src/docs/routing/nest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum/src/docs/routing/nest.md b/axum/src/docs/routing/nest.md index c3f7308fdb..3151729ea2 100644 --- a/axum/src/docs/routing/nest.md +++ b/axum/src/docs/routing/nest.md @@ -181,7 +181,7 @@ router. # Panics - If the route overlaps with another route. See [`Router::route`] -for more details. + for more details. - If the route contains a wildcard (`*`). - If `path` is empty. From e6b954d38571c29a323466884c672104fcfbb5e4 Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Tue, 18 Jun 2024 21:00:52 +0200 Subject: [PATCH 09/12] Fix CI errors --- axum-extra/src/extract/json_deserializer.rs | 3 +-- axum-extra/src/response/attachment.rs | 3 +-- axum-extra/src/response/mod.rs | 2 -- axum-extra/src/routing/typed.rs | 9 ++------- axum/src/boxed.rs | 1 + axum/src/json.rs | 3 +-- 6 files changed, 6 insertions(+), 15 deletions(-) diff --git a/axum-extra/src/extract/json_deserializer.rs b/axum-extra/src/extract/json_deserializer.rs index b138c50f3a..03f1a41911 100644 --- a/axum-extra/src/extract/json_deserializer.rs +++ b/axum-extra/src/extract/json_deserializer.rs @@ -23,8 +23,7 @@ use std::marker::PhantomData; /// Additionally, a `JsonRejection` error will be returned, when calling `deserialize` if: /// /// - The body doesn't contain syntactically valid JSON. -/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target -/// type. +/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target type. /// - Attempting to deserialize escaped JSON into a type that must be borrowed (e.g. `&'a str`). /// /// ⚠️ `serde` will implicitly try to borrow for `&str` and `&[u8]` types, but will error if the diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index 6f039d9ba0..b2f7556b74 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -91,8 +91,7 @@ where bytes.extend_from_slice(filename.as_bytes()); bytes.push(b'\"'); - HeaderValue::from_bytes(&bytes) - .expect("This was a HeaderValue so this can not fail") + HeaderValue::from_bytes(&bytes).expect("This was a HeaderValue so this can not fail") } else { HeaderValue::from_static("attachment") }; diff --git a/axum-extra/src/response/mod.rs b/axum-extra/src/response/mod.rs index be4dcfc5cf..d17f7be6db 100644 --- a/axum-extra/src/response/mod.rs +++ b/axum-extra/src/response/mod.rs @@ -6,7 +6,6 @@ mod erased_json; #[cfg(feature = "attachment")] mod attachment; - #[cfg(feature = "erased-json")] pub use erased_json::ErasedJson; @@ -14,7 +13,6 @@ pub use erased_json::ErasedJson; #[doc(no_inline)] pub use crate::json_lines::JsonLines; - #[cfg(feature = "attachment")] pub use attachment::Attachment; diff --git a/axum-extra/src/routing/typed.rs b/axum-extra/src/routing/typed.rs index f754282369..704d503eb0 100644 --- a/axum-extra/src/routing/typed.rs +++ b/axum-extra/src/routing/typed.rs @@ -84,13 +84,8 @@ use serde::Serialize; /// The macro expands to: /// /// - A `TypedPath` implementation. -/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`], -/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must -/// also implement [`serde::Deserialize`], unless it's a unit struct. -/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other -/// things, create links to known paths and have them verified statically. Note that the -/// [`Display`] implementation for each field must return something that's compatible with its -/// [`Deserialize`] implementation. +/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`], [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must also implement [`serde::Deserialize`], unless it's a unit struct. +/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other things, create links to known paths and have them verified statically. Note that the [`Display`] implementation for each field must return something that's compatible with its [`Deserialize`] implementation. /// /// Additionally the macro will verify the captures in the path matches the fields of the struct. /// For example this fails to compile since the struct doesn't have a `team_id` field: diff --git a/axum/src/boxed.rs b/axum/src/boxed.rs index f541a9fa30..32808f51de 100644 --- a/axum/src/boxed.rs +++ b/axum/src/boxed.rs @@ -103,6 +103,7 @@ where } } +#[allow(dead_code)] pub(crate) struct MakeErasedRouter { pub(crate) router: Router, pub(crate) into_route: fn(Router, S) -> Route, diff --git a/axum/src/json.rs b/axum/src/json.rs index c4435922a4..854ead4e3d 100644 --- a/axum/src/json.rs +++ b/axum/src/json.rs @@ -17,8 +17,7 @@ use serde::{de::DeserializeOwned, Serialize}; /// /// - The request doesn't have a `Content-Type: application/json` (or similar) header. /// - The body doesn't contain syntactically valid JSON. -/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target -/// type. +/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target type. /// - Buffering the request body fails. /// /// ⚠️ Since parsing JSON requires consuming the request body, the `Json` extractor must be From 469d510f8147735194e6b96fcdcefd8a61e1325b Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Wed, 19 Jun 2024 12:59:45 +0200 Subject: [PATCH 10/12] Update axum-extra/src/response/attachment.rs Co-authored-by: Jonas Platte --- axum-extra/src/response/attachment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index b2f7556b74..d621c0d3e2 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -32,7 +32,7 @@ use tracing::trace; /// /// # Note /// -/// When using this type in Axum with hyper, hyper will set the `Content-Length` if it is known. +/// If you use axum with hyper, hyper will set the `Content-Length` if it is known. /// #[derive(Debug)] pub struct Attachment { From 5a0669f6ff96bcfe8ebfe7970c9d9f0ddbed8303 Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Wed, 19 Jun 2024 12:59:58 +0200 Subject: [PATCH 11/12] Update axum-extra/src/response/attachment.rs Co-authored-by: Jonas Platte --- axum-extra/src/response/attachment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index d621c0d3e2..535f4d18c9 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -58,7 +58,7 @@ impl Attachment { self.filename = if let Ok(filename) = value.try_into() { Some(filename) } else { - trace!("Attachment filename contains invalid characters"); + error!("Attachment filename contains invalid characters"); None }; self From f1f691b60005b4508ecec918ca5b6916551fce6c Mon Sep 17 00:00:00 2001 From: Joey de Waal Date: Wed, 19 Jun 2024 13:18:43 +0200 Subject: [PATCH 12/12] Update docs --- axum-extra/src/response/attachment.rs | 4 ++-- axum-extra/src/routing/typed.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/axum-extra/src/response/attachment.rs b/axum-extra/src/response/attachment.rs index 535f4d18c9..923ad99116 100644 --- a/axum-extra/src/response/attachment.rs +++ b/axum-extra/src/response/attachment.rs @@ -1,6 +1,6 @@ use axum::response::IntoResponse; use http::{header, HeaderMap, HeaderValue}; -use tracing::trace; +use tracing::error; /// A file attachment response. /// @@ -69,7 +69,7 @@ impl Attachment { if let Ok(content_type) = value.try_into() { self.content_type = Some(content_type); } else { - trace!("Attachment content-type contains invalid characters"); + error!("Attachment content-type contains invalid characters"); } self } diff --git a/axum-extra/src/routing/typed.rs b/axum-extra/src/routing/typed.rs index 704d503eb0..34c9513bc3 100644 --- a/axum-extra/src/routing/typed.rs +++ b/axum-extra/src/routing/typed.rs @@ -84,8 +84,13 @@ use serde::Serialize; /// The macro expands to: /// /// - A `TypedPath` implementation. -/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`], [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must also implement [`serde::Deserialize`], unless it's a unit struct. -/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other things, create links to known paths and have them verified statically. Note that the [`Display`] implementation for each field must return something that's compatible with its [`Deserialize`] implementation. +/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`], +/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must +/// also implement [`serde::Deserialize`], unless it's a unit struct. +/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other +/// things, create links to known paths and have them verified statically. Note that the +/// [`Display`] implementation for each field must return something that's compatible with its +/// [`Deserialize`] implementation. /// /// Additionally the macro will verify the captures in the path matches the fields of the struct. /// For example this fails to compile since the struct doesn't have a `team_id` field: