Qualify namespaced OpenAPI XML attributes (#598)

This commit is contained in:
Gregory Schier
2026-08-19 22:30:50 -07:00
committed by GitHub
parent 7db652db57
commit 95b1beffcf
2 changed files with 75 additions and 9 deletions
+46 -8
View File
@@ -1164,17 +1164,31 @@ function valueToXml(
}
if (isRecord(value)) {
const properties = toRecord(resolvedSchema.properties);
const entries = Object.entries(value).map(([name, propertyValue]) => {
const propertySchema = toRecord(importState.resolve(properties[name]));
return { name, propertyValue, propertySchema, xml: toRecord(propertySchema.xml) };
});
const usedPrefixes = new Set(["xml", "xmlns"]);
const prefixesByNamespace = new Map<string, string>();
for (const xml of [schemaXml, ...entries.map(({ xml }) => xml)]) {
const namespace = stringAt(xml, "namespace");
const prefix = stringAt(xml, "prefix");
if (prefix == null || prefix.length === 0) continue;
usedPrefixes.add(prefix);
if (namespace != null && namespace.length > 0 && !prefixesByNamespace.has(namespace)) {
prefixesByNamespace.set(namespace, prefix);
}
}
const attributes: string[] = [];
const attributeNamespaces: UnknownRecord[] = [];
const children: string[] = [];
for (const [name, propertyValue] of Object.entries(value)) {
const propertySchema = toRecord(importState.resolve(properties[name]));
const xml = toRecord(propertySchema.xml);
for (const { name, propertyValue, propertySchema, xml } of entries) {
if (xml.attribute === true) {
const attributeXml = qualifyXmlAttribute(xml, usedPrefixes, prefixesByNamespace);
attributes.push(
`${qualifiedXmlName(name, xml)}="${escapeXml(stringifyExampleValue(propertyValue))}"`,
`${qualifiedXmlName(name, attributeXml)}="${escapeXml(stringifyExampleValue(propertyValue))}"`,
);
attributeNamespaces.push(xml);
attributeNamespaces.push(attributeXml);
} else {
children.push(valueToXml(propertyValue, propertySchema, importState, name));
}
@@ -1195,9 +1209,9 @@ function xmlElement(
const namespaces = new Map<string, string>();
for (const metadata of [xml, ...additionalNamespaces]) {
const namespace = stringAt(metadata, "namespace");
if (namespace == null) continue;
if (namespace == null || namespace.length === 0) continue;
const prefix = stringAt(metadata, "prefix");
namespaces.set(prefix == null ? "xmlns" : `xmlns:${prefix}`, namespace);
namespaces.set(prefix == null || prefix.length === 0 ? "xmlns" : `xmlns:${prefix}`, namespace);
}
const namespaceAttributes = [...namespaces].map(
([attribute, namespace]) => `${attribute}="${escapeXml(namespace)}"`,
@@ -1207,10 +1221,34 @@ function xmlElement(
return `${openingTag}${content}</${name}>`;
}
function qualifyXmlAttribute(
xml: UnknownRecord,
usedPrefixes: Set<string>,
prefixesByNamespace: Map<string, string>,
): UnknownRecord {
const namespace = stringAt(xml, "namespace");
const declaredPrefix = stringAt(xml, "prefix");
if (namespace != null && namespace.length === 0) {
const { prefix: _prefix, ...unqualifiedXml } = xml;
return unqualifiedXml;
}
if (namespace == null || (declaredPrefix != null && declaredPrefix.length > 0)) return xml;
const existingPrefix = prefixesByNamespace.get(namespace);
if (existingPrefix != null) return { ...xml, prefix: existingPrefix };
let suffix = 1;
while (usedPrefixes.has(`ns${suffix}`)) suffix++;
const generatedPrefix = `ns${suffix}`;
usedPrefixes.add(generatedPrefix);
prefixesByNamespace.set(namespace, generatedPrefix);
return { ...xml, prefix: generatedPrefix };
}
function qualifiedXmlName(fallbackName: string, xml: UnknownRecord): string {
const name = stringAt(xml, "name") ?? fallbackName;
const prefix = stringAt(xml, "prefix");
return prefix == null ? name : `${prefix}:${name}`;
return prefix == null || prefix.length === 0 ? name : `${prefix}:${name}`;
}
function escapeXml(value: string): string {
+29 -1
View File
@@ -1043,6 +1043,31 @@ describe("importer-openapi", () => {
example: "42",
xml: { attribute: true, namespace: "urn:metadata", prefix: "m" },
},
externalId: {
type: "string",
example: "external",
xml: {
attribute: true,
name: "external-id",
namespace: "urn:external",
prefix: "ns1",
},
},
tenant: {
type: "string",
example: "acme",
xml: { attribute: true, namespace: "urn:tenant" },
},
region: {
type: "string",
example: "west",
xml: { attribute: true, namespace: "urn:tenant" },
},
legacy: {
type: "string",
example: "plain",
xml: { attribute: true, namespace: "", prefix: "unbound" },
},
tags: {
type: "array",
example: ["one", "two"],
@@ -1094,7 +1119,10 @@ describe("importer-openapi", () => {
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text:
'<c:catalog xmlns:c="urn:catalog" xmlns:m="urn:metadata" m:id="42">' +
'<c:catalog xmlns:c="urn:catalog" xmlns:m="urn:metadata" ' +
'xmlns:ns1="urn:external" xmlns:ns2="urn:tenant" ' +
'm:id="42" ns1:external-id="external" ns2:tenant="acme" ns2:region="west" ' +
'legacy="plain">' +
'<t:tags xmlns:t="urn:tags"><tag>one</tag><tag>two</tag></t:tags>' +
'<a:alias xmlns:a="urn:aliases">Ada</a:alias>' +
'<a:alias xmlns:a="urn:aliases">A</a:alias>' +