mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 18:49:38 +00:00
fix types to be nullable
This commit is contained in:
@@ -107,10 +107,13 @@ impl TaskStatus {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaResult {
|
||||
pub url: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// Frame width in pixels. `None` when the API returns `null`/omits it
|
||||
/// (it does so intermittently); callers should persist NULL, not a sentinel.
|
||||
pub maybe_width: Option<u32>,
|
||||
/// Frame height in pixels. `None` when the API returns `null`/omits it.
|
||||
pub maybe_height: Option<u32>,
|
||||
// NB: We don't need these.
|
||||
// /// Width / height ratio (e.g. 1.777… for 16:9). `None` when the server returns null (e.g. width/height are 0).
|
||||
// /// Width / height ratio (e.g. 1.777… for 16:9). `None` when the server returns null.
|
||||
// pub ratio: Option<f64>,
|
||||
}
|
||||
|
||||
@@ -232,8 +235,8 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
|
||||
result_url: o.result_url,
|
||||
results: o.results.into_iter().map(|r| MediaResult {
|
||||
url: r.url,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
maybe_width: r.maybe_width,
|
||||
maybe_height: r.maybe_height,
|
||||
}).collect(),
|
||||
fail_reason,
|
||||
created_at: o.created_at,
|
||||
@@ -422,7 +425,7 @@ mod tests {
|
||||
task_status,
|
||||
result_url: o.result_url,
|
||||
results: o.results.into_iter().map(|r| MediaResult {
|
||||
url: r.url, width: r.width, height: r.height,
|
||||
url: r.url, maybe_width: r.maybe_width, maybe_height: r.maybe_height,
|
||||
}).collect(),
|
||||
fail_reason,
|
||||
created_at: o.created_at,
|
||||
@@ -472,17 +475,17 @@ mod tests {
|
||||
let orders = parse_orders(body);
|
||||
assert_eq!(orders.len(), 1);
|
||||
assert_eq!(orders[0].results.len(), 1);
|
||||
assert_eq!(orders[0].results[0].width, 569);
|
||||
assert_eq!(orders[0].results[0].height, 1023);
|
||||
assert_eq!(orders[0].results[0].maybe_width, Some(569));
|
||||
assert_eq!(orders[0].results[0].maybe_height, Some(1023));
|
||||
assert_eq!(orders[0].total_credits, Some(13));
|
||||
}
|
||||
|
||||
/// Regression (paging outage): the Kinovi API started returning result
|
||||
/// `width`/`height` as `null`, which blew up the whole poll with
|
||||
/// "invalid type: null, expected a JSON number" and stalled the queue.
|
||||
/// They must coerce to `0` and let the rest of the response parse.
|
||||
/// They must parse as `None` and let the rest of the response parse.
|
||||
#[test]
|
||||
fn null_dimensions_are_coerced_to_zero() {
|
||||
fn null_dimensions_parse_as_none() {
|
||||
let body = r#"[{"result":{"data":{"json":{"orders":[{
|
||||
"orderId":"ord_foo",
|
||||
"resultUrl":"https://static.seedance2-pro.com/videos/seedance_video.mp4",
|
||||
@@ -495,11 +498,29 @@ mod tests {
|
||||
let orders = parse_orders(body);
|
||||
assert_eq!(orders.len(), 1);
|
||||
assert_eq!(orders[0].results.len(), 1);
|
||||
assert_eq!(orders[0].results[0].width, 0);
|
||||
assert_eq!(orders[0].results[0].height, 0);
|
||||
assert_eq!(orders[0].results[0].maybe_width, None);
|
||||
assert_eq!(orders[0].results[0].maybe_height, None);
|
||||
assert_eq!(orders[0].task_status, TaskStatus::Completed);
|
||||
}
|
||||
|
||||
/// Omitted `width`/`height` keys (not just `null`) also parse as `None`.
|
||||
#[test]
|
||||
fn missing_dimensions_parse_as_none() {
|
||||
let body = r#"[{"result":{"data":{"json":{"orders":[{
|
||||
"orderId":"ord_no_dims",
|
||||
"resultUrl":"https://example.com/v.mp4",
|
||||
"taskStatus":"COMPLETED",
|
||||
"results":[{"url":"https://example.com/v.mp4"}],
|
||||
"failReason":null,
|
||||
"createdAt":"2026-02-19T01:20:50.398Z",
|
||||
"mediaType":"video"
|
||||
}],"nextCursor":null}}}}]"#;
|
||||
let orders = parse_orders(body);
|
||||
assert_eq!(orders.len(), 1);
|
||||
assert_eq!(orders[0].results[0].maybe_width, None);
|
||||
assert_eq!(orders[0].results[0].maybe_height, None);
|
||||
}
|
||||
|
||||
/// Unrecognised mediaType values shouldn't crash deserialisation;
|
||||
/// they end up as `Unknown(_)`.
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use crate::utils::number_coercion::{
|
||||
de_opt_u32_int_or_float, de_opt_u64_int_or_float, de_u32_int_or_float,
|
||||
de_opt_u32_int_or_float, de_opt_u64_int_or_float,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -51,9 +51,11 @@ pub(super) struct RawOrder {
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct RawVideoResult {
|
||||
pub url: String,
|
||||
#[serde(deserialize_with = "de_u32_int_or_float")]
|
||||
pub width: u32,
|
||||
#[serde(deserialize_with = "de_u32_int_or_float")]
|
||||
pub height: u32,
|
||||
/// The API intermittently returns `null` (or omits) these dimensions, so
|
||||
/// they're optional rather than coerced to a sentinel.
|
||||
#[serde(rename = "width", default, deserialize_with = "de_opt_u32_int_or_float")]
|
||||
pub maybe_width: Option<u32>,
|
||||
#[serde(rename = "height", default, deserialize_with = "de_opt_u32_int_or_float")]
|
||||
pub maybe_height: Option<u32>,
|
||||
// pub ratio: Option<f64>,
|
||||
}
|
||||
|
||||
@@ -29,15 +29,13 @@ fn coerce_to_u32(number: &Number) -> u32 {
|
||||
}
|
||||
|
||||
/// `#[serde(deserialize_with = "...")]` for a required `u64` field that may
|
||||
/// arrive as an integer or a float — or, increasingly, as `null`. A `null`
|
||||
/// (or JSON `null`) coerces to `0` rather than failing the whole parse; the
|
||||
/// Kinovi API intermittently returns `null` for fields it used to populate.
|
||||
/// arrive as an integer or a float.
|
||||
pub fn de_u64_int_or_float<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let maybe_number = Option::<Number>::deserialize(deserializer)?;
|
||||
Ok(maybe_number.as_ref().map(coerce_to_u64).unwrap_or(0))
|
||||
let number = Number::deserialize(deserializer)?;
|
||||
Ok(coerce_to_u64(&number))
|
||||
}
|
||||
|
||||
/// `#[serde(deserialize_with = "...")]` for an optional `u64` field (handles
|
||||
@@ -61,15 +59,13 @@ where
|
||||
}
|
||||
|
||||
/// `#[serde(deserialize_with = "...")]` for a required `u32` field that may
|
||||
/// arrive as an integer or a float — or, increasingly, as `null`. A `null`
|
||||
/// coerces to `0` rather than failing the whole parse; the Kinovi API
|
||||
/// intermittently returns `null` for result `width`/`height`.
|
||||
/// arrive as an integer or a float.
|
||||
pub fn de_u32_int_or_float<'de, D>(deserializer: D) -> Result<u32, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let maybe_number = Option::<Number>::deserialize(deserializer)?;
|
||||
Ok(maybe_number.as_ref().map(coerce_to_u32).unwrap_or(0))
|
||||
let number = Number::deserialize(deserializer)?;
|
||||
Ok(coerce_to_u32(&number))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -130,15 +126,6 @@ mod tests {
|
||||
assert_eq!(parse_u64(r#"{"value": -5.5}"#), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_fields_coerce_null_to_zero() {
|
||||
// The Kinovi API intermittently returns `null` for numbers it used to
|
||||
// populate (e.g. result width/height). Required coercers must treat that
|
||||
// as 0 rather than failing the whole response parse.
|
||||
assert_eq!(parse_u32(r#"{"value": null}"#), 0);
|
||||
assert_eq!(parse_u64(r#"{"value": null}"#), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_handles_null_missing_and_values() {
|
||||
assert_eq!(serde_json::from_str::<OptU32Holder>(r#"{"value": null}"#).unwrap().value, None);
|
||||
|
||||
+2
-2
@@ -224,8 +224,8 @@ async fn download_and_store_one_image(
|
||||
.maybe_batch_generation_token(maybe_batch_token)
|
||||
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.maybe_frame_height(Some(result.height))
|
||||
.maybe_frame_width(Some(result.width))
|
||||
.maybe_frame_height(result.maybe_height)
|
||||
.maybe_frame_width(result.maybe_width)
|
||||
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.maybe_platform_type(job.maybe_platform_type)
|
||||
|
||||
+3
-2
@@ -118,8 +118,9 @@ pub async fn process_successful_video_job(
|
||||
);
|
||||
|
||||
// Optionally extract frame dimensions from the order results.
|
||||
let maybe_frame_width = order.results.first().map(|r| r.width);
|
||||
let maybe_frame_height = order.results.first().map(|r| r.height);
|
||||
// Persist the integer dimensions when the API provides them, NULL otherwise.
|
||||
let maybe_frame_width = order.results.first().and_then(|r| r.maybe_width);
|
||||
let maybe_frame_height = order.results.first().and_then(|r| r.maybe_height);
|
||||
|
||||
// Insert media file record.
|
||||
let media_file_result = MediaFileInsertBuilder::new()
|
||||
|
||||
Reference in New Issue
Block a user