Skip to content

Commit

Permalink
Validate the serialization/deserialization of composed types in types…
Browse files Browse the repository at this point in the history
…cript (#1290)

* add unit tests to validate the logic in the serialization/deserialization of composed types

* revert formating

* add more unit tests

* chore: removes extraneuous imports

* fix unit test

---------

Co-authored-by: Vincent Biret <[email protected]>
Co-authored-by: rkodev <[email protected]>
  • Loading branch information
3 people authored Sep 5, 2024
1 parent 1a69fe1 commit 33d4023
Show file tree
Hide file tree
Showing 7 changed files with 472 additions and 3 deletions.
28 changes: 27 additions & 1 deletion packages/serialization/json/test/common/JsonParseNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { assert, describe, it } from "vitest";
import { JsonParseNode } from "../../src/index";
import { createTestParserFromDiscriminatorValue, type TestBackedModel, createTestBackedModelFromDiscriminatorValue, type TestParser } from "./testEntity";
import { createTestParserFromDiscriminatorValue, type TestBackedModel, createTestBackedModelFromDiscriminatorValue, type TestParser, TestUnionObject, BarResponse } from "./testEntity";
import { UntypedTestEntity, createUntypedTestEntityFromDiscriminatorValue } from "./untypedTestEntiy";
import { UntypedNode, UntypedObject, isUntypedArray, isUntypedBoolean, isUntypedNode, isUntypedNumber, isUntypedObject } from "@microsoft/kiota-abstractions";

Expand Down Expand Up @@ -318,4 +318,30 @@ describe("JsonParseNode", () => {
const result5 = new JsonParseNode("true");
assert.isUndefined(result5.getBooleanValue());
});

it("should parse a union of objects and primitive values when value is primitive", async () => {
const result = new JsonParseNode({
testUnionObject: "Test String Value",
}).getObjectValue(createTestParserFromDiscriminatorValue) as TestParser;
assert.equal(result.testUnionObject, "Test String Value");
});

it("should parse a union of objects and primitive values when value is an object", async () => {
const barResponse = {
propA: "property A test value",
propB: "property B test value",
propC: undefined,
};
const result = new JsonParseNode({
testUnionObject: barResponse as TestUnionObject,
}).getObjectValue(createTestParserFromDiscriminatorValue) as TestParser;
assert.equal(JSON.stringify(result.testUnionObject), JSON.stringify(barResponse));
});

it("should parse a union of objects and primitive values when value is a number", async () => {
const result = new JsonParseNode({
testUnionObject: 1234,
}).getObjectValue(createTestParserFromDiscriminatorValue) as TestParser;
assert.equal(result.testUnionObject, 1234);
});
});
47 changes: 47 additions & 0 deletions packages/serialization/json/test/common/jsonSerializationWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,51 @@ describe("JsonParseNode", () => {
const contentAsStr = decoder.decode(serializedContent);
assert.equal(contentAsStr, '{"id":"1","title":"title","location":{"address":{"city":"Redmond","postalCode":"98052","state":"Washington","street":"NE 36th St"},"coordinates":{"latitude":47.678581,"longitude":-122.131577},"displayName":"Microsoft Building 25","floorCount":50,"hasReception":true,"contact":null},"keywords":[{"created":"2023-07-26T10:41:26Z","label":"Keyword1","termGuid":"10e9cc83-b5a4-4c8d-8dab-4ada1252dd70","wssId":6442450941},{"created":"2023-07-26T10:51:26Z","label":"Keyword2","termGuid":"2cae6c6a-9bb8-4a78-afff-81b88e735fef","wssId":6442450942}],"extra":{"value":{"createdDateTime":{"value":"2024-01-15T00:00:00+00:00"}}}}');
});

it("it should serialize a union of object and primitive when the value is a string", async () => {
const inputObject: TestParser = {
testUnionObject: "Test String value",
};
const writer = new JsonSerializationWriter();
writer.writeObjectValue("", inputObject, serializeTestParser);
const serializedContent = writer.getSerializedContent();
const decoder = new TextDecoder();
const contentAsStr = decoder.decode(serializedContent);
const result = JSON.parse(contentAsStr);
assert.isTrue("testUnionObject" in result);
assert.equal(result["testUnionObject"], "Test String value");
});

