mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-22 11:23:59 +02:00
Qualify namespaced OpenAPI XML attributes (#598)
This commit is contained in:
@@ -1164,17 +1164,31 @@ function valueToXml(
|
|||||||
}
|
}
|
||||||
if (isRecord(value)) {
|
if (isRecord(value)) {
|
||||||
const properties = toRecord(resolvedSchema.properties);
|
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 attributes: string[] = [];
|
||||||
const attributeNamespaces: UnknownRecord[] = [];
|
const attributeNamespaces: UnknownRecord[] = [];
|
||||||
const children: string[] = [];
|
const children: string[] = [];
|
||||||
for (const [name, propertyValue] of Object.entries(value)) {
|
for (const { name, propertyValue, propertySchema, xml } of entries) {
|
||||||
const propertySchema = toRecord(importState.resolve(properties[name]));
|
|
||||||
const xml = toRecord(propertySchema.xml);
|
|
||||||
if (xml.attribute === true) {
|
if (xml.attribute === true) {
|
||||||
|
const attributeXml = qualifyXmlAttribute(xml, usedPrefixes, prefixesByNamespace);
|
||||||
attributes.push(
|
attributes.push(
|
||||||
`${qualifiedXmlName(name, xml)}="${escapeXml(stringifyExampleValue(propertyValue))}"`,
|
`${qualifiedXmlName(name, attributeXml)}="${escapeXml(stringifyExampleValue(propertyValue))}"`,
|
||||||
);
|
);
|
||||||
attributeNamespaces.push(xml);
|
attributeNamespaces.push(attributeXml);
|
||||||
} else {
|
} else {
|
||||||
children.push(valueToXml(propertyValue, propertySchema, importState, name));
|
children.push(valueToXml(propertyValue, propertySchema, importState, name));
|
||||||
}
|
}
|
||||||
@@ -1195,9 +1209,9 @@ function xmlElement(
|
|||||||
const namespaces = new Map<string, string>();
|
const namespaces = new Map<string, string>();
|
||||||
for (const metadata of [xml, ...additionalNamespaces]) {
|
for (const metadata of [xml, ...additionalNamespaces]) {
|
||||||
const namespace = stringAt(metadata, "namespace");
|
const namespace = stringAt(metadata, "namespace");
|
||||||
if (namespace == null) continue;
|
if (namespace == null || namespace.length === 0) continue;
|
||||||
const prefix = stringAt(metadata, "prefix");
|
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(
|
const namespaceAttributes = [...namespaces].map(
|
||||||
([attribute, namespace]) => `${attribute}="${escapeXml(namespace)}"`,
|
([attribute, namespace]) => `${attribute}="${escapeXml(namespace)}"`,
|
||||||
@@ -1207,10 +1221,34 @@ function xmlElement(
|
|||||||
return `${openingTag}${content}</${name}>`;
|
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 {
|
function qualifiedXmlName(fallbackName: string, xml: UnknownRecord): string {
|
||||||
const name = stringAt(xml, "name") ?? fallbackName;
|
const name = stringAt(xml, "name") ?? fallbackName;
|
||||||
const prefix = stringAt(xml, "prefix");
|
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 {
|
function escapeXml(value: string): string {
|
||||||
|
|||||||
@@ -1043,6 +1043,31 @@ describe("importer-openapi", () => {
|
|||||||
example: "42",
|
example: "42",
|
||||||
xml: { attribute: true, namespace: "urn:metadata", prefix: "m" },
|
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: {
|
tags: {
|
||||||
type: "array",
|
type: "array",
|
||||||
example: ["one", "two"],
|
example: ["one", "two"],
|
||||||
@@ -1094,7 +1119,10 @@ describe("importer-openapi", () => {
|
|||||||
|
|
||||||
expect(imported?.resources.httpRequests[0]?.body).toEqual({
|
expect(imported?.resources.httpRequests[0]?.body).toEqual({
|
||||||
text:
|
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>' +
|
'<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">Ada</a:alias>' +
|
||||||
'<a:alias xmlns:a="urn:aliases">A</a:alias>' +
|
'<a:alias xmlns:a="urn:aliases">A</a:alias>' +
|
||||||
|
|||||||
Reference in New Issue
Block a user