Extract typed ListResponse struct to eliminate double serialization

Replace inline json!() wrappers with a shared ListResponse<T> struct
that serializes directly, avoiding the intermediate serde_json::Value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-05 00:46:11 -05:00
parent 14322f40c2
commit 4f1297572d
2 changed files with 20 additions and 5 deletions

View file

@ -11,7 +11,7 @@ use axum::Json;
use crate::error::ApiError;
use crate::jwt_auth::AuthenticatedService;
use crate::server::{AppState, PaginationParams};
use crate::server::{AppState, ListResponse, PaginationParams};
fn paginated_response<T: serde::Serialize>(items: Vec<T>, pagination: &PaginationParams) -> Response {
let limit = pagination.limit.clamp(1, 100) as usize;
@ -23,7 +23,7 @@ fn paginated_response<T: serde::Serialize>(items: Vec<T>, pagination: &Paginatio
}
fn list_response<T: serde::Serialize>(items: T) -> Response {
(StatusCode::OK, Json(json!({ "data": items, "meta": { "has_more": false } }))).into_response()
(StatusCode::OK, Json(ListResponse::new(items))).into_response()
}
// ── Runs ───────────────────────────────────────────────────────────────

View file

@ -12,7 +12,6 @@ use tokio::sync::broadcast;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::StreamExt;
use serde_json::json;
use tracing::{error, info};
use arc_agent::LocalSandbox;
@ -45,6 +44,22 @@ pub struct PaginationParams {
pub offset: u32,
}
/// Non-paginated list response wrapper with `has_more: false`.
#[derive(serde::Serialize)]
pub struct ListResponse<T: serde::Serialize> {
data: T,
meta: PaginationMeta,
}
impl<T: serde::Serialize> ListResponse<T> {
pub fn new(data: T) -> Self {
Self {
data,
meta: PaginationMeta { has_more: false },
}
}
}
/// Snapshot of a managed run.
struct ManagedRun {
dot_source: String,
@ -645,7 +660,7 @@ async fn get_questions(
Some(managed_run) => {
let interviewer = match &managed_run.interviewer {
Some(i) => i,
None => return (StatusCode::OK, Json(json!({ "data": Vec::<ApiQuestion>::new(), "meta": { "has_more": false } }))).into_response(),
None => return (StatusCode::OK, Json(ListResponse::new(Vec::<ApiQuestion>::new()))).into_response(),
};
let pending = interviewer.pending_questions();
let questions: Vec<ApiQuestion> = pending
@ -666,7 +681,7 @@ async fn get_questions(
allow_freeform: pq.question.allow_freeform,
})
.collect();
(StatusCode::OK, Json(json!({ "data": questions, "meta": { "has_more": false } }))).into_response()
(StatusCode::OK, Json(ListResponse::new(questions))).into_response()
}
None => ApiError::not_found("Run not found.").into_response(),
}