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
This commit is contained in:
@@ -170,9 +170,6 @@ fn spec_name_references_undeclared(spec: &OperationSpec, path_template: &str) ->
|
|||||||
.and_then(|p| p.as_object())
|
.and_then(|p| p.as_object())
|
||||||
.map(|p| p.keys().cloned().collect::<Vec<_>>())
|
.map(|p| p.keys().cloned().collect::<Vec<_>>())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if properties.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let mut rest = path_template;
|
let mut rest = path_template;
|
||||||
while let Some(start) = rest.find('{') {
|
while let Some(start) = rest.find('{') {
|
||||||
let Some(end_rel) = rest[start..].find('}') else {
|
let Some(end_rel) = rest[start..].find('}') else {
|
||||||
@@ -722,4 +719,56 @@ mod tests {
|
|||||||
Err(e) => panic!("expected SchemaParse, got {e}"),
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user