it("it should serialize a union of object and primitive when the value is a number", async () => {
const inputObject: TestParser = {
testUnionObject: 1234,
};
const writer = new JsonSerializationWriter();
writer.writeObjectValue("", inputObject, serializeTestParser);
const serializedContent = writer.getSerializedContent();
const decoder = new TextDecoder();
const contentAsStr = decoder.decode(serializedContent);
const result = JSON.parse(contentAsStr);
assert.isTrue("testUnionObject" in result);
assert.equal(result["testUnionObject"], 1234);
});

it("it should serialize a union of object and primitive when the value is an object", async () => {
const barResponse = {
propA: "property A test value",
propB: "property B test value",
propC: undefined,
};
const inputObject: TestParser = {
testUnionObject: barResponse,
};
const writer = new JsonSerializationWriter();
writer.writeObjectValue("", inputObject, serializeTestParser);
const serializedContent = writer.getSerializedContent();
const decoder = new TextDecoder();
const contentAsStr = decoder.decode(serializedContent);
const result = JSON.parse(contentAsStr);
assert.isTrue("testUnionObject" in result);
assert.equal(JSON.stringify(result["testUnionObject"]), JSON.stringify(barResponse));
});
});
30 changes: 30 additions & 0 deletions packages/serialization/json/test/common/testEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface TestParser {
id?: string | null | undefined;
testNumber?: number | null | undefined;
testGuid?: Guid | null | undefined;
testUnionObject?: TestUnionObject | null | undefined;
}
export interface TestBackedModel extends TestParser, BackedModel {
backingStoreEnabled?: boolean | undefined;
Expand All @@ -35,6 +36,7 @@ export interface BarResponse extends Parsable {
propB?: string | undefined;
propC?: Date | undefined;
}
export type TestUnionObject = FooResponse | BarResponse | string | number;

export function createTestParserFromDiscriminatorValue(parseNode: ParseNode | undefined) {
if (!parseNode) throw new Error("parseNode cannot be undefined");
Expand Down Expand Up @@ -85,6 +87,9 @@ export function deserializeTestParser(testParser: TestParser | undefined = {}):
testGuid: (n) => {
testParser.testGuid = n.getGuidValue();
},
testUnionObject: (n) => {
testParser.testUnionObject = n.getStringValue() ?? n.getNumberValue() ?? n.getObjectValue(createTestUnionObjectFromDiscriminatorValue);
},
};
}

Expand Down Expand Up @@ -137,6 +142,13 @@ export function serializeTestParser(writer: SerializationWriter, entity: TestPar
writer.writeObjectValue("testObject", entity.testObject, serializeTestObject);
writer.writeCollectionOfObjectValues("foos", entity.foos, serializeFoo);
writer.writeAdditionalData(entity.additionalData);
if (typeof entity.testUnionObject === "string") {
writer.writeStringValue("testUnionObject", entity.testUnionObject);
} else if (typeof entity.testUnionObject === "number") {
writer.writeNumberValue("testUnionObject", entity.testUnionObject);
} else {
writer.writeObjectValue("testUnionObject", entity.testUnionObject as any, serializeTestUnionObject);
}
}

