fix(adapters): hoist path-template validation into shared forward core, run at from_openapi import (JS-02)

Import-time path-template checks now run on both adapters:
- from_jsonschema: validate_path_template moves to forward.rs (same
  checks, same messages, shared implementation)
- from_openapi: build_registration calls it before building each op, so
  a template like /x{open fails import with 'unterminated placeholder'
  instead of a per-call INTERNAL on first invoke
- new test: unterminated_path_template_fails_import_not_first_call
This commit is contained in:
2026-08-31 00:39:54 +00:00
parent d9971e9fad
commit 9a5b4e7936
3 changed files with 60 additions and 27 deletions
+1 -25
View File
@@ -32,7 +32,7 @@ use async_trait::async_trait;
use reqwest::Method;
use serde_json::Value;
use super::forward::{forward, forward_stream, HttpServiceConfig};
use super::forward::{forward, forward_stream, validate_path_template, HttpServiceConfig};
use crate::client::SharedHttpClient;
/// The HTTP-backed single-endpoint adapter (ADR-066): one caller-built
@@ -139,30 +139,6 @@ fn validate_method(method: &str) -> Result<(), AdapterError> {
})?;
Ok(())
}
fn validate_path_template(path_template: &str) -> Result<(), AdapterError> {
let mut rest = path_template;
while let Some(start) = rest.find('{') {
let Some(end_rel) = rest[start..].find('}') else {
return Err(AdapterError::SchemaParse {
message: format!("path template `{path_template}` has an unterminated placeholder"),
});
};
if rest[start + 1..start + end_rel].is_empty() {
return Err(AdapterError::SchemaParse {
message: format!("path template `{path_template}` has an empty placeholder name"),
});
}
rest = &rest[start + end_rel + 1..];
}
if path_template.contains('}') && !path_template.contains('{') {
return Err(AdapterError::SchemaParse {
message: format!("path template `{path_template}` has `}}` without a matching `{{`"),
});
}
Ok(())
}
fn spec_name_references_undeclared(spec: &OperationSpec, path_template: &str) -> bool {
let properties = spec
.input_schema