Skip to content

Commit df04e3b

Browse files
committed
fix(evidence): reject unsupported path bindings before writing
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
1 parent 73904a4 commit df04e3b

3 files changed

Lines changed: 77 additions & 0 deletions

File tree

‎crates/registry-evidencectl/src/suggest/mod.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,28 @@ fn validate_required_inputs(
10261026
body: Option<&types::OperationRequestBody>,
10271027
mappings: &[RequestMapping],
10281028
) -> Result<()> {
1029+
let mut placeholders = BTreeSet::new();
1030+
for segment in operation.path.split('/') {
1031+
if !segment.contains(['{', '}']) {
1032+
continue;
1033+
}
1034+
let name = segment
1035+
.strip_prefix('{')
1036+
.and_then(|segment| segment.strip_suffix('}'))
1037+
.ok_or_else(|| anyhow!("a path placeholder must occupy one complete segment"))?;
1038+
registry_platform_yaml::LocalId::new(name).with_context(|| {
1039+
format!("path parameter `{name}` must be a Registry Stack LocalId for a path binding")
1040+
})?;
1041+
if !placeholders.insert(name) {
1042+
bail!("path placeholders must be unique: `{name}` is repeated");
1043+
}
1044+
if !mappings
1045+
.iter()
1046+
.any(|mapping| mapping.target == RequestTarget::Path(name.to_owned()))
1047+
{
1048+
bail!("required request input `path:{name}` has no --map-selector assignment");
1049+
}
1050+
}
10291051
for parameter in parameters.iter().filter(|parameter| parameter.required) {
10301052
let target = match parameter.location {
10311053
ParameterLocation::Path => RequestTarget::Path(parameter.name.clone()),

‎crates/registry-evidencectl/tests/suggest_e2e.rs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,59 @@ fn mapped_get_and_post_projects_pass_the_offline_compiler() {
237237
}
238238
}
239239

240+
#[test]
241+
fn unsupported_path_placeholders_are_refused_without_mutation() {
242+
for name in [
243+
"RecordId".to_owned(),
244+
"1record".to_owned(),
245+
"record.id".to_owned(),
246+
"récord".to_owned(),
247+
"r".repeat(65),
248+
] {
249+
let document = request_openapi(
250+
"GET",
251+
&format!("/records/{{{name}}}"),
252+
vec![parameter(&name, "path", true, "string")],
253+
None,
254+
);
255+
let workspace = tempfile::tempdir().expect("tempdir");
256+
let project = scaffold(workspace.path(), &document);
257+
assert_mapping_failure_is_atomic(
258+
&project,
259+
&[format!(
260+
"subject.subject-reference-v1.reference=path:{name}"
261+
)],
262+
"must be a Registry Stack LocalId",
263+
&format!("unsupported path placeholder {name}"),
264+
);
265+
}
266+
for (path, parameters, mapping, expected) in [
267+
(
268+
"/records/{record_id}/{record_id}",
269+
vec![parameter("record_id", "path", true, "string")],
270+
"subject.subject-reference-v1.reference=path:record_id",
271+
"path placeholders must be unique",
272+
),
273+
(
274+
"/records/{missing}",
275+
vec![parameter("reference", "query", true, "string")],
276+
"subject.subject-reference-v1.reference=query:reference",
277+
"path:missing",
278+
),
279+
(
280+
"/records/prefix-{record_id}",
281+
vec![parameter("reference", "query", true, "string")],
282+
"subject.subject-reference-v1.reference=query:reference",
283+
"path placeholder must occupy one complete segment",
284+
),
285+
] {
286+
let document = request_openapi("GET", path, parameters, None);
287+
let workspace = tempfile::tempdir().expect("tempdir");
288+
let project = scaffold(workspace.path(), &document);
289+
assert_mapping_failure_is_atomic(&project, &[mapping.to_owned()], expected, path);
290+
}
291+
}
292+
240293
#[test]
241294
fn invalid_selector_mappings_refuse_before_mutating_the_project() {
242295
struct Case {

‎products/evidence/reference/authoring-projects/CONFIG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ target connection.
167167
The planner checks effective required inputs after operation overrides; optional
168168
inputs are inspected only when you map them. Parameter and request-body
169169
references must resolve locally so their requiredness is known.
170+
Every path placeholder must occupy a unique complete segment, have a mapping,
171+
and use a Registry Stack local identifier (`[a-z][a-z0-9_-]{0,63}`).
170172

171173
Scalar types must agree, and the selector's admitted domain must fit the
172174
supported string-length or integer-range constraints. Integer formats `int32`

0 commit comments

Comments
 (0)