export function serializeFoo(writer: SerializationWriter, entity: FooResponse | undefined = {}): void {
Expand All @@ -153,3 +165,21 @@ export function serializeBar(writer: SerializationWriter, entity: BarResponse |
export function serializeTestBackModel(writer: SerializationWriter, entity: TestBackedModel | undefined = {}): void {
serializeTestParser(writer, entity);
}

// Factory Method
export function createTestUnionObjectFromDiscriminatorValue(parseNode: ParseNode | undefined): (instance?: Parsable) => Record<string, (node: ParseNode) => void> {
return deserializeIntoTestUnionObject;
}

// Deserialization methods
export function deserializeIntoTestUnionObject(fooBar: Partial<TestUnionObject> | undefined = {}): Record<string, (node: ParseNode) => void> {
return {
...deserializeFooParser(fooBar as FooResponse),
...deserializeBarParser(fooBar as BarResponse),
};
}

export function serializeTestUnionObject(writer: SerializationWriter, fooBar: Partial<TestUnionObject> | undefined = {}): void {
serializeFoo(writer, fooBar as FooResponse);
serializeBar(writer, fooBar as BarResponse);
}
24 changes: 24 additions & 0 deletions packages/serialization/json/test/common/testUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export function deepEqual(obj1: any, obj2: any): boolean {
if (obj1 === obj2) {
return true;
}

if (obj1 == null || obj2 == null || typeof obj1 !== "object" || typeof obj2 !== "object") {
return false;
}

const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);

if (keys1.length !== keys2.length) {
return false;
}

for (const key of keys1) {
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
return false;
}
}

return true;
}
216 changes: 216 additions & 0 deletions packages/serialization/json/test/common/unionOfObjectsAndPrimitives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { type AdditionalDataHolder, type BaseRequestBuilder, type Parsable, type ParsableFactory, type ParseNode, type RequestConfiguration, type RequestInformation, type RequestsMetadata, type SerializationWriter } from "@microsoft/kiota-abstractions";

