Skip to content

Commit

Permalink
[typescript][typescript-node] Serialize/deserialize maps and nullable…
Browse files Browse the repository at this point in the history
… types correctly (#19362)

* Remove deprecated suppressImplicitAnyIndexErrors property from templates

* Add encoding / decoding tests for the typescript generator

* Add encoding / decoding tests for the typescript-node generator

* [Typescript][Typescript Node] fix deserializing complex types

This fixes deserializing of models that are composed with null,
undefined, or are inside of a map.

* Use more sensible `startsWith` implementation

* Remove use of magic number offsets

* Fix endsWith bounds checking

* Regenerate samples
  • Loading branch information
simon-abbott authored Sep 25, 2024
1 parent 9147e99 commit a6581e8
Show file tree
Hide file tree
Showing 89 changed files with 14,438 additions and 143 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/samples-typescript-encode-decode.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: TypeScript Client (Encoding / Decoding Test)

on:
pull_request:
paths:
- samples/client/others/typescript/encode-decode/**
- .github/workflows/samples-typescript-encode-decode.yaml
jobs:
build:
name: Test TypeScript Encoding / Decoding
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sample:
# clients
- samples/client/others/typescript/encode-decode/test
node-version:
- 16
- 18
- 20
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install
working-directory: ${{ matrix.sample }}
run: |
npm run preinstall
npm i
- name: Test
working-directory: ${{ matrix.sample }}
run: npm test
37 changes: 37 additions & 0 deletions .github/workflows/samples-typescript-node-encode-decode.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: TypeScript Node Client (Encoding / Decoding Test)

on:
pull_request:
paths:
- samples/client/others/typescript-node/encode-decode/**
- .github/workflows/samples-typescript-node-encode-decode.yaml
jobs:
build:
name: Test TypeScript Node Encoding / Decoding
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sample:
# clients
- samples/client/others/typescript-node/encode-decode/test
node-version:
- 16
- 18
- 20
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install
working-directory: ${{ matrix.sample }}
run: |
npm run preinstall
npm i
- name: Test
working-directory: ${{ matrix.sample }}
run: npm test
11 changes: 11 additions & 0 deletions bin/configs/typescript-encode-decode.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generatorName: typescript
outputDir: samples/client/others/typescript/encode-decode/build
inputSpec: modules/openapi-generator/src/test/resources/3_1/encode-decode.yaml
templateDir: modules/openapi-generator/src/main/resources/typescript
additionalProperties:
artifactId: encode-decode-typescript
hideGenerationTimestamp: "true"
npmVersion: 1.0.0
npmName: '@openapitools/typescript-encode-decode'
nullSafeAdditionalProps: true
platform: node
10 changes: 10 additions & 0 deletions bin/configs/typescript-node-encode-decode.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
generatorName: typescript-node
outputDir: samples/client/others/typescript-node/encode-decode/build
inputSpec: modules/openapi-generator/src/test/resources/3_1/encode-decode.yaml
templateDir: modules/openapi-generator/src/main/resources/typescript-node
additionalProperties:
artifactId: encode-decode-typescript-node
hideGenerationTimestamp: "true"
npmVersion: 1.0.0
npmName: '@openapitools/typescript-node-encode-decode'
nullSafeAdditionalProps: true
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true,
"target": "{{#supportsES6}}es6{{/supportsES6}}{{^supportsES6}}es5{{/supportsES6}}",
"module": "{{#supportsES6}}es6{{/supportsES6}}{{^supportsES6}}commonjs{{/supportsES6}}",
"moduleResolution": "node",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true,
"target": "{{#supportsES6}}es6{{/supportsES6}}{{^supportsES6}}es5{{/supportsES6}}",
"module": "{{#supportsES6}}es6{{/supportsES6}}{{^supportsES6}}commonjs{{/supportsES6}}",
"moduleResolution": "node",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true,
"target": "{{#supportsES6}}ES6{{/supportsES6}}{{^supportsES6}}ES5{{/supportsES6}}",
"moduleResolution": "node",
"removeComments": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ let typeMap: {[index: string]: any} = {
{{/models}}
}

// Check if a string starts with another string without using es6 features
function startsWith(str: string, match: string): boolean {
return str.substring(0, match.length) === match;
}

// Check if a string ends with another string without using es6 features
function endsWith(str: string, match: string): boolean {
return str.length >= match.length && str.substring(str.length - match.length) === match;
}

const nullableSuffix = " | null";
const optionalSuffix = " | undefined";
const arrayPrefix = "Array<";
const arraySuffix = ">";
const mapPrefix = "{ [key: string]: ";
const mapSuffix = "; }";

export class ObjectSerializer {
public static findCorrectType(data: any, expectedType: string) {
if (data == undefined) {
Expand Down Expand Up @@ -104,20 +121,35 @@ export class ObjectSerializer {
}
}
public static serialize(data: any, type: string) {
public static serialize(data: any, type: string): any {
if (data == undefined) {
return data;
} else if (primitives.indexOf(type.toLowerCase()) !== -1) {
return data;
} else if (type.lastIndexOf("Array<", 0) === 0) { // string.startsWith pre es6
let subType: string = type.replace("Array<", ""); // Array<Type> => Type>
subType = subType.substring(0, subType.length - 1); // Type> => Type
} else if (endsWith(type, nullableSuffix)) {
let subType: string = type.slice(0, -nullableSuffix.length); // Type | null => Type
return ObjectSerializer.serialize(data, subType);
} else if (endsWith(type, optionalSuffix)) {
let subType: string = type.slice(0, -optionalSuffix.length); // Type | undefined => Type
return ObjectSerializer.serialize(data, subType);
} else if (startsWith(type, arrayPrefix)) {
let subType: string = type.slice(arrayPrefix.length, -arraySuffix.length); // Array<Type> => Type
let transformedData: any[] = [];
for (let index = 0; index < data.length; index++) {
let datum = data[index];
transformedData.push(ObjectSerializer.serialize(datum, subType));
}
return transformedData;
} else if (startsWith(type, mapPrefix)) {
let subType: string = type.slice(mapPrefix.length, -mapSuffix.length); // { [key: string]: Type; } => Type
let transformedData: { [key: string]: any } = {};
for (let key in data) {
transformedData[key] = ObjectSerializer.serialize(
data[key],
subType,
);
}
return transformedData;
} else if (type === "Date") {
return data.toISOString();
} else {
Expand All @@ -142,22 +174,37 @@ export class ObjectSerializer {
}
}
public static deserialize(data: any, type: string) {
public static deserialize(data: any, type: string): any {
// polymorphism may change the actual type.
type = ObjectSerializer.findCorrectType(data, type);
if (data == undefined) {
return data;
} else if (primitives.indexOf(type.toLowerCase()) !== -1) {
return data;
} else if (type.lastIndexOf("Array<", 0) === 0) { // string.startsWith pre es6
let subType: string = type.replace("Array<", ""); // Array<Type> => Type>
subType = subType.substring(0, subType.length - 1); // Type> => Type
} else if (endsWith(type, nullableSuffix)) {
let subType: string = type.slice(0, -nullableSuffix.length); // Type | null => Type
return ObjectSerializer.deserialize(data, subType);
} else if (endsWith(type, optionalSuffix)) {
let subType: string = type.slice(0, -optionalSuffix.length); // Type | undefined => Type
return ObjectSerializer.deserialize(data, subType);
} else if (startsWith(type, arrayPrefix)) {
let subType: string = type.slice(arrayPrefix.length, -arraySuffix.length); // Array<Type> => Type
let transformedData: any[] = [];
for (let index = 0; index < data.length; index++) {
let datum = data[index];
transformedData.push(ObjectSerializer.deserialize(datum, subType));
}
return transformedData;
} else if (startsWith(type, mapPrefix)) {
let subType: string = type.slice(mapPrefix.length, -mapSuffix.length); // { [key: string]: Type; } => Type
let transformedData: { [key: string]: any } = {};
for (let key in data) {
transformedData[key] = ObjectSerializer.deserialize(
data[key],
subType,
);
}
return transformedData;
} else if (type === "Date") {
return new Date(data);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true,
"target": "{{#supportsES6}}ES6{{/supportsES6}}{{^supportsES6}}ES5{{/supportsES6}}",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ const supportedMimeTypePredicatesWithPriority: MimeTypePredicate[] = [
isFormUrlencodedMimeType,
];

const nullableSuffix = " | null";
const optionalSuffix = " | undefined";
const arrayPrefix = "Array<";
const arraySuffix = ">";
const mapPrefix = "{ [key: string]: ";
const mapSuffix = "; }";

export class ObjectSerializer {
public static findCorrectType(data: any, expectedType: string) {
if (data == undefined) {
Expand Down Expand Up @@ -140,19 +147,35 @@ export class ObjectSerializer {
}
}
public static serialize(data: any, type: string, format: string) {
public static serialize(data: any, type: string, format: string): any {
if (data == undefined) {
return data;
} else if (primitives.indexOf(type.toLowerCase()) !== -1) {
return data;
} else if (type.lastIndexOf("Array<", 0) === 0) { // string.startsWith pre es6
let subType: string = type.replace("Array<", ""); // Array<Type> => Type>
subType = subType.substring(0, subType.length - 1); // Type> => Type
} else if (type.endsWith(nullableSuffix)) {
let subType: string = type.slice(0, -nullableSuffix.length); // Type | null => Type
return ObjectSerializer.serialize(data, subType, format);
} else if (type.endsWith(optionalSuffix)) {
let subType: string = type.slice(0, -optionalSuffix.length); // Type | undefined => Type
return ObjectSerializer.serialize(data, subType, format);
} else if (type.startsWith(arrayPrefix)) {
let subType: string = type.slice(arrayPrefix.length, -arraySuffix.length); // Array<Type> => Type
let transformedData: any[] = [];
for (let date of data) {
transformedData.push(ObjectSerializer.serialize(date, subType, format));
}
return transformedData;
} else if (type.startsWith(mapPrefix)) {
let subType: string = type.slice(mapPrefix.length, -mapSuffix.length); // { [key: string]: Type; } => Type
let transformedData: { [key: string]: any } = {};
for (let key in data) {
transformedData[key] = ObjectSerializer.serialize(
data[key],
subType,
format,
);
}
return transformedData;
} else if (type === "Date") {
if (format == "date") {
let month = data.getMonth()+1
Expand Down Expand Up @@ -185,21 +208,37 @@ export class ObjectSerializer {
}
}
public static deserialize(data: any, type: string, format: string) {
public static deserialize(data: any, type: string, format: string): any {
// polymorphism may change the actual type.
type = ObjectSerializer.findCorrectType(data, type);
if (data == undefined) {
return data;
} else if (primitives.indexOf(type.toLowerCase()) !== -1) {
return data;
} else if (type.lastIndexOf("Array<", 0) === 0) { // string.startsWith pre es6
let subType: string = type.replace("Array<", ""); // Array<Type> => Type>
subType = subType.substring(0, subType.length - 1); // Type> => Type
} else if (type.endsWith(nullableSuffix)) {
let subType: string = type.slice(0, -nullableSuffix.length); // Type | null => Type
return ObjectSerializer.deserialize(data, subType, format);
} else if (type.endsWith(optionalSuffix)) {
let subType: string = type.slice(0, -optionalSuffix.length); // Type | undefined => Type
return ObjectSerializer.deserialize(data, subType, format);
} else if (type.startsWith(arrayPrefix)) {
let subType: string = type.slice(arrayPrefix.length, -arraySuffix.length); // Array<Type> => Type
let transformedData: any[] = [];
for (let date of data) {
transformedData.push(ObjectSerializer.deserialize(date, subType, format));
}
return transformedData;
} else if (type.startsWith(mapPrefix)) {
let subType: string = type.slice(mapPrefix.length, -mapSuffix.length); // { [key: string]: Type; } => Type
let transformedData: { [key: string]: any } = {};
for (let key in data) {
transformedData[key] = ObjectSerializer.deserialize(
data[key],
subType,
format,
);
}
return transformedData;
} else if (type === "Date") {
return new Date(data);
} else {
Expand Down
Loading

0 comments on commit a6581e8

Please sign in to comment.