diff --git a/packages/abstractions/src/serialization/index.ts b/packages/abstractions/src/serialization/index.ts index 6d9d87903..e5235b6d0 100644 --- a/packages/abstractions/src/serialization/index.ts +++ b/packages/abstractions/src/serialization/index.ts @@ -1,4 +1,6 @@ export * from "./additionalDataHolder"; +export * from "./kiotaJsonSerializer"; +export * from "./kiotaSerializer"; export * from "./parsable"; export * from "./parsableFactory"; export * from "./parseNode"; diff --git a/packages/abstractions/src/serialization/kiotaJsonSerializer.ts b/packages/abstractions/src/serialization/kiotaJsonSerializer.ts new file mode 100644 index 000000000..84355927b --- /dev/null +++ b/packages/abstractions/src/serialization/kiotaJsonSerializer.ts @@ -0,0 +1,94 @@ +import { + deserialize, + deserializeCollection, + serialize, + serializeCollection, + serializeCollectionToString as serializeCollectionAsString, + serializeToString as serializeAsString, +} from "./kiotaSerializer"; +import type { Parsable } from "./parsable"; +import type { ParsableFactory } from "./parsableFactory"; +import type { ModelSerializerFunction } from "./serializationFunctionTypes"; + +const jsonContentType = "application/json"; +/** + * Serializes a parsable object into a buffer + * @param value the value to serialize + * @param serializationFunction the serialization function for the model type + * @returns a buffer containing the serialized value + */ +export function serializeToJson( + value: T, + serializationFunction: ModelSerializerFunction, +): ArrayBuffer { + return serialize(jsonContentType, value, serializationFunction); +} + +/** + * Serializes a parsable object into a string representation + * @param value the value to serialize + * @param serializationFunction the serialization function for the model type + * @returns a string representing the serialized value + */ +export function serializeToJsonAsString( + value: T, + serializationFunction: ModelSerializerFunction, +): string { + return serializeAsString(jsonContentType, value, serializationFunction); +} + +/** + * Serializes a collection of parsable objects into a buffer + * @param value the value to serialize + * @param serializationFunction the serialization function for the model type + * @returns a string representing the serialized value + */ +export function serializeCollectionToJson( + values: T[], + serializationFunction: ModelSerializerFunction, +): ArrayBuffer { + return serializeCollection(jsonContentType, values, serializationFunction); +} + +/** + * Serializes a collection of parsable objects into a string representation + * @param value the value to serialize + * @param serializationFunction the serialization function for the model type + * @returns a string representing the serialized value + */ +export function serializeCollectionToJsonAsString( + values: T[], + serializationFunction: ModelSerializerFunction, +): string { + return serializeCollectionAsString( + jsonContentType, + values, + serializationFunction, + ); +} + +/** + * Deserializes a buffer into a parsable object + * @param bufferOrString the value to serialize + * @param factory the factory for the model type + * @returns the deserialized parsable object + */ +export function deserializeFromJson( + bufferOrString: ArrayBuffer | string, + factory: ParsableFactory, +): Parsable { + return deserialize(jsonContentType, bufferOrString, factory); +} + +/** + * Deserializes a buffer into a a collection of parsable object + * @param bufferOrString the value to serialize + * @param factory the factory for the model type + * @returns the deserialized collection of parsable objects + */ +export function deserializeCollectionFromJson( + bufferOrString: ArrayBuffer | string, + factory: ParsableFactory, +): T[] | undefined { + return deserializeCollection(jsonContentType, bufferOrString, factory); +} diff --git a/packages/abstractions/src/serialization/kiotaSerializer.ts b/packages/abstractions/src/serialization/kiotaSerializer.ts new file mode 100644 index 000000000..aab4288dc --- /dev/null +++ b/packages/abstractions/src/serialization/kiotaSerializer.ts @@ -0,0 +1,172 @@ +import type { Parsable } from "./parsable"; +import type { ParsableFactory } from "./parsableFactory"; +import type { ParseNode } from "./parseNode"; +import { ParseNodeFactoryRegistry } from "./parseNodeFactoryRegistry"; +import type { ModelSerializerFunction } from "./serializationFunctionTypes"; +import type { SerializationWriter } from "./serializationWriter"; +import { SerializationWriterFactoryRegistry } from "./serializationWriterFactoryRegistry"; + +/** + * Serializes a parsable object into a buffer + * @param contentType the content type to serialize to + * @param value the value to serialize + * @param serializationFunction the serialization function for the model type + * @returns a buffer containing the serialized value + */ +export function serialize( + contentType: string, + value: T, + serializationFunction: ModelSerializerFunction, +): ArrayBuffer { + const writer = getSerializationWriter( + contentType, + value, + serializationFunction, + ); + writer.writeObjectValue(undefined, value, serializationFunction); + return writer.getSerializedContent(); +} +/** + * Serializes a parsable object into a string representation + * @param contentType the content type to serialize to + * @param value the value to serialize + * @param serializationFunction the serialization function for the model type + * @returns a string representing the serialized value + */ +export function serializeToString( + contentType: string, + value: T, + serializationFunction: ModelSerializerFunction, +): string { + const buffer = serialize(contentType, value, serializationFunction); + return getStringValueFromBuffer(buffer); +} +/** + * Serializes a collection of parsable objects into a buffer + * @param contentType the content type to serialize to + * @param value the value to serialize + * @param serializationFunction the serialization function for the model type + * @returns a string representing the serialized value + */ +export function serializeCollection( + contentType: string, + values: T[], + serializationFunction: ModelSerializerFunction, +): ArrayBuffer { + const writer = getSerializationWriter( + contentType, + values, + serializationFunction, + ); + writer.writeCollectionOfObjectValues( + undefined, + values, + serializationFunction, + ); + return writer.getSerializedContent(); +} + +/** + * Serializes a collection of parsable objects into a string representation + * @param contentType the content type to serialize to + * @param value the value to serialize + * @param serializationFunction the serialization function for the model type + * @returns a string representing the serialized value + */ +export function serializeCollectionToString( + contentType: string, + values: T[], + serializationFunction: ModelSerializerFunction, +): string { + const buffer = serializeCollection( + contentType, + values, + serializationFunction, + ); + return getStringValueFromBuffer(buffer); +} + +function getSerializationWriter( + contentType: string, + value: unknown, + serializationFunction: unknown, +): SerializationWriter { + if (!contentType) { + throw new Error("content type cannot be undefined or empty"); + } + if (!value) { + throw new Error("value cannot be undefined"); + } + if (!serializationFunction) { + throw new Error("serializationFunction cannot be undefined"); + } + return SerializationWriterFactoryRegistry.defaultInstance.getSerializationWriter( + contentType, + ); +} + +function getStringValueFromBuffer(buffer: ArrayBuffer): string { + const decoder = new TextDecoder(); + return decoder.decode(buffer); +} + +/** + * Deserializes a buffer into a parsable object + * @param contentType the content type to serialize to + * @param bufferOrString the value to serialize + * @param factory the factory for the model type + * @returns the deserialized parsable object + */ +export function deserialize( + contentType: string, + bufferOrString: ArrayBuffer | string, + factory: ParsableFactory, +): Parsable { + if (typeof bufferOrString === "string") { + bufferOrString = getBufferFromString(bufferOrString); + } + const reader = getParseNode(contentType, bufferOrString, factory); + return reader.getObjectValue(factory); +} +function getParseNode( + contentType: string, + buffer: ArrayBuffer, + factory: unknown, +): ParseNode { + if (!contentType) { + throw new Error("content type cannot be undefined or empty"); + } + if (!buffer) { + throw new Error("buffer cannot be undefined"); + } + if (!factory) { + throw new Error("factory cannot be undefined"); + } + return ParseNodeFactoryRegistry.defaultInstance.getRootParseNode( + contentType, + buffer, + ); +} +/** + * Deserializes a buffer into a a collection of parsable object + * @param contentType the content type to serialize to + * @param bufferOrString the value to serialize + * @param factory the factory for the model type + * @returns the deserialized collection of parsable objects + */ +export function deserializeCollection( + contentType: string, + bufferOrString: ArrayBuffer | string, + factory: ParsableFactory, +): T[] | undefined { + if (typeof bufferOrString === "string") { + bufferOrString = getBufferFromString(bufferOrString); + } + const reader = getParseNode(contentType, bufferOrString, factory); + return reader.getCollectionOfObjectValues(factory); +} + +function getBufferFromString(value: string): ArrayBuffer { + const encoder = new TextEncoder(); + return encoder.encode(value).buffer; +} diff --git a/packages/serialization/json/test/common/kiotaSerializer.ts b/packages/serialization/json/test/common/kiotaSerializer.ts new file mode 100644 index 000000000..2aeb0ca01 --- /dev/null +++ b/packages/serialization/json/test/common/kiotaSerializer.ts @@ -0,0 +1,289 @@ +import { + deserialize, + deserializeCollection, + deserializeFromJson, + type ModelSerializerFunction, + type Parsable, + type ParsableFactory, + type ParseNode, + type ParseNodeFactory, + ParseNodeFactoryRegistry, + type SerializationWriter, + type SerializationWriterFactory, + SerializationWriterFactoryRegistry, + serialize, + serializeCollection, + serializeCollectionToJsonAsString, + serializeCollectionToString, + serializeToJsonAsString, + serializeToString, +} from "@microsoft/kiota-abstractions"; +import { assert } from "chai"; + +import { + createTestBackedModelFromDiscriminatorValue, + serializeTestBackModel, + type TestBackedModel, +} from "./testEntity"; +const jsonContentType = "application/json"; +describe("kiotaSerializer", () => { + it("defends serialize", () => { + assert.throws(() => + serialize( + "", + undefined as unknown as Parsable, + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serialize( + jsonContentType, + undefined as unknown as Parsable, + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serialize( + jsonContentType, + {} as unknown as Parsable, + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serializeCollection( + "", + undefined as unknown as Parsable[], + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serializeCollection( + jsonContentType, + undefined as unknown as Parsable[], + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serializeCollection( + jsonContentType, + [], + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serializeToString( + "", + undefined as unknown as Parsable, + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serializeToString( + jsonContentType, + undefined as unknown as Parsable, + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serializeToString( + jsonContentType, + {} as unknown as Parsable, + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serializeCollectionToString( + "", + undefined as unknown as Parsable[], + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serializeCollectionToString( + jsonContentType, + undefined as unknown as Parsable[], + undefined as unknown as ModelSerializerFunction, + ), + ); + assert.throws(() => + serializeCollectionToString( + jsonContentType, + [], + undefined as unknown as ModelSerializerFunction, + ), + ); + }); + it("defends deserialize", () => { + assert.throws(() => + deserialize( + "", + "", + undefined as unknown as ParsableFactory, + ), + ); + assert.throws(() => + deserialize( + jsonContentType, + "", + undefined as unknown as ParsableFactory, + ), + ); + assert.throws(() => + deserialize( + jsonContentType, + "{}", + undefined as unknown as ParsableFactory, + ), + ); + assert.throws(() => + deserializeCollection( + "", + "", + undefined as unknown as ParsableFactory, + ), + ); + assert.throws(() => + deserializeCollection( + jsonContentType, + "", + undefined as unknown as ParsableFactory, + ), + ); + assert.throws(() => + deserializeCollection( + jsonContentType, + "{}", + undefined as unknown as ParsableFactory, + ), + ); + }); + it("Serializes an object", () => { + registerMockSerializer(`{"id": "123"}`); + const testEntity = { + id: "123", + } as TestBackedModel; + + const result = serializeToJsonAsString(testEntity, serializeTestBackModel); + + assert.equal(result, `{"id": "123"}`); + + SerializationWriterFactoryRegistry.defaultInstance.contentTypeAssociatedFactories.clear(); + }); + it("Serializes a collection", () => { + registerMockSerializer(`[{"id": "123"}]`); + const testEntity = { + id: "123", + } as TestBackedModel; + + const result = serializeCollectionToJsonAsString( + [testEntity], + serializeTestBackModel, + ); + + assert.equal(result, `[{"id": "123"}]`); + + SerializationWriterFactoryRegistry.defaultInstance.contentTypeAssociatedFactories.clear(); + }); + it("Deserializes an object", () => { + registerMockParseNode({ + id: "123", + } as TestBackedModel); + const result = deserializeFromJson( + `{"id": "123"}`, + createTestBackedModelFromDiscriminatorValue, + ); + + assert.deepEqual(result, { + id: "123", + } as TestBackedModel); + + ParseNodeFactoryRegistry.defaultInstance.contentTypeAssociatedFactories.clear(); + }); + it("Deserializes a collection", () => { + registerMockParseNode([ + { + id: "123", + } as TestBackedModel, + ]); + const result = deserializeFromJson( + `[{"id": "123"}]`, + createTestBackedModelFromDiscriminatorValue, + ); + + assert.deepEqual(result, [ + { + id: "123", + } as TestBackedModel, + ]); + + ParseNodeFactoryRegistry.defaultInstance.contentTypeAssociatedFactories.clear(); + }); +}); +function registerMockParseNode(value: unknown): void { + const mockParseNode = { + getObjectValue( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + parsableFactory: ParsableFactory, + ): T { + return value as T; + }, + getCollectionOfObjectValues( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + parsableFactory: ParsableFactory, + ): T[] | undefined { + return value as T[]; + }, + } as unknown as ParseNode; + const mockParseNodeFactory = { + getRootParseNode( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + contentType: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + buffer: ArrayBuffer, + ) { + return mockParseNode; + }, + } as unknown as ParseNodeFactory; + ParseNodeFactoryRegistry.defaultInstance.contentTypeAssociatedFactories.set( + jsonContentType, + mockParseNodeFactory, + ); +} +function registerMockSerializer(value: string): void { + const mockSerializationWriter = { + getSerializedContent(): ArrayBuffer { + const encoder = new TextEncoder(); + return encoder.encode(value).buffer; + }, + writeObjectValue( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + key?: string | undefined, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + value?: T | undefined, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + serializerMethod?: ModelSerializerFunction, + ): void { + return; + }, + writeCollectionOfObjectValues( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + key?: string | undefined, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + values?: T[], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + serializerMethod?: ModelSerializerFunction, + ): void { + return; + }, + } as unknown as SerializationWriter; + const mockSerializationWriterFactory = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getSerializationWriter(contentType: string, value: unknown) { + return mockSerializationWriter; + }, + } as unknown as SerializationWriterFactory; + SerializationWriterFactoryRegistry.defaultInstance.contentTypeAssociatedFactories.set( + jsonContentType, + mockSerializationWriterFactory, + ); +} diff --git a/packages/serialization/json/test/common/testEntity.ts b/packages/serialization/json/test/common/testEntity.ts index 3a06bfea5..5e2e60feb 100644 --- a/packages/serialization/json/test/common/testEntity.ts +++ b/packages/serialization/json/test/common/testEntity.ts @@ -10,6 +10,7 @@ export interface TestParser { additionalData?: Record; testDate?: Date | undefined; foos?: FooResponse[] | undefined; + id?: string | undefined; } export interface TestBackedModel extends TestParser, BackedModel { backingStoreEnabled?: boolean | undefined; @@ -95,7 +96,10 @@ export function deserializeTestBackedModel( }, foos: (n) => { testParser.foos = n.getCollectionOfObjectValues(createFooParserFromDiscriminatorValue); - } + }, + id: (n) => { + testParser.id = n.getStringValue(); + }, }; } @@ -149,3 +153,20 @@ export function serializeTestParser( writer.writeObjectValue("testObject", entity.testObject, serializeTestObject); writer.writeAdditionalData(entity.additionalData); } + +export function serializeTestBackModel( + writer: SerializationWriter, + entity: TestBackedModel | undefined = {}, +): void { + writer.writeCollectionOfPrimitiveValues( + "testCollection", + entity.testCollection, + ); + writer.writeStringValue("testString", entity.testString); + writer.writeStringValue("testComplexString", entity.testComplexString); + writer.writeStringValue("id", entity.id); + + writer.writeDateValue("testDate", entity.testDate); + writer.writeObjectValue("testObject", entity.testObject, serializeTestObject); + writer.writeAdditionalData(entity.additionalData); +}