feat: implement custom deserializer for path segments with format
extensionepisode-actions
parent
73928e78f4
commit
3c4af12fa1
|
@ -6,11 +6,12 @@ use axum::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
db::{self, User},
|
||||
db,
|
||||
server::{
|
||||
error::{AppError, AppResult},
|
||||
gpodder::{
|
||||
auth_middleware,
|
||||
format::{Format, StringWithFormat},
|
||||
models::{Device, DevicePatch, DeviceType},
|
||||
},
|
||||
Context,
|
||||
|
@ -26,14 +27,14 @@ pub fn router(ctx: Context) -> Router<Context> {
|
|||
|
||||
async fn get_devices(
|
||||
State(ctx): State<Context>,
|
||||
Path(username): Path<String>,
|
||||
Extension(user): Extension<User>,
|
||||
Path(username): Path<StringWithFormat>,
|
||||
Extension(user): Extension<db::User>,
|
||||
) -> AppResult<Json<Vec<Device>>> {
|
||||
// Check suffix is present and return 404 otherwise; axum doesn't support matching part of a
|
||||
// route segment
|
||||
let username = username.strip_suffix(".json").ok_or(AppError::NotFound)?;
|
||||
if username.format != Format::Json {
|
||||
return Err(AppError::NotFound);
|
||||
}
|
||||
|
||||
if username != user.username {
|
||||
if *username != user.username {
|
||||
return Err(AppError::BadRequest);
|
||||
}
|
||||
|
||||
|
@ -55,14 +56,13 @@ async fn get_devices(
|
|||
|
||||
async fn post_device(
|
||||
State(ctx): State<Context>,
|
||||
Path((_username, id)): Path<(String, String)>,
|
||||
Extension(user): Extension<User>,
|
||||
Path((_username, id)): Path<(String, StringWithFormat)>,
|
||||
Extension(user): Extension<db::User>,
|
||||
Json(patch): Json<DevicePatch>,
|
||||
) -> AppResult<()> {
|
||||
let id = id
|
||||
.strip_suffix(".json")
|
||||
.ok_or(AppError::NotFound)?
|
||||
.to_string();
|
||||
if id.format != Format::Json {
|
||||
return Err(AppError::NotFound);
|
||||
}
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Some(mut device) = db::Device::by_device_id(&ctx.pool, user.id, &id)? {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
mod advanced;
|
||||
mod format;
|
||||
mod models;
|
||||
mod simple;
|
||||
|
||||
|
|
Loading…
Reference in New Issue