use std::collections::BTreeMap;
use std::fmt;
use reqwest::Method;
use serde::ser::{SerializeSeq, Serializer};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::config::ServiceBase;
use crate::{Client, Result};
#[derive(Clone)]
pub struct VisionVideosClient {
client: Client,
}
impl VisionVideosClient {
pub(crate) fn new(client: Client) -> Self {
Self { client }
}
pub async fn get(&self, args: &GetVideoArgs) -> Result<Video> {
self.client
.request(
ServiceBase::Vision,
Method::GET,
format!("/v1/videos/{}", args.video_id.as_str()),
)
.send_json()
.await
}
pub async fn list(&self, args: &ListVideosArgs) -> Result<Vec<Video>> {
let response: ListVideosResponse = self
.client
.request(ServiceBase::Vision, Method::GET, "/v1/videos")
.query(args)
.send_json()
.await?;
Ok(response.results)
}
pub async fn delete(&self, args: &DeleteVideoArgs) -> Result<DeleteVideoResponse> {
self.client
.request(
ServiceBase::Vision,
Method::DELETE,
format!("/v1/videos/{}", args.video_id.as_str()),
)
.send_json()
.await
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GetVideoArgs {
pub video_id: VideoId,
}
impl GetVideoArgs {
pub fn new(video_id: impl Into<VideoId>) -> Self {
Self {
video_id: video_id.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ListVideosArgs {
pub ids: Option<Vec<VideoId>>,
}
impl ListVideosArgs {
pub fn new() -> Self {
Self::default()
}
pub fn with_ids<I, V>(mut self, ids: I) -> Self
where
I: IntoIterator<Item = V>,
V: Into<VideoId>,
{
let ids = ids.into_iter().map(Into::into).collect::<Vec<_>>();
self.ids = (!ids.is_empty()).then_some(ids);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeleteVideoArgs {
pub video_id: VideoId,
}
impl DeleteVideoArgs {
pub fn new(video_id: impl Into<VideoId>) -> Self {
Self {
video_id: video_id.into(),
}
}
}
impl Serialize for ListVideosArgs {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let ids = self.ids.as_deref().unwrap_or(&[]);
let mut seq = serializer.serialize_seq(Some(ids.len()))?;
for video_id in ids {
seq.serialize_element(&("ids", video_id.as_str()))?;
}
seq.end()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Video {
pub video_id: VideoId,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub metadata: Option<VideoMetadata>,
#[serde(default)]
pub indexing_status: Option<String>,
#[serde(default)]
pub indexing_type: Option<String>,
#[serde(default)]
pub group_id: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct VideoMetadata {
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
#[serde(default)]
pub avg_fps: Option<f64>,
#[serde(default)]
pub video_name: Option<String>,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub video_start_timestamp_utc_ms: Option<i64>,
#[serde(default)]
pub duration: Option<f64>,
#[serde(default)]
pub thumbnail: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub source: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeleteVideoResponse {
pub status: String,
#[serde(default)]
pub message: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct VideoId(String);
impl VideoId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for VideoId {
fn from(value: &str) -> Self {
Self(value.to_owned())
}
}
impl From<String> for VideoId {
fn from(value: String) -> Self {
Self(value)
}
}
impl AsRef<str> for VideoId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for VideoId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
struct ListVideosResponse {
results: Vec<Video>,
#[serde(flatten)]
extra: BTreeMap<String, Value>,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{
DeleteVideoArgs, DeleteVideoResponse, GetVideoArgs, ListVideosArgs, ListVideosResponse,
Video, VideoId,
};
#[test]
fn video_id_round_trips_as_string() {
let video_id = VideoId::from("550e8400-e29b-41d4-a716-446655440000");
assert_eq!(video_id.as_str(), "550e8400-e29b-41d4-a716-446655440000");
assert_eq!(video_id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn typed_video_requests_wrap_video_ids() {
let get = GetVideoArgs::new("video-1");
let delete = DeleteVideoArgs::new("video-2");
assert_eq!(get.video_id, VideoId::from("video-1"));
assert_eq!(delete.video_id, VideoId::from("video-2"));
}
#[test]
fn list_videos_params_serializes_repeated_ids_query_parameters() {
let args = ListVideosArgs::new().with_ids(["video-1", "video-2"]);
let request = reqwest::Client::new()
.get("https://vision-agent.api.reka.ai/v1/videos")
.query(&args)
.build()
.expect("request should build");
assert_eq!(request.url().query(), Some("ids=video-1&ids=video-2"));
}
#[test]
fn video_deserializes_metadata_and_unknown_fields() {
let video: Video = serde_json::from_value(json!({
"video_id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://example.com/video.mp4",
"metadata": {
"width": 1920,
"height": 1080,
"avg_fps": 23.976,
"video_name": "demo.mp4",
"title": "Demo",
"video_start_timestamp_utc_ms": 1,
"duration": 641.939,
"thumbnail": "https://example.com/thumb.jpg",
"description": "Uploaded video file",
"source": "/tmp/demo.mp4",
"codec": "h264"
},
"indexing_status": "indexed",
"indexing_type": "fast_search",
"group_id": "default",
"owner": "test-user"
}))
.expect("video should deserialize");
assert_eq!(
video.video_id,
VideoId::from("550e8400-e29b-41d4-a716-446655440000")
);
assert_eq!(
video.metadata.as_ref().and_then(|metadata| metadata.width),
Some(1920)
);
assert_eq!(
video
.metadata
.as_ref()
.and_then(|metadata| metadata.extra.get("codec")),
Some(&json!("h264"))
);
assert_eq!(video.extra["owner"], "test-user");
}
#[test]
fn list_videos_response_deserializes_results_envelope() {
let response: ListVideosResponse = serde_json::from_value(json!({
"results": [
{
"video_id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://example.com/video.mp4"
}
],
"next_cursor": null
}))
.expect("list response should deserialize");
assert_eq!(response.results.len(), 1);
assert_eq!(
response.results[0].video_id,
VideoId::from("550e8400-e29b-41d4-a716-446655440000")
);
assert_eq!(response.extra["next_cursor"], json!(null));
}
#[test]
fn delete_video_response_deserializes_status_message() {
let response: DeleteVideoResponse = serde_json::from_value(json!({
"status": "success",
"message": "deleted"
}))
.expect("delete response should deserialize");
assert_eq!(response.status, "success");
assert_eq!(response.message.as_deref(), Some("deleted"));
}
}