From 568954348b4d857f9e871788b2b96984eb87f567 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sun, 30 Aug 2026 21:06:08 +0000 Subject: [PATCH] fix(adapters): reject unbound placeholders when from_jsonschema input_schema has no properties The eager placeholder-binding check in spec_name_references_undeclared short-circuited on an empty properties set, letting FromJsonSchema::new accept a template whose placeholder could never be bound (every later call failed INTERNAL, or INVALID_INPUT if a peer supplied the key). Drop the early return so any placeholder against a declared-nothing (or properties-less) input_schema is unbound, matching the from_openapi unbound_placeholders path. - add construction-time test: no-properties schema + {id} template fails with the placeholder error; no-properties schema + placeholder- free template still constructs and imports as Visibility::Internal Verification: scripts/verify.sh (342 passed), scripts/verify.sh --all-features (454 passed); clippy -D warnings and fmt --check clean --- src/adapters/from_jsonschema.rs | 55 +++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/adapters/from_jsonschema.rs b/src/adapters/from_jsonschema.rs index 760e3a1..b95b747 100644 --- a/src/adapters/from_jsonschema.rs +++ b/src/adapters/from_jsonschema.rs @@ -170,9 +170,6 @@ fn spec_name_references_undeclared(spec: &OperationSpec, path_template: &str) -> .and_then(|p| p.as_object()) .map(|p| p.keys().cloned().collect::>()) .unwrap_or_default(); - if properties.is_empty() { - return false; - } let mut rest = path_template; while let Some(start) = rest.find('{') { let Some(end_rel) = rest[start..].find('}') else { @@ -722,4 +719,56 @@ mod tests { Err(e) => panic!("expected SchemaParse, got {e}"), } } + + #[test] + fn construction_rejects_placeholder_when_input_schema_has_no_properties() { + let spec = OperationSpec::new( + "svc/getWidget", + OperationType::Query, + Visibility::Internal, + serde_json::json!({"type":"object"}), + serde_json::json!({"type":"object"}), + vec![], + AccessControl::default(), + None, + ); + let result = FromJsonSchema::new( + spec, + test_config("svc", "https://api.example.com"), + "/widgets/{id}".to_string(), + "GET".to_string(), + test_http_client(), + ); + match result { + Err(AdapterError::SchemaParse { message }) => { + assert!(message.contains("placeholder"), "message was: {message}"); + assert!(message.contains("{id}"), "message was: {message}"); + } + Ok(_) => { + panic!("no-properties schema with a placeholder template must fail at construction") + } + Err(e) => panic!("expected SchemaParse, got {e}"), + } + + let spec = OperationSpec::new( + "svc/listWidgets", + OperationType::Query, + Visibility::Internal, + serde_json::json!({"type":"object"}), + serde_json::json!({"type":"object"}), + vec![], + AccessControl::default(), + None, + ); + let adapter = FromJsonSchema::new( + spec, + test_config("svc", "https://api.example.com"), + "/widgets".to_string(), + "GET".to_string(), + test_http_client(), + ) + .expect("placeholder-free template with a no-properties schema builds"); + let bundles = futures::executor::block_on(adapter.import()).unwrap(); + assert_eq!(bundles[0].spec.visibility, Visibility::Internal); + } }