diff --git a/crates/impossible-ocr-domain/src/lib.rs b/crates/impossible-ocr-domain/src/lib.rs index 08e588f..8df06c6 100644 --- a/crates/impossible-ocr-domain/src/lib.rs +++ b/crates/impossible-ocr-domain/src/lib.rs @@ -294,7 +294,10 @@ pub enum OcrLanguage { } /// Caller-controlled OCR behavior within the v0.1 contract. +/// +/// Every field is optional on the wire; omitted fields take their [`Default`] values. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(default)] pub struct OcrOptions { /// Requested language contract. pub language: OcrLanguage, diff --git a/crates/impossible-ocr-server/src/lib.rs b/crates/impossible-ocr-server/src/lib.rs index 2035245..21dc7d5 100644 --- a/crates/impossible-ocr-server/src/lib.rs +++ b/crates/impossible-ocr-server/src/lib.rs @@ -2214,6 +2214,36 @@ mod tests { Ok(()) } + #[tokio::test] + async fn partial_json_options_use_defaults_for_omitted_fields() + -> Result<(), Box> { + for (options, expected_words) in [ + (json!({}), 1), + (json!({"include_words": false}), 0), + (json!({"minimum_confidence": 0.5}), 1), + (json!({"language": "english"}), 1), + ] { + let mut request = request_json(); + request["options"] = options; + let response = test_server() + .router() + .oneshot( + Request::post(http::OCR_ROUTE) + .header("content-type", "application/json") + .body(Body::from(serde_json::to_vec(&request)?))?, + ) + .await?; + assert_eq!(response.status(), StatusCode::OK); + let body: Value = + serde_json::from_slice(&response.into_body().collect().await?.to_bytes())?; + let words = body["result"]["pages"][0]["blocks"][0]["lines"][0]["words"] + .as_array() + .map(Vec::len); + assert_eq!(words, Some(expected_words)); + } + Ok(()) + } + #[tokio::test] async fn malformed_oversized_and_poisoned_batches_fail_before_backend() -> Result<(), Box> {