Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/const-is-a-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'fiber': patch
---

Read `const` when building a body from a schema.

OpenAPI 3.1 uses JSON Schema 2020-12, where a literal is written as `const` and a literal union as `anyOf: [{const: "once"}, {const: "always"}]`. That is what 3.1 generators emit where 3.0 would have written an `enum`. Fiber read `example`, `default` and `enum`, but not `const` — so it walked into the first branch, found nothing to go on, and printed the `string` placeholder for a field whose only legal values were named right there. Sending that body back got it rejected by the very document it came from.

One real spec this was found against writes 543 `const`s and not a single `enum`, so the existing `enum` handling never fired once across 658 paths. A `const` is now taken as the value it names, in request skeletons and in form fields alike.
71 changes: 70 additions & 1 deletion src-tauri/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,16 @@ fn example_string(schema: &serde_json::Value) -> String {
schema
.get("example")
.or_else(|| schema.get("default"))
// Same reasoning as `skeleton`: a `const` is the value, stated. A form
// field whose schema pins it to one string should arrive holding it
// rather than empty.
.or_else(|| schema.get("const"))
.or_else(|| {
schema
.get("enum")
.and_then(|it| it.as_array())
.and_then(|it| it.first())
})
.map(|value| match value {
serde_json::Value::String(text) => text.clone(),
other => other.to_string(),
Expand Down Expand Up @@ -597,7 +607,15 @@ fn skeleton(
};

// Anything the document states outright beats anything we invent.
for key in ["example", "default"] {
//
// `const` is JSON Schema's single-value `enum`, and OpenAPI 3.1 documents
// lean on it heavily: a literal union is written as
// `anyOf: [{const: "once"}, {const: "always"}]`, which is what most 3.1
// generators emit where 3.0 would have written an `enum`. Missing it meant
// walking into the first branch, finding nothing to go on, and printing the
// `string` placeholder for a field whose only legal values were right
// there — so the body came back rejected by the very schema it came from.
for key in ["example", "default", "const"] {
if let Some(value) = object.get(key) {
return value.clone();
}
Expand Down Expand Up @@ -1100,6 +1118,57 @@ paths:
assert!(body.contains("\"anything\": unknown"), "{body}");
}

/// How OpenAPI 3.1 writes a literal union — and 3.0's `enum` never appears
/// in documents that do. Emitting the `string` placeholder for a field
/// whose only legal values are named right there produced a body the
/// document's own validator rejected.
#[test]
fn a_const_is_the_value_it_names() {
let spec = r##"{
"openapi": "3.1.1",
"info": { "title": "Acme", "version": "1" },
"paths": {
"/agent/deny-tool-call": {
"post": {
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["agentId", "seq", "scope"],
"properties": {
"agentId": { "type": "string" },
"seq": { "type": "integer", "minimum": 1 },
"scope": {
"anyOf": [
{ "type": "string", "const": "once" },
{ "type": "string", "const": "always" }
]
},
"kind": { "const": "denial" }
}
}
}
}
}
}
}
}
}"##;

let import = parse(spec).unwrap();
let body = &import.endpoints[0].body;

// The first branch of the choice, and its value rather than its type.
assert!(body.contains("\"scope\": \"once\""), "{body}");
// A bare `const`, with no choice around it.
assert!(body.contains("\"kind\": \"denial\""), "{body}");
// Everything else still shows what to type.
assert!(body.contains("\"agentId\": string"), "{body}");
assert!(body.contains("\"seq\": integer"), "{body}");
}

/// The other spelling of nullable, and the one real specs in the wild use:
/// a choice with a `null` branch in it. Which side it sits on is arbitrary,
/// so the branch that says something wins either way.
Expand Down
Loading