use serde::{Deserialize, Deserializer};
#[derive(Debug, Deserialize)]
pub struct ListFilter {
#[serde(default)]
pub status: Status,
#[serde(default)]
pub sort: SortField,
#[serde(default = "default_page")]
pub page: u32,
#[serde(default = "default_per_page")]
pub per_page: u32,
#[serde(default, deserialize_with = "parse_bool")]
pub include_archived: bool,
#[serde(default)]
pub q: Option<String>,
}
#[derive(Debug, Default, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Status {
#[default]
All,
Active,
Suspended,
}
#[derive(Debug, Default, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SortField {
#[default]
Created,
Updated,
Name,
}
fn default_page() -> u32 {
1
}
fn default_per_page() -> u32 {
25
}
fn parse_bool<'de, D>(de: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(de)?;
match raw.to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Ok(true),
"0" | "false" | "no" | "off" | "" => Ok(false),
other => Err(serde::de::Error::custom(format!("invalid bool: {other}"))),
}
}
use crate::filter::ListFilter;
use std::fmt;
const MAX_PER_PAGE: u32 = 100;
#[derive(Debug)]
pub enum FilterError {
Decode(String),
Invalid(String),
}
impl fmt::Display for FilterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FilterError::Decode(m) => write!(f, "could not parse query: {m}"),
FilterError::Invalid(m) => write!(f, "invalid filter: {m}"),
}
}
}
impl ListFilter {
pub fn from_query(qs: &str) -> Result<Self, FilterError> {
let mut filter: ListFilter = serde_urlencoded::from_str(qs)
.map_err(|e| FilterError::Decode(e.to_string()))?;
filter.validate()?;
Ok(filter)
}
fn validate(&mut self) -> Result<(), FilterError> {
if self.page == 0 {
return Err(FilterError::Invalid("page must be >= 1".into()));
}
if self.per_page == 0 {
return Err(FilterError::Invalid("per_page must be >= 1".into()));
}
if self.per_page > MAX_PER_PAGE {
self.per_page = MAX_PER_PAGE;
}
if let Some(q) = &self.q {
if q.trim().is_empty() {
self.q = None;
}
}
Ok(())
}
}
mod filter;
mod parse;
use filter::ListFilter;
fn main() {
let cases = [
"status=active&sort=name&per_page=500&include_archived=yes",
"q=%20%20&page=3",
"",
"sort=creatd",
];
for qs in cases {
match ListFilter::from_query(qs) {
Ok(filter) => println!("{qs:?} => {filter:?}"),
Err(err) => println!("{qs:?} => ERROR: {err}"),
}
}
}
This snippet shows how a raw URL query string is turned into a strongly typed filter struct, applying defaults for missing keys and validating ranges. The pattern is common in list/search endpoints where clients send ?status=active&sort=created&per_page=50 and the handler needs a well-formed, bounded set of options rather than a loose map of strings.
In filter.rs, ListFilter is the target struct. Instead of leaving fields raw, it uses serde attributes to bridge the gap between the wire format and Rust types. Every field carries a #[serde(default = "...")] pointing at a small free function like default_page or default_per_page, so a query string that omits a key still deserializes cleanly. The status and sort fields are modeled as enums (Status, SortField) with #[serde(rename_all = "lowercase")], which means an unknown value fails loudly instead of silently defaulting — the enum acts as a whitelist. Status also derives Default so its absence resolves to Status::All.
The key detail is deserialize_with = "parse_bool" on include_archived. Query strings are all strings, so "true", "1", and "yes" must be coerced to a real bool; parse_bool handles that and rejects anything else. This is the kind of tolerant-but-strict parsing real APIs need.
In parse.rs, ListFilter::from_query is the entry point. It leans on the serde_urlencoded crate, which deserializes application/x-www-form-urlencoded data — exactly what a query string is — directly into any Deserialize type. After decoding, validate runs to clamp or reject out-of-range values: per_page is capped at MAX_PER_PAGE rather than trusting the client, and page must be at least one. Clamping instead of erroring is a deliberate trade-off that keeps pagination robust against hostile input.
main.rs exercises the whole path with three representative query strings, including an empty one that exercises every default. Note the pitfall this design avoids: because enums reject unknown variants, a typo like sort=creatd returns a clear FilterError instead of quietly falling back, which callers can map to an HTTP 400. Reach for this approach whenever an endpoint accepts structured filters and correctness matters more than accepting anything.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
Share this code
Here's the card — post it anywhere.