
TSOA Fallstricke: Enumerationen in der Schnittstelle verwenden (Deutsch)
Dies ist der vierte Teil einer Serie von Beiträgen zum Thema Fallstricke beim Einsatz von TSOA. Dabei geht es um Lösungen von Schwierigkeiten die im praktischen Einsatz dieses Frameworks auftreten. In diesem Beitrag geht es darum was bei der Verwendung von Enumerationen als Parameter oder Rückgabewert einer REST Service Funktion die über TSOA generiert wurde zu beachten ist.
Enumerationen in der Schnittstelle verwenden
Bei der Verwendung von Enumerationen in der von TSOA generierten Schnittstelle gibt es mehrere Varianten Enumerationen einzubinden, die jedoch unterschiedlich gut funktionieren. Enumerationen bieten sich für konstante Typen / Aufzählungen an, die sowohl im Client als auch im Service genutzt werden. Wie bereits früher beschrieben, können Enumerationen sowohl als Path / Query Parameter als auch als Rückgabewert verwendet werden.
In Typescript gibt es mehrere Möglichkeiten eine Enumeration zu deklarieren.
Variante | Beispiel | Beschreibung | als Parameter | als JSON Parameter | als Return |
---|---|---|---|---|---|
1 | enum Manufacturer { AUDI, BMW } |
einfaches Enum | ⚠ | ⚠ | ⚠ |
2 | enum Manufacturer { AUDI = 'Audi', BMW = 'BMW' } |
String Enum | ( ✅ ) | ✅ | ✅ |
3 | type Manufacturer = 'Audi' | 'BMW' |
benannter String Literal Type | ( ✅ ) | ( ✅ ) | ( ✅ ) |
4 | param: 'Audi' | 'BMW' |
anonymer String Literal Type | ( ✅ ) | ( ✅ ) | ( ✅ ) |
Aus der Übersicht ist klar zu erkennen, dass bei Verwendung von Enumerationen die Variante 2 mit einem (JSON) Objekt Wrapper zu empfehlen ist.
Die Vor- und Nachteile der einzelnen Varianten im Überblick.
- Variante – einfaches Enum
- als Parameter
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
param === Manufacturer.BMW
) - ➖ enum wird nur als anonymes Enum exportiert, d.h. im clientseitigen Code müssen die string Literale verwendet werden (
return '1' // BMW
) - ➖ als enum Werte werden nur die Indizes exportiert, d.h. im clientseitigen Code bzw. Service Funktionsaufrufen müssen die Indizes angegeben werden (
return '1' // BMW
) ⚠
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
- als JSON Parameter
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
param.value === Manufacturer.BMW
) - ➕ enum wird komplett mit Namen exportiert, d.h. im clientseitigen Code kann das Enum über den Namen angesprochen werden (
return { value: Manufacturer.1 }
) - ➖ als enum Werte werden nur die Indizes exportiert, d.h. im clientseitigen Code bzw. Service Funktionsaufrufen müssen die Indizes angegeben werden (
return { value: Manufacturer.1 }
) ⚠
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
- als Return
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
return Manufacturer.BMW
) - ➕ enum wird komplett mit Namen exportiert, d.h. im clientseitigen Code kann das Enum über den Namen angesprochen werden (
param === Manufacturer.1
) - ➖ als enum Werte werden nur die Indizes exportiert, d.h. im clientseitigen Code bzw. Service Funktionsaufrufen müssen die Indizes angegeben werden (
param === Manufacturer.1
) ⚠
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
- als Parameter
- Variante – String Enum
- als Parameter
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
param === Manufacturer.BMW
) - ➖ enum wird nur als anonymes Enum exportiert, d.h. im clientseitigen Code müssen die string Literale verwendet werden (
return 'Audi'
) ( ✅ )
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
- als JSON Parameter
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
param.value === Manufacturer.BMW
) - ➕ enum wird komplett mit Namen exportiert, d.h. im clientseitigen Code kann das Enum über den Namen angesprochen werden (
return { value: Manufacturer.AUDI }
)
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
- als Return
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
return Manufacturer.BMW
) - ➕ enum wird komplett mit Namen exportiert, d.h. im clientseitigen Code kann das Enum über den Namen angesprochen werden (
param === Manufacturer.1
)
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
- als Parameter
- Variante – benannter String Literal Type
-
- als Parameter
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
param === 'BMW'
) ( ✅ ) - ➖ enum wird nur als anonymes Enum exportiert, d.h. im clientseitigen Code müssen die string Literale verwendet werden (
return 'Audi'
) ( ✅ )
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
- als JSON Parameter
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
param.value === Manufacturer.BMW
) - ➕ enum wird komplett mit Namen exportiert, d.h. im clientseitigen Code kann das Enum über den Namen angesprochen werden (
return { value: Manufacturer.AUDI }
)
- ➕ enum kann im serviceseitigen Code über Enum Namen angesprochen werden (
- als Return
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
return 'Audi'
) ( ✅ )
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
- als Parameter
-
- Variante – anonymer String Literal Type
- als Parameter
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
return 'Audi'
) ( ✅ )
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
- als JSON Parameter
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
return 'Audi'
) ( ✅ )
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
- als Return
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
return 'Audi'
) ( ✅ )
- ➕ enum kann im serviceseitigen und clientseitigen Code ausschließlich über string Literale angesprochen werden (
- als Parameter
Info |
---|
Swagger Codegen bietet die Möglichkeit aus einer (JSON) Swagger Beschreibung Type-Script Client Sourcecode zu generieren. Die vorhandenen Codegeneratoren generieren immer Enumerationen als String Literale Types, d.h. Variante 3 oder Variante 4. ( ✅ ) Das bedeutet, dass im Client Code stets die string Literale der Enumerationen zu verwenden sind ( param === 'Audi' ). |
Enumerations als einfache Übergabeparameter
Im folgenden Beispielcode werden alle 4 Varianten als Übergabeparameter für Service Funktionen demonstriert. Werden die Funktionen aufgerufen, wird der übergebene Enumeration Wert ausgegeben.
export enum Manufacturer { AUDI, BMW, FIAT, HONDA, PEUGEOT, RENAULT, SEAT, VOLVO, VW } export enum Manufacturer2 { AUDI = 'Audi', BMW = 'BMW', FIAT = 'Fiat', HONDA = 'Honda', PEUGEOT = 'Peugeot', RENAULT = 'Renault', SEAT = 'Seat', VOLVO = 'Volvo', VW = 'VW' } export type Manufacturer3 = 'Audi' | 'BMW' | 'Fiat' | 'Honda' | 'Peugeot' | 'Renault' | 'Seat' | 'Volvo' | 'VW' @Route('Car') @injectable() export class CarController extends Controller { constructor () { super() } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/Manufacturer') public async setManufacturer (@Path('plateNr') plate: string, @Query() man: Manufacturer): Promise { LOG.info('Manufacturer', man) // 'Manufacturer 0' } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/Manufacturer2') public async setManufacturer2 (@Path('plateNr') plate: string, @Query() man: Manufacturer2): Promise { LOG.info('Manufacturer', man) // 'Manufacturer Audi' } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/Manufacturer3') public async setManufacturer3 (@Path('plateNr') plate: string, @Query() man: Manufacturer3): Promise { LOG.info('Manufacturer', man) // 'Manufacturer Audi' } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/Manufacturer4') public async setManufacturer4 (@Path('plateNr') plate: string, @Query() man: 'Audi' | 'BMW' | 'Fiat' | 'Honda' | 'Peugeot' | 'Renault' | 'Seat' | 'Volvo' | 'VW'): Promise { LOG.info('Manufacturer', man) // 'Manufacturer Audi' }
Die Ausgabe zeigt bereits ein Problem mit Variante 1. Statt des Aufzählungsnamens wird der Index ausgegeben ⚠.
Über die Swagger Console kann eine HTML Beschreibung und Testoberfläche für die generierte Schnittstelle mit deren Funktionen angezeigt werden.
Während für Variante 2, 3 und 4 die korrekten Marken auswählbar sind ✅, wird für Variante 1nur der Index des Enums angeboten ⚠.
Die generierte Swagger API Beschreibung zeigt, wie TSOA die Enumerationen auflöst. Für Variante 1 wird ein anonymes Enum mit den Indizes generiert. Für die Varianten 2, 3 und 4 wird ein anonymes Enum mit den korrekten String Literalen des Aufzählungstyps generiert.
Da in allen Fällen TSOA ein anonymes Enum generiert, kann bei der Clientgenerierung kein benanntes Enum erzeugt werden. Der Name des Enums geht in allen Fällen verloren und die Werte müssen immer als String Literale angegeben werden. ⚠
"/Car/{plateNr}/Manufacturer": { "put": { "operationId": "SetManufacturer", "produces": [ "application/json" ], "responses": { "200": { "description": "successful" } }, "tags": [ "Car" ], "security": [], "parameters": [ { "in": "path", "name": "plateNr", "required": true, "type": "string" }, { "in": "query", "name": "man", "required": true, "type": "string", "enum": [ "0", "1", "2", "3", "4", "5", "6", "7", "8" ] } ] } }, "/Car/{plateNr}/Manufacturer2": { "put": { "operationId": "SetManufacturer2", "produces": [ "application/json" ], "responses": { "200": { "description": "successful" } }, "tags": [ "Car" ], "security": [], "parameters": [ { "in": "path", "name": "plateNr", "required": true, "type": "string" }, { "in": "query", "name": "man", "required": true, "type": "string", "enum": [ "Audi", "BMW", "Fiat", "Honda", "Peugeot", "Renault", "Seat", "Volvo", "VW" ] } ] } },
Durch Verpacken in ein Objekt werden Enumerationen vollständig exportiert
TSOA kann dazu gebracht werden, eine Enumeration komplett als eigene Definition mit Namen zu exportieren. Dazu muss die Enumeraton in ein Objekt verpackt werden. Dieser Wrapper kann dann, als Parameter verwendet werden. TSOA erzeugt für Wrapper und Enum die vollständige Swagger Beschreibung.
Die Lösung wird im folgenden Beispiel mit Variante 2 demonstriert. Die Lösung funktioniert auch für die anderen Varianten.
export enum Manufacturer { AUDI = 'Audi', /** * Description of the BMW enum value. */ BMW = 'BMW', FIAT = 'Fiat', HONDA = 'Honda', /** * Description of the Peugeot enum value. */ PEUGEOT = 'Peugeot', RENAULT = 'Renault', SEAT = 'Seat', VOLVO = 'Volvo', VW = 'VW' } interface ManufacturerInput { value: Manufacturer } @Route('Car') @injectable() export class CarController extends Controller { constructor () { super() } @Tags('Car') @SuccessResponse('200', 'successful') @Put('{plateNr}/Manufacturer') public async setManufacturer (@Path('plateNr') plate: string, @Body() man: ManufacturerInput): Promise { // ... }
Über die Swagger Console kann eine HTML Beschreibung und Testoberfläche für die generierte Schnittstelle mit deren Funktionen angezeigt werden.
- ➖ in der Test Oberfläche fehlt die Dropdown-Liste mit den Werten der Enumeration, die Werte müssen als string Literale im JSON Model angegeben werden
Die von TSOA generierte Swagger API Beschreibung beinhaltet die komplette Beschreibung des Wrappers und der Enumeration Manufacturer. Die Clientgenerierung kann anhand dieser Beschreibung eine komplette Enumeration erzeugen.
- ➖ die Codedokumentation einzelner Werte der Enumeration wird von TSOA verworfen
"Manufacturer": { "enum": [ "Audi", "BMW", "Fiat", "Honda", "Peugeot", "Renault", "Seat", "Volvo", "VW" ], "type": "string" }, "ManufacturerInput": { "properties": { "value": { "$ref": "#/definitions/Manufacturer" } }, "required": [ "value" ], "type": "object" }, "/Car/{plateNr}/Manufacturer": { "put": { "operationId": "SetManufacturer", "produces": [ "application/json" ], "responses": { "200": { "description": "successful" } }, "tags": [ "Car" ], "security": [], "parameters": [ { "in": "path", "name": "plateNr", "required": true, "type": "string" }, { "in": "body", "name": "man", "required": true, "schema": { "$ref": "#/definitions/ManufacturerInput" } } ] } },
Enumerationen als Rückgabewert
Im folgenden Beispielcode werden alle 4 Varianten als Rückgabewert für Service Funktionen demonstriert. Werden die Funktionen aufgerufen, wird immer der Aufzählungswert Peugeot zurückgegeben.
export enum Manufacturer { AUDI, BMW, FIAT, HONDA, PEUGEOT, RENAULT, SEAT, VOLVO, VW } export enum Manufacturer2 { AUDI = 'Audi', BMW = 'BMW', FIAT = 'Fiat', HONDA = 'Honda', PEUGEOT = 'Peugeot', RENAULT = 'Renault', SEAT = 'Seat', VOLVO = 'Volvo', VW = 'VW' } export type Manufacturer3 = 'Audi' | 'BMW' | 'Fiat' | 'Honda' | 'Peugeot' | 'Renault' | 'Seat' | 'Volvo' | 'VW' @Route('Car') @injectable() export class CarController extends Controller { constructor () { super() } @Tags('Car') @Get('{plateNr}/Manufacturer') public async getManufacturer1 (@Path('plateNr') plate: string): Promise { return Manufacturer.PEUGEOT // 4 } @Tags('Car') @Get('{plateNr}/Manufacturer2') public async getManufacturer2 (@Path('plateNr') plate: string): Promise { return Manufacturer2.PEUGEOT // "Peugeot" } @Tags('Car') @Get('{plateNr}/Manufacturer3') public async getManufacturer3 (@Path('plateNr') plate: string): Promise { return 'Peugeot' // "Peugeot" } @Tags('Car') @Get('{plateNr}/Manufacturer4') public async getManufacturer4 (@Path('plateNr') plate: string): Promise<'Audi' | 'BMW' | 'Fiat' | 'Honda' | 'Peugeot' | 'Renault' | 'Seat' | 'Volvo' | 'VW'> { return 'Peugeot' // "Peugeot" }
Die Ausgabe des Rückgabewerts zeigt bereits ein Problem mit Variante 1. Statt des Aufzählungsnamens wird dessen Index zurückgegeben ⚠.
Die generierte Swagger API Beschreibung zeigt, wie TSOA die Enumerationen auflöst. Für Variante 1 wird eine vollständige Enumeration allerdings mit den Indizes generiert. Für die Varianten 3 und 4 wird ein anonymes Enum mit den korrekten String Literalen des Aufzählungstyps generiert ( ✅ ). Nur für Variante 2 wird ein vollständiges und benanntes Enum mit den korrekten String Literalen erzeugt. ✅
"Manufacturer": { "enum": [ "0", "1", "2", "3", "4", "5", "6", "7", "8" ], "type": "string" }, "Manufacturer2": { "enum": [ "Audi", "BMW", "Fiat", "Honda", "Peugeot", "Renault", "Seat", "Volvo", "VW" ], "type": "string" }, "/Car/{plateNr}/Manufacturer": { "get": { "operationId": "GetManufacturer1", "produces": [ "application/json" ], "responses": { "200": { "description": "Ok", "schema": { "$ref": "#/definitions/Manufacturer" } } }, "tags": [ "Car" ], "security": [], "parameters": [ { "in": "path", "name": "plateNr", "required": true, "type": "string" } ] }, "/Car/{plateNr}/Manufacturer2": { "get": { "operationId": "GetManufacturer2", "produces": [ "application/json" ], "responses": { "200": { "description": "Ok", "schema": { "$ref": "#/definitions/Manufacturer2" } } }, "tags": [ "Car" ], "security": [], "parameters": [ { "in": "path", "name": "plateNr", "required": true, "type": "string" } ] }, "/Car/{plateNr}/Manufacturer3": { "get": { "operationId": "GetManufacturer3", "produces": [ "application/json" ], "responses": { "200": { "description": "Ok", "schema": { "type": "string", "enum": [ "Audi", "BMW", "Fiat", "Honda", "Peugeot", "Renault", "Seat", "Volvo", "VW" ] } } }, "tags": [ "Car" ], "security": [], "parameters": [ { "in": "path", "name": "plateNr", "required": true, "type": "string" } ] }, "/Car/{plateNr}/Manufacturer4": { "get": { "operationId": "GetManufacturer4", "produces": [ "application/json" ], "responses": { "200": { "description": "Ok", "schema": { "type": "string", "enum": [ "Audi", "BMW", "Fiat", "Honda", "Peugeot", "Renault", "Seat", "Volvo", "VW" ] } } }, "tags": [ "Car" ], "security": [], "parameters": [ { "in": "path", "name": "plateNr", "required": true, "type": "string" } ] },