Add support for composed types in Typescript
Fixes: https://github.com/microsoft/kiota/issues/1812
Related Tickets
- https://github.com/microsoft/kiota/issues/2462
- https://github.com/microsoft/kiota/issues/4326
TODO
- [X] Handle Union of primitive values e.g https://github.com/microsoft/kiota/issues/2462
- [ ] Handle Objects Unions - by using Discriminator info and Intersections
Generation Logic Design:
1. Composed Types Comprised of Primitives Without Discriminator Property
Sample yml
openapi: 3.0.1
info:
title: Example of UnionTypes
version: 1.0.0
paths:
/primitives:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/primitives'
components:
schemas:
primitives:
oneOf:
- type: string
- type: number
For the union of primitive values shown above, the generation logic is as follows:
The factory method for union of primitives determines the return type from the parse node value. For example, if the node is number | string, the method should return the correct type based on the node's value, as shown below:
export type Primitives = number | string;
export function createPrimitivesFromDiscriminatorValue3(parseNode: ParseNode | undefined) : number | string | undefined {
if (parseNode) {
return parseNode.getNumberValue() || parseNode.getStringValue();
}
return undefined;
}
There is no need for a deserialization method, so it is omitted during generation.
The serialization method will determine the type of the node and call the respective method on the abstraction library for that type. Using the example above, if the node is number | string, the serializer should call either writer.writeNumberValue or writer.writeStringValue based on the node's actual value, as shown below:
export function serializePrimitives(writer: SerializationWriter, key: string, primitives: Primitives | undefined) : void {
if (primitives == undefined) return;
switch (typeof primitives) {
case "number":
writer.writeNumberValue(key, primitives);
break;
case "string":
writer.writeStringValue(key, primitives);
break;
}
}
2. Composed Types Comprised of Objects with a Discriminator Property Specified
Sample yml
openapi: 3.0.3
info:
title: Pet API
description: An API to return pet information.
version: 1.0.0
servers:
- url: http://localhost:8080
description: Local server
paths:
/pet:
get:
summary: Get pet information
operationId: getPet
responses:
'200':
description: Successful response
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: petType
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
'400':
description: Bad Request
'500':
description: Internal Server Error
components:
schemas:
Pet:
type: object
required:
- id
- name
- petType
properties:
id:
type: integer
example: 1
name:
type: string
example: "Fido"
petType:
type: string
description: "Type of the pet"
example: "cat"
Cat:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
favoriteToy:
type: string
example: "Mouse"
Dog:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
breed:
type: string
example: "Labrador"
For the example above, the factory method determines which pet to create, either Cat or Dog, based on the discriminator information, as shown below:
export type PetGetResponse = Cat | Dog;
export function createPetGetResponseFromDiscriminatorValue(parseNode: ParseNode | undefined) : ((instance?: Parsable) => Record<string, (node: ParseNode) => void>) {
const mappingValueNode = parseNode.getChildNode("petType");
if (mappingValueNode) {
const mappingValue = mappingValueNode.getStringValue();
if (mappingValue) {
switch (mappingValue) {
case "cat":
return deserializeIntoCat;
case "dog":
return deserializeIntoDog;
}
}
}
throw new Error("A discriminator property is required to distinguish a union type");
}
The deserializer is not necessary since the function has been delegated to the respective handlers as shown above. So instead of deserializePet, we have either deserializeCat if the response is a Cat, or deserializeDog if the response is a Dog.
The serializer will also delegate the writing functionality to the respective writer. In the example above, the generated output will be as follows:
export function serializePetGetResponse(writer: SerializationWriter, petGetResponse: Partial<PetGetResponse> | undefined = {}) : void {
if (petGetResponse == undefined) return;
switch (petGetResponse.petType) {
case "cat":
serializeCat(writer, petGetResponse);
break;
case "dog":
serializeDog(writer, petGetResponse);
break;
}
}
3. Intersection of Object Values
// Dummy intersection Objects to use in the tests
type Foo = { foo?: string; }
type Bar = { bar?: string; }
export type FooBar = Foo & Bar;
// Factory Method
export function createFooBarFromDiscriminatorValue(parseNode: ParseNode | undefined) : ((instance?: Parsable) => Record<string, (node: ParseNode) => void>) {
return deserializeIntoFooBar;
}
// Deserialization methods
export function deserializeIntoFooBar(fooBar: Partial<FooBar> | undefined = {}) : Record<string, (node: ParseNode) => void> {
return {
...deserializeIntoFoo(fooBar),
...deserializeIntoBar(fooBar),
}
}
export function deserializeIntoFoo(foo: Partial<Foo> | undefined = {}) : Record<string, (node: ParseNode) => void> {
return {
"foo": n => { foo.foo = n.getStringValue(); },
}
}
export function deserializeIntoBar(bar: Partial<Bar> | undefined = {}) : Record<string, (node: ParseNode) => void> {
return {
"bar": n => { bar.bar = n.getStringValue(); },
}
}
// Serialization methods
export function serializeFoo(writer: SerializationWriter, foo: Partial<Foo> | undefined = {}) : void {
writer.writeStringValue("foo", foo.foo);
}
export function serializeBar(writer: SerializationWriter, bar: Partial<Bar> | undefined = {}) : void {
writer.writeStringValue("bar", bar.bar);
}
export function serializeFooBar(writer: SerializationWriter, fooBar: Partial<FooBar> | undefined = {}) : void {
serializeFoo(writer, fooBar);
serializeBar(writer, fooBar);
}
@rkodev Please also remember to refactor the composed type which comprises of primitive values only, The factory methods I used won't work for primitive only values. My proposal is remove the factory methods together with serializers and deserializers.
then if the composed type field is property inside another object the existing logic will handle it e.g:
Deserializing
primitiveComposedType: (n) => {
response.primitiveComposedType= n.getNumberValue() ?? n.getStringValue();
},
Serializing
switch (typeof response.primitiveComposedType) {
case "number":
writer.writeNumberValue("data", response.primitiveComposedType);
break;
case "string":
writer.writeStringValue("data", response.primitiveComposedType);
break;
}
However if the composed type is at the root of the payload then sendPrimitive should be modified since basically there is no corresponding factory
@koros there are a number of changes that have been made
For serializing this is the syntax to check for types when its a collection or single value of primitives
switch (true) {
case typeof bank_account.account === "string":
writer.writeStringValue("account", bank_account.account as string);
break;
case Array.isArray(bank_account.account) && bank_account.account.every(item => typeof item === 'number'):
writer.writeCollectionOfPrimitiveValues<string>("account", bank_account.account);
break;
default:
writer.writeObjectValue<Account>("account", bank_account.account as Account | undefined | null, serializeBank_account_account);
break;
}
as for the deserializing, here is an example of a composed type that will serialize objects / collection of objects or primitives
petGetResponse.data = n.getObjectValue<Cat | Dog>(createpetResponseFromDiscriminatorValue) ?? n.getCollectionOfObjectValues<Dog>(createDogFromDiscriminatorValue) ?? n.getNumberValue() ?? n.getStringValue()
@rkodev can you also look at the defects reported here (ignore the complexity ones) and address the ones you can before I give it another review please? https://sonarcloud.io/project/issues?id=microsoft_kiota&pullRequest=4602&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true
@andrueastman FYI you still have two comments that are unresolved in the history. I didn't resolve them so you get a chance at checking whether they still apply
Quality Gate passed
Issues
0 New issues
0 Accepted issues
Measures
0 Security Hotspots
86.7% Coverage on New Code
0.0% Duplication on New Code
@rkodev I just noticed we forgot the changelog entry. Can you stand a quick pr please?
Done