mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-23 03:44:08 +02:00
fix(openapi): honor XML schema metadata
This commit is contained in:
@@ -1139,7 +1139,7 @@ function formatMediaTypeBody(
|
|||||||
) {
|
) {
|
||||||
return typeof example === "string"
|
return typeof example === "string"
|
||||||
? example
|
? example
|
||||||
: valueToXml(example, schema, importState, stringAt(toRecord(schema).xml, "name") ?? "root");
|
: valueToXml(example, schema, importState, "root");
|
||||||
}
|
}
|
||||||
return formatBodyText(example);
|
return formatBodyText(example);
|
||||||
}
|
}
|
||||||
@@ -1151,30 +1151,66 @@ function valueToXml(
|
|||||||
elementName: string,
|
elementName: string,
|
||||||
): string {
|
): string {
|
||||||
const resolvedSchema = toRecord(importState.resolve(schema));
|
const resolvedSchema = toRecord(importState.resolve(schema));
|
||||||
|
const schemaXml = toRecord(resolvedSchema.xml);
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const itemSchema = importState.resolve(resolvedSchema.items);
|
const itemSchema = importState.resolve(resolvedSchema.items);
|
||||||
const itemName = stringAt(toRecord(itemSchema).xml, "name") ?? "item";
|
const itemName =
|
||||||
|
stringAt(toRecord(itemSchema).xml, "name") ??
|
||||||
|
(schemaXml.wrapped === true ? stringAt(schemaXml, "name") ?? elementName : elementName);
|
||||||
const items = value.map((item) => valueToXml(item, itemSchema, importState, itemName)).join("");
|
const items = value.map((item) => valueToXml(item, itemSchema, importState, itemName)).join("");
|
||||||
return `<${elementName}>${items}</${elementName}>`;
|
return schemaXml.wrapped === true
|
||||||
|
? xmlElement(elementName, schemaXml, items)
|
||||||
|
: items;
|
||||||
}
|
}
|
||||||
if (isRecord(value)) {
|
if (isRecord(value)) {
|
||||||
const properties = toRecord(resolvedSchema.properties);
|
const properties = toRecord(resolvedSchema.properties);
|
||||||
const attributes: string[] = [];
|
const attributes: string[] = [];
|
||||||
|
const attributeNamespaces: UnknownRecord[] = [];
|
||||||
const children: string[] = [];
|
const children: string[] = [];
|
||||||
for (const [name, propertyValue] of Object.entries(value)) {
|
for (const [name, propertyValue] of Object.entries(value)) {
|
||||||
const propertySchema = toRecord(importState.resolve(properties[name]));
|
const propertySchema = toRecord(importState.resolve(properties[name]));
|
||||||
const xml = toRecord(propertySchema.xml);
|
const xml = toRecord(propertySchema.xml);
|
||||||
const xmlName = stringAt(xml, "name") ?? name;
|
|
||||||
if (xml.attribute === true) {
|
if (xml.attribute === true) {
|
||||||
attributes.push(`${xmlName}="${escapeXml(stringifyExampleValue(propertyValue))}"`);
|
attributes.push(
|
||||||
|
`${qualifiedXmlName(name, xml)}="${escapeXml(stringifyExampleValue(propertyValue))}"`,
|
||||||
|
);
|
||||||
|
attributeNamespaces.push(xml);
|
||||||
} else {
|
} else {
|
||||||
children.push(valueToXml(propertyValue, propertySchema, importState, xmlName));
|
children.push(valueToXml(propertyValue, propertySchema, importState, name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const attributeText = attributes.length > 0 ? ` ${attributes.join(" ")}` : "";
|
return xmlElement(elementName, schemaXml, children.join(""), attributes, attributeNamespaces);
|
||||||
return `<${elementName}${attributeText}>${children.join("")}</${elementName}>`;
|
|
||||||
}
|
}
|
||||||
return `<${elementName}>${escapeXml(stringifyExampleValue(value))}</${elementName}>`;
|
return xmlElement(elementName, schemaXml, escapeXml(stringifyExampleValue(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function xmlElement(
|
||||||
|
fallbackName: string,
|
||||||
|
xml: UnknownRecord,
|
||||||
|
content: string,
|
||||||
|
attributes: string[] = [],
|
||||||
|
additionalNamespaces: UnknownRecord[] = [],
|
||||||
|
): string {
|
||||||
|
const name = qualifiedXmlName(fallbackName, xml);
|
||||||
|
const namespaces = new Map<string, string>();
|
||||||
|
for (const metadata of [xml, ...additionalNamespaces]) {
|
||||||
|
const namespace = stringAt(metadata, "namespace");
|
||||||
|
if (namespace == null) continue;
|
||||||
|
const prefix = stringAt(metadata, "prefix");
|
||||||
|
namespaces.set(prefix == null ? "xmlns" : `xmlns:${prefix}`, namespace);
|
||||||
|
}
|
||||||
|
const namespaceAttributes = [...namespaces].map(
|
||||||
|
([attribute, namespace]) => `${attribute}="${escapeXml(namespace)}"`,
|
||||||
|
);
|
||||||
|
const attributeText = [...namespaceAttributes, ...attributes].join(" ");
|
||||||
|
const openingTag = attributeText.length > 0 ? `<${name} ${attributeText}>` : `<${name}>`;
|
||||||
|
return `${openingTag}${content}</${name}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function qualifiedXmlName(fallbackName: string, xml: UnknownRecord): string {
|
||||||
|
const name = stringAt(xml, "name") ?? fallbackName;
|
||||||
|
const prefix = stringAt(xml, "prefix");
|
||||||
|
return prefix == null ? name : `${prefix}:${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeXml(value: string): string {
|
function escapeXml(value: string): string {
|
||||||
|
|||||||
@@ -1023,6 +1023,68 @@ describe("importer-openapi", () => {
|
|||||||
expect(imported?.resources.httpRequests[1]?.body).toEqual({ text: '"hello"' });
|
expect(imported?.resources.httpRequests[1]?.body).toEqual({ text: '"hello"' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Honors XML array wrapping and namespaces", async () => {
|
||||||
|
const imported = await convertOpenApi(
|
||||||
|
JSON.stringify({
|
||||||
|
openapi: "3.0.4",
|
||||||
|
info: { title: "XML Metadata Test", version: "1.0.0" },
|
||||||
|
paths: {
|
||||||
|
"/catalog": {
|
||||||
|
post: {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/xml": {
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
xml: { name: "catalog", namespace: "urn:catalog", prefix: "c" },
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
example: "42",
|
||||||
|
xml: { attribute: true, namespace: "urn:metadata", prefix: "m" },
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: "array",
|
||||||
|
example: ["one", "two"],
|
||||||
|
xml: {
|
||||||
|
name: "tags",
|
||||||
|
namespace: "urn:tags",
|
||||||
|
prefix: "t",
|
||||||
|
wrapped: true,
|
||||||
|
},
|
||||||
|
items: { type: "string", xml: { name: "tag" } },
|
||||||
|
},
|
||||||
|
aliases: {
|
||||||
|
type: "array",
|
||||||
|
example: ["Ada", "A"],
|
||||||
|
xml: { name: "ignored", wrapped: false },
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
xml: { name: "alias", namespace: "urn:aliases", prefix: "a" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(imported?.resources.httpRequests[0]?.body).toEqual({
|
||||||
|
text:
|
||||||
|
'<c:catalog xmlns:c="urn:catalog" xmlns:m="urn:metadata" m:id="42">' +
|
||||||
|
'<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>' +
|
||||||
|
"</c:catalog>",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("Omits read-only properties from generated request bodies", async () => {
|
test("Omits read-only properties from generated request bodies", async () => {
|
||||||
const imported = await convertOpenApi(
|
const imported = await convertOpenApi(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
Reference in New Issue
Block a user