diff --git a/packages/core/src/getters/res-req-types.ts b/packages/core/src/getters/res-req-types.ts index b1ad6d077..d01022c10 100644 --- a/packages/core/src/getters/res-req-types.ts +++ b/packages/core/src/getters/res-req-types.ts @@ -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'; @@ -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; @@ -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, @@ -170,6 +183,8 @@ export const getResReqTypes = ( name: propName, schemaObject: mediaType.schema!, context, + isRequestBodyOptional: + 'required' in res && res.required === false, }) : undefined; @@ -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, @@ -220,23 +241,47 @@ export const getResReqTypes = ( ); }; +const getFormDataAdditionalImports = ({ + schemaObject, + context, +}: { + schemaObject: SchemaObject | ReferenceObject; + context: ContextSpecs; +}): GeneratorImport[] => { + const { schema } = resolveRef(schemaObject, context); + if (schema.type === 'object') { + if (schema.oneOf || schema.anyOf) { + const combinedSchemas = schema.oneOf || schema.anyOf; + + return combinedSchemas!.map((schema) => { + const { imports } = resolveRef(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, context); const propName = camel( !isRef && isReference(schemaObject) ? imports[0].name : name, ); + const additionalImports: GeneratorImport[] = []; const variableName = isUrlEncoded ? 'formUrlEncoded' : 'formData'; let form = isUrlEncoded @@ -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'); @@ -275,6 +332,7 @@ const getSchemaFormDataAndUrlEncoded = ({ variableName, propName, context, + isRequestBodyOptional, }); form += formDataValues; @@ -303,11 +361,13 @@ 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]) => { @@ -315,27 +375,34 @@ const resolveSchemaPropertiesToFormData = ({ 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) { @@ -344,7 +411,7 @@ const resolveSchemaPropertiesToFormData = ({ return ( acc + - `if(${valueKey} !== undefined && ${valueKey} !== null) {\n ${formDataValue} }\n` + `if(${valueKey} !== undefined && ${nonOptionalValueKey} !== null) {\n ${formDataValue} }\n` ); } diff --git a/tests/configs/swr.config.ts b/tests/configs/swr.config.ts index a8e659afd..4efa0a6fc 100644 --- a/tests/configs/swr.config.ts +++ b/tests/configs/swr.config.ts @@ -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', + }, + }, + }, }); diff --git a/tests/specifications/form-data-optional-request.yaml b/tests/specifications/form-data-optional-request.yaml new file mode 100644 index 000000000..a1a81e9c6 --- /dev/null +++ b/tests/specifications/form-data-optional-request.yaml @@ -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