Skip to content

Commit

Permalink
fix: take optional request body into account when creating form data (#…
Browse files Browse the repository at this point in the history
…1416)

* fix: take optional request body into account when creating form data

* fix: don't forget to import types

* fix: split form data and imports to separate functions

---------

Co-authored-by: Alfred Jonsson <alfred.jonsson@decerno.se>
  • Loading branch information
AllieJonsson and Alfred Jonsson authored Jun 7, 2024
1 parent d1cf87f commit 9c44e9e
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 18 deletions.
103 changes: 85 additions & 18 deletions packages/core/src/getters/res-req-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from 'openapi3-ts/oas30';
import { resolveObject } from '../resolvers/object';
import { resolveExampleRefs, resolveRef } from '../resolvers/ref';
import { ContextSpecs, ResReqTypesValue } from '../types';
import { ContextSpecs, GeneratorImport, ResReqTypesValue } from '../types';
import { camel } from '../utils';
import { isReference } from '../utils/assertion';
import { pascal } from '../utils/case';
Expand Down Expand Up @@ -95,6 +95,9 @@ export const getResReqTypes = (
...context,
specKey: specKey || context.specKey,
},
isRequestBodyOptional:
// Even though required is false by default, we only consider required to be false if specified. (See pull 1277)
'required' in bodySchema && bodySchema.required === false,
isRef: true,
})
: undefined;
Expand All @@ -107,15 +110,25 @@ export const getResReqTypes = (
...context,
specKey: specKey || context.specKey,
},
isRequestBodyOptional:
'required' in bodySchema && bodySchema.required === false,
isUrlEncoded: true,
isRef: true,
})
: undefined;

const additionalImports = getFormDataAdditionalImports({
schemaObject: mediaType?.schema,
context: {
...context,
specKey: specKey || context.specKey,
},
});

return [
{
value: name,
imports: [{ name, specKey, schemaName }],
imports: [{ name, specKey, schemaName }, ...additionalImports],
schemas: [],
type: 'unknown',
isEnum: false,
Expand Down Expand Up @@ -170,6 +183,8 @@ export const getResReqTypes = (
name: propName,
schemaObject: mediaType.schema!,
context,
isRequestBodyOptional:
'required' in res && res.required === false,
})
: undefined;

Expand All @@ -179,12 +194,18 @@ export const getResReqTypes = (
schemaObject: mediaType.schema!,
context,
isUrlEncoded: true,
isRequestBodyOptional:
'required' in res && res.required === false,
})
: undefined;

const additionalImports = getFormDataAdditionalImports({
schemaObject: mediaType.schema!,
context,
});
return {
...resolvedValue,
imports: resolvedValue.imports,
imports: [...resolvedValue.imports, ...additionalImports],
formData,
formUrlEncoded,
contentType,
Expand Down Expand Up @@ -220,23 +241,47 @@ export const getResReqTypes = (
);
};

const getFormDataAdditionalImports = ({
schemaObject,
context,
}: {
schemaObject: SchemaObject | ReferenceObject;
context: ContextSpecs;
}): GeneratorImport[] => {
const { schema } = resolveRef<SchemaObject>(schemaObject, context);
if (schema.type === 'object') {
if (schema.oneOf || schema.anyOf) {
const combinedSchemas = schema.oneOf || schema.anyOf;

return combinedSchemas!.map((schema) => {
const { imports } = resolveRef<SchemaObject>(schema, context);
return imports[0];
});
}
}
return [];
};

const getSchemaFormDataAndUrlEncoded = ({
name,
schemaObject,
context,
isRequestBodyOptional,
isUrlEncoded,
isRef,
}: {
name: string;
schemaObject: SchemaObject | ReferenceObject;
context: ContextSpecs;
isRequestBodyOptional: boolean;
isUrlEncoded?: boolean;
isRef?: boolean;
}) => {
}): string => {
const { schema, imports } = resolveRef<SchemaObject>(schemaObject, context);
const propName = camel(
!isRef && isReference(schemaObject) ? imports[0].name : name,
);
const additionalImports: GeneratorImport[] = [];

const variableName = isUrlEncoded ? 'formUrlEncoded' : 'formData';
let form = isUrlEncoded
Expand All @@ -256,12 +301,24 @@ const getSchemaFormDataAndUrlEncoded = ({
context,
);

return resolveSchemaPropertiesToFormData({
schema: combinedSchema,
variableName,
propName: shouldCast ? `(${propName} as any)` : propName,
context,
});
if (shouldCast) additionalImports.push(imports[0]);

const newPropName = shouldCast
? `${propName}${pascal(imports[0].name)}`
: propName;
const newPropDefinition = shouldCast
? `const ${newPropName} = (${propName} as ${imports[0].name}${isRequestBodyOptional ? ' | undefined' : ''});\n`
: '';
return (
newPropDefinition +
resolveSchemaPropertiesToFormData({
schema: combinedSchema,
variableName,
propName: newPropName,
context,
isRequestBodyOptional,
})
);
})
.filter((x) => x)
.join('\n');
Expand All @@ -275,6 +332,7 @@ const getSchemaFormDataAndUrlEncoded = ({
variableName,
propName,
context,
isRequestBodyOptional,
});

