diff --git a/plugins/importer-openapi/src/index.ts b/plugins/importer-openapi/src/index.ts
index 49658049..656562f1 100644
--- a/plugins/importer-openapi/src/index.ts
+++ b/plugins/importer-openapi/src/index.ts
@@ -809,14 +809,13 @@ function importBody({
(c): c is string => typeof c === "string",
);
const bodyType = contentType ?? "application/json";
+ const schema = importState.resolve(bodyParameter.schema);
+ const example = schemaToExample(schema, importState);
+ const isBinary = stringAt(schema, "format") === "binary";
return {
headers: [{ enabled: true, name: "Content-Type", value: bodyType }],
- bodyType,
- body: {
- text: formatBodyText(
- schemaToExample(importState.resolve(bodyParameter.schema), importState),
- ),
- },
+ bodyType: isBinary ? "binary" : bodyType,
+ body: isBinary ? {} : { text: formatMediaTypeBody(bodyType, example, schema, importState) },
};
}
@@ -834,11 +833,15 @@ function importBody({
headers: [{ enabled: true, name: "Content-Type", value: contentType }],
bodyType: contentType,
body: {
- form: formParameters.map((p) => ({
- enabled: p.required === true,
- name: stringAt(p, "name") ?? "",
- value: parameterExample(p, importState),
- })),
+ form: formParameters.map((p) => {
+ const base = {
+ enabled: p.required === true,
+ name: stringAt(p, "name") ?? "",
+ };
+ return stringAt(p, "type") === "file"
+ ? { ...base, file: "" }
+ : { ...base, value: parameterExample(p, importState) };
+ }),
},
};
}
@@ -861,45 +864,125 @@ function importBodyFromContent(importState: ImportState, content: UnknownRecord)
headers: [{ enabled: true, name: "Content-Type", value: contentType }],
bodyType: contentType,
body: {
- form: schemaToFormParameters(importState.resolve(mediaType.schema), importState),
+ form: schemaToFormParameters(
+ importState.resolve(mediaType.schema),
+ importState,
+ isRecord(example) ? example : undefined,
+ ),
},
};
}
+ const schema = importState.resolve(mediaType.schema);
+ const isBinary =
+ contentType === "application/octet-stream" || stringAt(schema, "format") === "binary";
+
return {
headers: [{ enabled: true, name: "Content-Type", value: contentType }],
- bodyType: contentType === "application/octet-stream" ? "binary" : contentType,
- body: contentType === "application/octet-stream" ? {} : { text: formatBodyText(example) },
+ bodyType: isBinary ? "binary" : contentType,
+ body: isBinary ? {} : { text: formatMediaTypeBody(contentType, example, schema, importState) },
};
}
function chooseContentType(contentTypes: string[]): string | null {
+ const jsonType = contentTypes.find((contentType) => {
+ const normalized = contentType.toLowerCase().split(";", 1)[0];
+ return normalized?.endsWith("+json") === true;
+ });
for (const preference of BODY_CONTENT_TYPE_PREFERENCE) {
const exact = contentTypes.find((c) => c.toLowerCase() === preference);
if (exact != null) return exact;
+ if (preference === "application/json" && jsonType != null) return jsonType;
}
return contentTypes[0] ?? null;
}
+function formatMediaTypeBody(
+ contentType: string,
+ example: unknown,
+ schema: unknown,
+ importState: ImportState,
+): string {
+ const normalized = contentType.toLowerCase().split(";", 1)[0] ?? "";
+ if (normalized === "application/json" || normalized.endsWith("+json")) {
+ return JSON.stringify(example, null, 2) ?? "";
+ }
+ if (
+ normalized === "application/xml" ||
+ normalized === "text/xml" ||
+ normalized.endsWith("+xml")
+ ) {
+ return typeof example === "string"
+ ? example
+ : valueToXml(example, schema, importState, stringAt(toRecord(schema).xml, "name") ?? "root");
+ }
+ return formatBodyText(example);
+}
+
+function valueToXml(
+ value: unknown,
+ schema: unknown,
+ importState: ImportState,
+ elementName: string,
+): string {
+ const resolvedSchema = toRecord(importState.resolve(schema));
+ if (Array.isArray(value)) {
+ const itemSchema = importState.resolve(resolvedSchema.items);
+ const itemName = stringAt(toRecord(itemSchema).xml, "name") ?? "item";
+ const items = value.map((item) => valueToXml(item, itemSchema, importState, itemName)).join("");
+ return `<${elementName}>${items}${elementName}>`;
+ }
+ if (isRecord(value)) {
+ const properties = toRecord(resolvedSchema.properties);
+ const attributes: string[] = [];
+ const children: string[] = [];
+ for (const [name, propertyValue] of Object.entries(value)) {
+ const propertySchema = toRecord(importState.resolve(properties[name]));
+ const xml = toRecord(propertySchema.xml);
+ const xmlName = stringAt(xml, "name") ?? name;
+ if (xml.attribute === true) {
+ attributes.push(`${xmlName}="${escapeXml(stringifyExampleValue(propertyValue))}"`);
+ } else {
+ children.push(valueToXml(propertyValue, propertySchema, importState, xmlName));
+ }
+ }
+ const attributeText = attributes.length > 0 ? ` ${attributes.join(" ")}` : "";
+ return `<${elementName}${attributeText}>${children.join("")}${elementName}>`;
+ }
+ return `<${elementName}>${escapeXml(stringifyExampleValue(value))}${elementName}>`;
+}
+
+function escapeXml(value: string): string {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
function mediaTypeExample(mediaType: UnknownRecord, importState: ImportState): unknown {
const directExample = firstPresent(mediaType.example, firstExampleValue(mediaType.examples));
if (directExample != null) return directExample;
return schemaToExample(importState.resolve(mediaType.schema), importState);
}
-function schemaToFormParameters(schema: unknown, importState: ImportState) {
+function schemaToFormParameters(
+ schema: unknown,
+ importState: ImportState,
+ example?: UnknownRecord,
+) {
const resolvedSchema = toRecord(importState.resolve(schema));
const required = toArray(resolvedSchema.required).filter(
(name): name is string => typeof name === "string",
);
- const properties = Object.entries(toRecord(resolvedSchema.properties)).slice(
- 0,
- MAX_EXAMPLE_PROPERTIES,
- );
+ const properties = Object.entries(toRecord(resolvedSchema.properties))
+ .filter(([, property]) => toRecord(importState.resolve(property)).readOnly !== true)
+ .slice(0, MAX_EXAMPLE_PROPERTIES);
return properties.map(([name, property]) => {
const resolvedProperty = toRecord(importState.resolve(property));
- const example = schemaToExample(resolvedProperty, importState);
+ const propertyExample = example?.[name] ?? schemaToExample(resolvedProperty, importState);
const base = {
enabled: required.includes(name),
name,
@@ -907,7 +990,7 @@ function schemaToFormParameters(schema: unknown, importState: ImportState) {
if (stringAt(resolvedProperty, "format") === "binary") {
return { ...base, file: "" };
}
- return { ...base, value: stringifyExampleValue(example) };
+ return { ...base, value: stringifyExampleValue(propertyExample) };
});
}
@@ -954,11 +1037,13 @@ function schemaToExample(
const required = toArray(resolved.required).filter(
(name): name is string => typeof name === "string",
);
- const properties = Object.entries(toRecord(resolved.properties)).sort(([a], [b]) => {
- const aRequired = required.includes(a);
- const bRequired = required.includes(b);
- return aRequired === bRequired ? 0 : aRequired ? -1 : 1;
- });
+ const properties = Object.entries(toRecord(resolved.properties))
+ .filter(([, property]) => toRecord(importState.resolve(property)).readOnly !== true)
+ .sort(([a], [b]) => {
+ const aRequired = required.includes(a);
+ const bRequired = required.includes(b);
+ return aRequired === bRequired ? 0 : aRequired ? -1 : 1;
+ });
return Object.fromEntries(
properties
diff --git a/plugins/importer-openapi/tests/index.test.ts b/plugins/importer-openapi/tests/index.test.ts
index 35250182..a1ac2375 100644
--- a/plugins/importer-openapi/tests/index.test.ts
+++ b/plugins/importer-openapi/tests/index.test.ts
@@ -673,6 +673,136 @@ describe("importer-openapi", () => {
);
});
+ test("Serializes request examples according to their media type", async () => {
+ const imported = await convertOpenApi(
+ JSON.stringify({
+ openapi: "3.0.4",
+ info: { title: "Media Type Test", version: "1.0.0" },
+ paths: {
+ "/xml": {
+ post: {
+ requestBody: {
+ content: {
+ "application/xml": {
+ schema: {
+ type: "object",
+ xml: { name: "user" },
+ properties: { name: { type: "string", example: "Ada" } },
+ },
+ },
+ },
+ },
+ responses: {},
+ },
+ },
+ "/json-string": {
+ post: {
+ requestBody: {
+ content: { "application/json": { schema: { type: "string", example: "hello" } } },
+ },
+ responses: {},
+ },
+ },
+ },
+ }),
+ );
+
+ expect(imported?.resources.httpRequests[0]?.body).toEqual({
+ text: "Ada",
+ });
+ expect(imported?.resources.httpRequests[1]?.body).toEqual({ text: '"hello"' });
+ });
+
+ test("Omits read-only properties from generated request bodies", async () => {
+ const imported = await convertOpenApi(
+ JSON.stringify({
+ openapi: "3.0.4",
+ info: { title: "Read Only Test", version: "1.0.0" },
+ paths: {
+ "/users": {
+ post: {
+ requestBody: {
+ content: {
+ "application/json": {
+ schema: {
+ type: "object",
+ properties: {
+ id: { type: "string", readOnly: true, example: "server-id" },
+ name: { type: "string", example: "Ada" },
+ },
+ },
+ },
+ },
+ },
+ responses: {},
+ },
+ },
+ },
+ }),
+ );
+
+ expect(imported?.resources.httpRequests[0]?.body).toEqual({
+ text: JSON.stringify({ name: "Ada" }, null, 2),
+ });
+ });
+
+ test("Imports Swagger 2 file parameters as file form entries", async () => {
+ const imported = await convertOpenApi(
+ JSON.stringify({
+ swagger: "2.0",
+ info: { title: "File Upload Test", version: "1.0.0" },
+ host: "example.com",
+ consumes: ["multipart/form-data"],
+ paths: {
+ "/upload": {
+ post: {
+ parameters: [{ name: "upload", in: "formData", required: true, type: "file" }],
+ responses: {},
+ },
+ },
+ },
+ }),
+ );
+
+ expect(imported?.resources.httpRequests[0]?.body).toEqual({
+ form: [{ enabled: true, name: "upload", file: "" }],
+ });
+ });
+
+ test("Serializes Swagger 2 XML request bodies as XML", async () => {
+ const imported = await convertOpenApi(
+ JSON.stringify({
+ swagger: "2.0",
+ info: { title: "Swagger XML Test", version: "1.0.0" },
+ host: "example.com",
+ consumes: ["application/xml"],
+ paths: {
+ "/users": {
+ post: {
+ parameters: [
+ {
+ name: "user",
+ in: "body",
+ required: true,
+ schema: {
+ type: "object",
+ xml: { name: "user" },
+ properties: { name: { type: "string", example: "Ada" } },
+ },
+ },
+ ],
+ responses: {},
+ },
+ },
+ },
+ }),
+ );
+
+ expect(imported?.resources.httpRequests[0]?.body).toEqual({
+ text: "Ada",
+ });
+ });
+
test("Imports Swagger 2 basic auth and cookie API keys", async () => {
const imported = await convertOpenApi(
JSON.stringify({