use std::fmt;
use serde::{
de::{self, MapAccess, SeqAccess, Visitor},
Deserialize, Deserializer, Serialize,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Auth {
pub jwt: Option<JWT>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JWT {
pub location: Option<JWTLocationConfig>,
pub secret: String,
pub expiration: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "from")]
pub enum JWTLocation {
Bearer,
Query { name: String },
Cookie { name: String },
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum JWTLocationConfig {
Single(JWTLocation),
Multiple(Vec<JWTLocation>),
}
impl<'de> Deserialize<'de> for JWTLocationConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct LocationConfigVisitor;
impl<'de> Visitor<'de> for LocationConfigVisitor {
type Value = JWTLocationConfig;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
"a JWT location map such as `{from: Cookie, name: auth_token}`, or a list of \
them to try in order",
)
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
JWTLocation::deserialize(de::value::MapAccessDeserializer::new(map))
.map(JWTLocationConfig::Single)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
Vec::<JWTLocation>::deserialize(de::value::SeqAccessDeserializer::new(seq))
.map(JWTLocationConfig::Multiple)
}
}
deserializer.deserialize_any(LocationConfigVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(yaml: &str) -> Result<JWTLocationConfig, serde_yaml::Error> {
serde_yaml::from_str(yaml)
}
#[test]
fn a_map_is_a_single_location() {
let config = parse("from: Cookie\nname: auth_token").expect("a map should parse");
assert!(matches!(
config,
JWTLocationConfig::Single(JWTLocation::Cookie { name }) if name == "auth_token"
));
}
#[test]
fn a_list_is_a_fallback_chain() {
let config = parse("- from: Cookie\n name: auth_token\n- from: Bearer")
.expect("a list should parse");
let JWTLocationConfig::Multiple(locations) = config else {
panic!("a YAML list should deserialize to `Multiple`");
};
assert_eq!(locations.len(), 2);
}
#[test]
fn a_misspelled_source_names_the_accepted_values() {
let err = parse("from: cookie\nname: auth_token").expect_err("`cookie` is not a variant");
let message = err.to_string();
assert!(
message.contains("unknown variant") && message.contains("Cookie"),
"the error should name the accepted sources, got: {message}"
);
assert!(
!message.contains("untagged"),
"the opaque untagged message should be gone, got: {message}"
);
}
#[test]
fn a_cookie_without_a_name_says_which_field_is_missing() {
let err = parse("from: Cookie").expect_err("`Cookie` requires a `name`");
let message = err.to_string();
assert!(
message.contains("name"),
"the error should name the missing field, got: {message}"
);
assert!(
!message.contains("untagged"),
"the opaque untagged message should be gone, got: {message}"
);
}
#[test]
fn a_bare_scalar_describes_both_accepted_shapes() {
let err = parse("Bearer").expect_err("a bare string is not a location");
let message = err.to_string();
assert!(
message.contains("from: Cookie") && message.contains("list"),
"the error should describe the map and the list forms, got: {message}"
);
}
}