form += formDataValues;
Expand Down Expand Up @@ -303,39 +361,48 @@ const resolveSchemaPropertiesToFormData = ({
variableName,
propName,
context,
isRequestBodyOptional,
}: {
schema: SchemaObject;
variableName: string;
propName: string;
context: ContextSpecs;
isRequestBodyOptional: boolean;
}) => {
const formDataValues = Object.entries(schema.properties ?? {}).reduce(
(acc, [key, value]) => {
const { schema: property } = resolveRef<SchemaObject>(value, context);

let formDataValue = '';

const formatedKey = !keyword.isIdentifierNameES5(key)
const formattedKeyPrefix = !isRequestBodyOptional
? ''
: !keyword.isIdentifierNameES5(key)
? '?.'
: '?';
const formattedKey = !keyword.isIdentifierNameES5(key)
? `['${key}']`
: `.${key}`;

const valueKey = `${propName}${formatedKey}`;
const valueKey = `${propName}${formattedKeyPrefix}${formattedKey}`;
const nonOptionalValueKey = `${propName}${formattedKey}`;

if (property.type === 'object') {
formDataValue = `${variableName}.append('${key}', JSON.stringify(${valueKey}));\n`;
formDataValue = `${variableName}.append('${key}', JSON.stringify(${nonOptionalValueKey}));\n`;
} else if (property.type === 'array') {
formDataValue = `${valueKey}.forEach(value => ${variableName}.append('${key}', value));\n`;
formDataValue = `${nonOptionalValueKey}.forEach(value => ${variableName}.append('${key}', value));\n`;
} else if (
property.type === 'number' ||
property.type === 'integer' ||
property.type === 'boolean'
) {
formDataValue = `${variableName}.append('${key}', ${valueKey}.toString())\n`;
formDataValue = `${variableName}.append('${key}', ${nonOptionalValueKey}.toString())\n`;
} else {
formDataValue = `${variableName}.append('${key}', ${valueKey})\n`;
formDataValue = `${variableName}.append('${key}', ${nonOptionalValueKey})\n`;
}

const isRequired = schema.required?.includes(key);
const isRequired =
schema.required?.includes(key) && !isRequestBodyOptional;

if (property.nullable) {
if (isRequired) {
Expand All @@ -344,7 +411,7 @@ const resolveSchemaPropertiesToFormData = ({

return (
acc +
`if(${valueKey} !== undefined && ${valueKey} !== null) {\n ${formDataValue} }\n`
`if(${valueKey} !== undefined && ${nonOptionalValueKey} !== null) {\n ${formDataValue} }\n`
);
}

Expand Down
14 changes: 14 additions & 0 deletions tests/configs/swr.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,18 @@ export default defineConfig({
},
},
},
formData: {
output: {
target: '../generated/swr/form-data-optional-request/endpoints.ts',
schemas: '../generated/swr/form-data-optional-request/model',
client: 'swr',
mock: true,
},
input: {
target: '../specifications/form-data-optional-request.yaml',
override: {
transformer: '../transformers/add-version.js',
},
},
},
});
90 changes: 90 additions & 0 deletions tests/specifications/form-data-optional-request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
openapi: '3.0.0'
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
post:
summary: Create a pet
operationId: createPets
tags:
- pets
requestBody:
required: false
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/Pet'
responses:
'200':
description: Created Pet
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Pet:
type: object
oneOf:
- $ref: '#/components/schemas/PetBase'
- $ref: '#/components/schemas/PetExtended'
required:
- id
- name
properties:
'@id':
type: string
format: iri-reference
email:
type: string
format: email
callingCode:
type: string
enum: ['+33', '+420', '+33'] # intentional duplicated value
country:
type: string
enum: ["People's Republic of China", 'Uruguay']
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

PetBase:
type: object
required:
- name
properties:
name:
type: string
tag:
type: string
PetExtended:
type: object
required:
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string

0 comments on commit 9c44e9e

Please sign in to comment.