export interface Cat extends Parsable, Pet {
/**
* The favoriteToy property
*/
favoriteToy?: string;
}
/**
* Creates a new instance of the appropriate class based on discriminator value
* @param parseNode The parse node to use to read the discriminator value and create the object
* @returns {Cat}
*/
export function createCatFromDiscriminatorValue(parseNode: ParseNode | undefined): (instance?: Parsable) => Record<string, (node: ParseNode) => void> {
return deserializeIntoCat;
}
/**
* Creates a new instance of the appropriate class based on discriminator value
* @param parseNode The parse node to use to read the discriminator value and create the object
* @returns {Dog}
*/
export function createDogFromDiscriminatorValue(parseNode: ParseNode | undefined): (instance?: Parsable) => Record<string, (node: ParseNode) => void> {
return deserializeIntoDog;
}
/**
* Creates a new instance of the appropriate class based on discriminator value
* @param parseNode The parse node to use to read the discriminator value and create the object
* @returns {Pet}
*/
export function createPetFromDiscriminatorValue(parseNode: ParseNode | undefined): (instance?: Parsable) => Record<string, (node: ParseNode) => void> {
return deserializeIntoPet;
}
/**
* The deserialization information for the current model
* @returns {Record<string, (node: ParseNode) => void>}
*/
export function deserializeIntoCat(cat: Partial<Cat> | undefined = {}): Record<string, (node: ParseNode) => void> {
return {
...deserializeIntoPet(cat),
favoriteToy: (n) => {
cat.favoriteToy = n.getStringValue();
},
};
}
/**
* The deserialization information for the current model
* @returns {Record<string, (node: ParseNode) => void>}
*/
export function deserializeIntoDog(dog: Partial<Dog> | undefined = {}): Record<string, (node: ParseNode) => void> {
return {
...deserializeIntoPet(dog),
breed: (n) => {
dog.breed = n.getStringValue();
},
};
}
/**
* The deserialization information for the current model
* @returns {Record<string, (node: ParseNode) => void>}
*/
export function deserializeIntoPet(pet: Partial<Pet> | undefined = {}): Record<string, (node: ParseNode) => void> {
return {
age: (n) => {
pet.age = n.getNumberValue();
},
name: (n) => {
pet.name = n.getStringValue();
},
};
}
export interface Dog extends Parsable, Pet {
/**
* The breed property
*/
breed?: string;
}
export interface Pet extends AdditionalDataHolder, Parsable {
/**
* Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.
*/
additionalData?: Record<string, unknown>;
/**
* The age property
*/
age?: number;
/**
* The name property
*/
name?: string;
}
/**
* Serializes information the current object
* @param writer Serialization writer to use to serialize this model
*/
export function serializeCat(writer: SerializationWriter, cat: Partial<Cat> | undefined = {}): void {
serializePet(writer, cat);
writer.writeStringValue("favoriteToy", cat.favoriteToy);
}
/**
* Serializes information the current object
* @param writer Serialization writer to use to serialize this model
*/
export function serializeDog(writer: SerializationWriter, dog: Partial<Dog> | undefined = {}): void {
serializePet(writer, dog);
writer.writeStringValue("breed", dog.breed);
}
/**
* Serializes information the current object
* @param writer Serialization writer to use to serialize this model
*/
export function serializePet(writer: SerializationWriter, pet: Partial<Pet> | undefined = {}): void {
writer.writeNumberValue("age", pet.age);
writer.writeStringValue("name", pet.name);
writer.writeAdditionalData(pet.additionalData);
}
/* tslint:enable */
/* eslint-enable */
/**
* Creates a new instance of the appropriate class based on discriminator value
* @param parseNode The parse node to use to read the discriminator value and create the object
* @returns {Cat | Dog}
*/
export function createPetGetResponse_dataFromDiscriminatorValue(parseNode: ParseNode | undefined): (instance?: Parsable) => Record<string, (node: ParseNode) => void> {
return deserializeIntoPetGetResponse_data;
}
/**
* Creates a new instance of the appropriate class based on discriminator value
* @param parseNode The parse node to use to read the discriminator value and create the object
* @returns {PetGetResponse}
*/
export function createPetGetResponseFromDiscriminatorValue(parseNode: ParseNode | undefined): (instance?: Parsable) => Record<string, (node: ParseNode) => void> {
return deserializeIntoPetGetResponse;
}
/**
* The deserialization information for the current model
* @returns {Record<string, (node: ParseNode) => void>}
*/
export function deserializeIntoPetGetResponse(petGetResponse: Partial<PetGetResponse> | undefined = {}): Record<string, (node: ParseNode) => void> {
return {
data: (n) => {
petGetResponse.data = n.getNumberValue() ?? n.getStringValue() ?? n.getObjectValue<Cat | Dog>(createPetGetResponse_dataFromDiscriminatorValue);
},
request_id: (n) => {
petGetResponse.request_id = n.getStringValue();
},
};
}
/**
* The deserialization information for the current model
* @returns {Record<string, (node: ParseNode) => void>}
*/
export function deserializeIntoPetGetResponse_data(petGetResponse_data: Partial<Cat | Dog> | undefined = {}): Record<string, (node: ParseNode) => void> {
return {
...deserializeIntoCat(petGetResponse_data as Cat),
...deserializeIntoDog(petGetResponse_data as Dog),
};
}
export interface PetGetResponse extends AdditionalDataHolder, Parsable {
/**
* Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.
*/
additionalData?: Record<string, unknown>;
/**
* The data property
*/
data?: Cat | Dog | number | string;
/**
* The request_id property
*/
request_id?: string;
}
export type PetGetResponse_data = Cat | Dog | number | string;
/**
* Builds and executes requests for operations under /pet
*/
export interface PetRequestBuilder extends BaseRequestBuilder<PetRequestBuilder> {
/**
* Get pet information
* @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options.
* @returns {Promise<PetGetResponse>}
*/
get(requestConfiguration?: RequestConfiguration<object> | undefined): Promise<PetGetResponse | undefined>;
/**
* Get pet information
* @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options.
* @returns {RequestInformation}
*/
toGetRequestInformation(requestConfiguration?: RequestConfiguration<object> | undefined): RequestInformation;
}
/**
* Serializes information the current object
* @param writer Serialization writer to use to serialize this model
*/
export function serializePetGetResponse(writer: SerializationWriter, petGetResponse: Partial<PetGetResponse> | undefined = {}): void {
switch (true) {
case typeof petGetResponse.data === "number":
writer.writeNumberValue("data", petGetResponse.data);
break;
case typeof petGetResponse.data === "string":
writer.writeStringValue("data", petGetResponse.data);
break;
default:
writer.writeObjectValue<Cat | Dog>("data", petGetResponse.data, serializePetGetResponse_data);
break;
}
writer.writeStringValue("request_id", petGetResponse.request_id);
writer.writeAdditionalData(petGetResponse.additionalData);
}
/**
* Serializes information the current object
* @param writer Serialization writer to use to serialize this model
*/
export function serializePetGetResponse_data(writer: SerializationWriter, petGetResponse_data: Partial<Cat | Dog> | undefined = {}): void {
serializeCat(writer, petGetResponse_data as Cat);
serializeDog(writer, petGetResponse_data as Dog);
}
Loading

0 comments on commit 33d4023

Please sign in to comment.