use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use serde::{Deserialize, Serialize};
use std::convert::Infallible;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SelectOption {
pub value: String,
pub label: String,
}
impl SelectOption {
pub fn new(value: impl ToString, label: impl Into<String>) -> Self {
Self {
value: value.to_string(),
label: label.into(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct OptionQuery {
pub q: String,
pub values: Vec<String>,
}
impl OptionQuery {
pub fn is_lookup(&self) -> bool {
!self.values.is_empty()
}
pub fn values_as<T: std::str::FromStr>(&self) -> Vec<T> {
self.values.iter().filter_map(|v| v.parse().ok()).collect()
}
pub fn parse(query: &str) -> Self {
let mut parsed = OptionQuery::default();
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
match key.as_ref() {
"q" => parsed.q = value.trim().to_owned(),
"values" | "values[]" if !value.trim().is_empty() && parsed.values.len() < 100 => {
parsed.values.push(value.trim().to_owned());
}
_ => {}
}
}
parsed
}
}
impl<S: Send + Sync> FromRequestParts<S> for OptionQuery {
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Infallible> {
Ok(OptionQuery::parse(parts.uri.query().unwrap_or_default()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_searches_and_lookups() {
let search = OptionQuery::parse("q=%20Iced%20Coffee%20");
assert_eq!(search.q, "Iced Coffee");
assert!(!search.is_lookup());
let lookup = OptionQuery::parse("values=3&values=&values%5B%5D=5&other=x");
assert_eq!(lookup.values, ["3", "5"]);
assert_eq!(
OptionQuery::parse("values=3&values=x").values_as::<i64>(),
[3]
);
assert!(lookup.is_lookup());
let many = OptionQuery::parse(&"values=1&".repeat(500));
assert_eq!(many.values.len(), 100);
}
#[test]
fn options_serialize_with_text_values() {
let json = serde_json::to_string(&SelectOption::new(3, "Coffee")).unwrap();
assert_eq!(json, r#"{"value":"3","label":"Coffee"}"#);
}
}