diff --git a/crates/api_clients/fal_client/src/webhook_api/mod.rs b/crates/api_clients/fal_client/src/webhook_api/mod.rs index 9bb83b48ba..df3d3e37b6 100644 --- a/crates/api_clients/fal_client/src/webhook_api/mod.rs +++ b/crates/api_clients/fal_client/src/webhook_api/mod.rs @@ -1,3 +1,4 @@ pub mod parse_webhook_inner_payload; pub mod parse_webhook_payload; -pub mod payload; \ No newline at end of file +pub mod payload; +pub mod success_case_extractors; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/webhook_api/parse_webhook_inner_payload.rs b/crates/api_clients/fal_client/src/webhook_api/parse_webhook_inner_payload.rs index 8231e1a710..6286411f67 100644 --- a/crates/api_clients/fal_client/src/webhook_api/parse_webhook_inner_payload.rs +++ b/crates/api_clients/fal_client/src/webhook_api/parse_webhook_inner_payload.rs @@ -1,8 +1,9 @@ use serde_json::Value; use crate::webhook_api::payload::webhook_error_type::WebhookErrorType; -use crate::webhook_api::payload::webhook_inner_payload::{ErrorData, ExtractedContents, PayloadErrorData, SuccessData, WebhookInnerPayload}; +use crate::webhook_api::payload::webhook_inner_payload::{ErrorData, PayloadErrorData, SuccessData, WebhookInnerPayload}; use crate::webhook_api::payload::webhook_payload::{WebhookPayload, WebhookStatus}; +use crate::webhook_api::success_case_extractors::extract_contents_from_payload; /// Parse the inner payload of a FAL webhook into one of three cases. /// @@ -28,18 +29,12 @@ pub fn parse_webhook_inner_payload(webhook: &WebhookPayload) -> WebhookInnerPayl }); } } - + let value = webhook.payload .clone() .unwrap_or(Value::Null); - - let mut extracted_contents = ExtractedContents { - image: None, - images: None, - video: None, - model_glb: None, - model_mesh: None, - }; + + let extracted_contents = extract_contents_from_payload(&value); WebhookInnerPayload::Success(SuccessData { payload: value, diff --git a/crates/api_clients/fal_client/src/webhook_api/payload/webhook_inner_payload.rs b/crates/api_clients/fal_client/src/webhook_api/payload/webhook_inner_payload.rs index c0c4c26dd6..74dab9fa17 100644 --- a/crates/api_clients/fal_client/src/webhook_api/payload/webhook_inner_payload.rs +++ b/crates/api_clients/fal_client/src/webhook_api/payload/webhook_inner_payload.rs @@ -28,7 +28,7 @@ pub struct SuccessData { /// If there are any extracted sub-payload contents, such as "images" or "video", /// then they are included here. This may not be fully inclusive of future /// payload types. - pub extracted_contents: ExtractedContents, + pub extracted_contents: Option, } #[derive(Debug)] diff --git a/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_image_payload.rs b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_image_payload.rs new file mode 100644 index 0000000000..0c312401dd --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_image_payload.rs @@ -0,0 +1,110 @@ +use serde_json::{Map, Value}; + +/// Extract the `image` key from a webhook success payload object. +/// Returns a clone of the value if the key exists. +pub fn extract_image(obj: &Map) -> Option { + obj.get("image").cloned() +} + +#[cfg(test)] +mod tests { + use crate::webhook_api::parse_webhook_inner_payload::parse_webhook_inner_payload; + use crate::webhook_api::payload::webhook_inner_payload::WebhookInnerPayload; + use crate::webhook_api::payload::webhook_payload::{WebhookPayload, WebhookStatus}; + + fn load_test_webhook(filename: &str) -> WebhookPayload { + let path = format!("test_data/webhooks/{}", filename); + let json = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e)); + serde_json::from_str(&json) + .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path, e)) + } + + #[test] + fn images_payload_populates_images_field() { + let webhook = load_test_webhook("success/images_payload_1.json"); + let result = parse_webhook_inner_payload(&webhook); + + let WebhookInnerPayload::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + // The raw payload should have "images". + assert!(data.payload.get("images").is_some()); + + // extracted_contents should be populated. + let contents = data.extracted_contents + .expect("extracted_contents should be Some for an images payload"); + + // The "images" field should be an array with 2 items. + let images = contents.images.expect("images should be Some"); + let images_arr = images.as_array().expect("images should be an array"); + assert_eq!(images_arr.len(), 2); + + // First image should have a url. + let first = &images_arr[0]; + assert_eq!( + first.get("url").and_then(|v| v.as_str()), + Some("https://v3b.fal.media/files/b/01234567/name1.png"), + ); + assert_eq!( + first.get("content_type").and_then(|v| v.as_str()), + Some("image/png"), + ); + + // Second image. + let second = &images_arr[1]; + assert_eq!( + second.get("url").and_then(|v| v.as_str()), + Some("https://v3b.fal.media/files/b/01234567/name2.png"), + ); + + // The "image" (singular) field should be None since the payload has "images" not "image". + assert!(contents.image.is_none()); + + // The video/model fields should be None. + assert!(contents.video.is_none()); + assert!(contents.model_glb.is_none()); + assert!(contents.model_mesh.is_none()); + } + + #[test] + fn payload_without_known_keys_has_none_extracted_contents() { + let webhook = WebhookPayload { + request_id: "test-no-keys".to_string(), + gateway_request_id: "test-no-keys".to_string(), + status: WebhookStatus::Ok, + error: None, + payload: Some(serde_json::json!({"some_other_key": "value"})), + payload_error: None, + }; + + let result = parse_webhook_inner_payload(&webhook); + + let WebhookInnerPayload::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + assert!(data.extracted_contents.is_none()); + } + + #[test] + fn null_payload_has_none_extracted_contents() { + let webhook = WebhookPayload { + request_id: "test-null".to_string(), + gateway_request_id: "test-null".to_string(), + status: WebhookStatus::Ok, + error: None, + payload: None, + payload_error: None, + }; + + let result = parse_webhook_inner_payload(&webhook); + + let WebhookInnerPayload::Success(data) = result else { + panic!("Expected Success, got {:?}", result); + }; + + assert!(data.extracted_contents.is_none()); + } +} diff --git a/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_images_payload.rs b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_images_payload.rs new file mode 100644 index 0000000000..a824165837 --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_images_payload.rs @@ -0,0 +1,7 @@ +use serde_json::{Map, Value}; + +/// Extract the `images` key from a webhook success payload object. +/// Returns a clone of the value if the key exists. +pub fn extract_images(obj: &Map) -> Option { + obj.get("images").cloned() +} diff --git a/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_model_glb_payload.rs b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_model_glb_payload.rs new file mode 100644 index 0000000000..7f0e266c98 --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_model_glb_payload.rs @@ -0,0 +1,7 @@ +use serde_json::{Map, Value}; + +/// Extract the `model_glb` key from a webhook success payload object. +/// Returns a clone of the value if the key exists. +pub fn extract_model_glb(obj: &Map) -> Option { + obj.get("model_glb").cloned() +} diff --git a/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_model_mesh_payload.rs b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_model_mesh_payload.rs new file mode 100644 index 0000000000..7a6f5edd8d --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_model_mesh_payload.rs @@ -0,0 +1,7 @@ +use serde_json::{Map, Value}; + +/// Extract the `model_mesh` key from a webhook success payload object. +/// Returns a clone of the value if the key exists. +pub fn extract_model_mesh(obj: &Map) -> Option { + obj.get("model_mesh").cloned() +} diff --git a/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_video_payload.rs b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_video_payload.rs new file mode 100644 index 0000000000..466fbef2af --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/extract_video_payload.rs @@ -0,0 +1,7 @@ +use serde_json::{Map, Value}; + +/// Extract the `video` key from a webhook success payload object. +/// Returns a clone of the value if the key exists. +pub fn extract_video(obj: &Map) -> Option { + obj.get("video").cloned() +} diff --git a/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/mod.rs b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/mod.rs new file mode 100644 index 0000000000..717d62271d --- /dev/null +++ b/crates/api_clients/fal_client/src/webhook_api/success_case_extractors/mod.rs @@ -0,0 +1,38 @@ +pub mod extract_image_payload; +pub mod extract_images_payload; +pub mod extract_video_payload; +pub mod extract_model_glb_payload; +pub mod extract_model_mesh_payload; + +use serde_json::Value; + +use crate::webhook_api::payload::webhook_inner_payload::ExtractedContents; + +/// Try to extract known content keys from a success payload. +/// +/// Checks the payload (as a JSON object) for any of: `image`, `images`, +/// `video`, `model_glb`, `model_mesh`. If at least one is found, returns +/// `Some(ExtractedContents)` with copies of the relevant values. Multiple +/// keys can be populated simultaneously. Returns `None` if the payload is +/// not an object or none of the known keys are present. +pub fn extract_contents_from_payload(payload: &Value) -> Option { + let obj = payload.as_object()?; + + let image = extract_image_payload::extract_image(obj); + let images = extract_images_payload::extract_images(obj); + let video = extract_video_payload::extract_video(obj); + let model_glb = extract_model_glb_payload::extract_model_glb(obj); + let model_mesh = extract_model_mesh_payload::extract_model_mesh(obj); + + if image.is_none() && images.is_none() && video.is_none() && model_glb.is_none() && model_mesh.is_none() { + return None; + } + + Some(ExtractedContents { + image, + images, + video, + model_glb, + model_mesh, + }) +}