feat: implement custom deserializer for path segments with format

extension
episode-actions
Jef Roosens 2025-02-24 13:24:23 +01:00
parent 73928e78f4
commit 3c4af12fa1
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
3 changed files with 79 additions and 13 deletions

View File

@ -6,11 +6,12 @@ use axum::{
}; };
use crate::{ use crate::{
db::{self, User}, db,
server::{ server::{
error::{AppError, AppResult}, error::{AppError, AppResult},
gpodder::{ gpodder::{
auth_middleware, auth_middleware,
format::{Format, StringWithFormat},
models::{Device, DevicePatch, DeviceType}, models::{Device, DevicePatch, DeviceType},
}, },
Context, Context,
@ -26,14 +27,14 @@ pub fn router(ctx: Context) -> Router<Context> {
async fn get_devices( async fn get_devices(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(username): Path<String>, Path(username): Path<StringWithFormat>,
Extension(user): Extension<User>, Extension(user): Extension<db::User>,
) -> AppResult<Json<Vec<Device>>> { ) -> AppResult<Json<Vec<Device>>> {
// Check suffix is present and return 404 otherwise; axum doesn't support matching part of a if username.format != Format::Json {
// route segment return Err(AppError::NotFound);
let username = username.strip_suffix(".json").ok_or(AppError::NotFound)?; }
if username != user.username { if *username != user.username {
return Err(AppError::BadRequest); return Err(AppError::BadRequest);
} }
@ -55,14 +56,13 @@ async fn get_devices(
async fn post_device( async fn post_device(
State(ctx): State<Context>, State(ctx): State<Context>,
Path((_username, id)): Path<(String, String)>, Path((_username, id)): Path<(String, StringWithFormat)>,
Extension(user): Extension<User>, Extension(user): Extension<db::User>,
Json(patch): Json<DevicePatch>, Json(patch): Json<DevicePatch>,
) -> AppResult<()> { ) -> AppResult<()> {
let id = id if id.format != Format::Json {
.strip_suffix(".json") return Err(AppError::NotFound);
.ok_or(AppError::NotFound)? }
.to_string();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
if let Some(mut device) = db::Device::by_device_id(&ctx.pool, user.id, &id)? { if let Some(mut device) = db::Device::by_device_id(&ctx.pool, user.id, &id)? {

View File

@ -0,0 +1,65 @@
use std::ops::Deref;
use serde::{
de::{value::StrDeserializer, Visitor},
Deserialize,
};
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Format {
Json,
OPML,
Plaintext,
}
#[derive(Debug)]
pub struct StringWithFormat {
pub s: String,
pub format: Format,
}
impl Deref for StringWithFormat {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.s
}
}
impl<'de> Deserialize<'de> for StringWithFormat {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct StrVisitor;
impl<'de> Visitor<'de> for StrVisitor {
type Value = StringWithFormat;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(
"`text.ext` format, with `ext` being one of `json`, `opml` or `plaintext`",
)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if let Some((text, ext)) = v.rsplit_once('.') {
let format = Format::deserialize(StrDeserializer::new(ext))?;
Ok(StringWithFormat {
s: text.to_string(),
format,
})
} else {
Err(E::custom(format!("invalid format '{}'", v)))
}
}
}
deserializer.deserialize_str(StrVisitor)
}
}

View File

@ -1,4 +1,5 @@
mod advanced; mod advanced;
mod format;
mod models; mod models;
mod simple; mod simple;