mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-20 18:34:04 +02:00
Qualify namespaced OpenAPI XML attributes (#598)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>' +
|
||||
|
||||
Reference in New Issue
Block a user