Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: process null values #1248

Merged
merged 17 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ import type { Parsable } from "./parsable";
import type { ParseNode } from "./parseNode";
import type { SerializationWriter } from "./serializationWriter";

export type ModelSerializerFunction<T extends Parsable> = (writer: SerializationWriter, value?: Partial<T> | undefined) => void;
export type ModelSerializerFunction<T extends Parsable> = (writer: SerializationWriter, value?: Partial<T> | null | undefined) => void;

export type DeserializeIntoModelFunction<T extends Parsable> = (value?: Partial<T> | undefined) => Record<string, (node: ParseNode) => void>;
24 changes: 12 additions & 12 deletions packages/abstractions/src/serialization/serializationWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,74 +25,74 @@ export interface SerializationWriter {
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeStringValue(key?: string | undefined, value?: string | undefined): void;
writeStringValue(key?: string | undefined, value?: string | null | undefined): void;
baywet marked this conversation as resolved.
Show resolved Hide resolved
/**
* Writes the specified boolean value to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeBooleanValue(key?: string | undefined, value?: boolean | undefined): void;
writeBooleanValue(key?: string | undefined, value?: boolean | null | undefined): void;
/**
* Writes the specified number value to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeNumberValue(key?: string | undefined, value?: number | undefined): void;
writeNumberValue(key?: string | undefined, value?: number | null | undefined): void;
/**
* Writes the specified Guid value to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeGuidValue(key?: string | undefined, value?: Guid | undefined): void;
writeGuidValue(key?: string | undefined, value?: Guid | null | undefined): void;
/**
* Writes the specified Date value to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeDateValue(key?: string | undefined, value?: Date | undefined): void;
writeDateValue(key?: string | undefined, value?: Date | null | undefined): void;
/**
* Writes the specified Duration value to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeDurationValue(key?: string | undefined, value?: Duration | undefined): void;
writeDurationValue(key?: string | undefined, value?: Duration | null | undefined): void;
/**
* Writes the specified TimeOnly value to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeTimeOnlyValue(key?: string | undefined, value?: TimeOnly | undefined): void;
writeTimeOnlyValue(key?: string | undefined, value?: TimeOnly | null | undefined): void;
/**
* Writes the specified DateOnly value to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeDateOnlyValue(key?: string | undefined, value?: DateOnly | undefined): void;
writeDateOnlyValue(key?: string | undefined, value?: DateOnly | null | undefined): void;
/**
* Writes the specified collection of primitive values to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeCollectionOfPrimitiveValues<T>(key?: string | undefined, values?: T[] | undefined): void;
writeCollectionOfPrimitiveValues<T>(key?: string | undefined, values?: T[] | null | undefined): void;
/**
* Writes the specified collection of object values to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeCollectionOfObjectValues<T extends Parsable>(key?: string | undefined, values?: T[], serializerMethod?: ModelSerializerFunction<T>): void;
writeCollectionOfObjectValues<T extends Parsable>(key?: string | undefined, values?: T[] | null, serializerMethod?: ModelSerializerFunction<T>): void;
/**
* Writes the specified model object value to the stream with an optional given key.
* @param key the key to write the value with.
* @param value the value to write to the stream.
*/
writeObjectValue<T extends Parsable>(key?: string | undefined, value?: T | undefined, serializerMethod?: ModelSerializerFunction<T>): void;
writeObjectValue<T extends Parsable>(key?: string | undefined, value?: T | null | undefined, serializerMethod?: ModelSerializerFunction<T>): void;

/**
* Writes the specified enum value to the stream with an optional given key.
* @param key the key to write the value with.
* @param values the value to write to the stream.
*/
writeEnumValue<T>(key?: string | undefined, ...values: (T | undefined)[]): void;
writeEnumValue<T>(key?: string | undefined, ...values: (T | null | undefined)[]): void;
/**
* Writes a null value for the specified key.
* @param key the key to write the value with.
Expand Down
6 changes: 4 additions & 2 deletions packages/abstractions/src/serialization/untypedNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import type { Parsable } from "./parsable";
import type { ParseNode } from "./parseNode";
import type { SerializationWriter } from "./serializationWriter";
import type { ParsableFactory } from "./parsableFactory";
import type { DeserializeIntoModelFunction } from "./serializationFunctionTypes";

/** Defines the base interface for defining an untyped node. */
export interface UntypedNode extends Parsable {
Expand All @@ -25,7 +27,7 @@ export interface UntypedNode extends Parsable {
/**
* Factory to create an UntypedNode from a string during deserialization.
*/
export const createUntypedNodeFromDiscriminatorValue = (_parseNode: ParseNode | undefined): ((_instance?: Parsable) => Record<string, (_node: ParseNode) => void>) => {
export const createUntypedNodeFromDiscriminatorValue: ParsableFactory<UntypedNode> = (_parseNode: ParseNode | undefined): ((_instance?: Parsable) => Record<string, (_node: ParseNode) => void>) => {
return deserializeIntoUntypedNode;
};

Expand All @@ -42,7 +44,7 @@ export const isUntypedNode = (node: unknown): node is UntypedNode => {
/**
* The deserialization implementation for UntypedNode.
*/
export const deserializeIntoUntypedNode = (untypedNode: Partial<UntypedNode> | undefined = {}): Record<string, (node: ParseNode) => void> => {
export const deserializeIntoUntypedNode: DeserializeIntoModelFunction<UntypedNode> = (untypedNode: Partial<UntypedNode> | undefined = {}): Record<string, (node: ParseNode) => void> => {
return {
value: (_n) => {
untypedNode.value = null;
Expand Down
81 changes: 56 additions & 25 deletions packages/serialization/form/src/formSerializationWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class FormSerializationWriter implements SerializationWriter {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
key?: string | undefined,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
value?: ArrayBuffer | undefined,
value?: ArrayBuffer | null | undefined,
): void {
throw new Error("serialization of byt arrays is not supported with URI encoding");
}
Expand All @@ -24,7 +24,10 @@ export class FormSerializationWriter implements SerializationWriter {
public onBeforeObjectSerialization: ((value: Parsable) => void) | undefined;
public onAfterObjectSerialization: ((value: Parsable) => void) | undefined;
public onStartObjectSerialization: ((value: Parsable, writer: SerializationWriter) => void) | undefined;
public writeStringValue = (key?: string, value?: string): void => {
public writeStringValue = (key?: string, value?: string | null): void => {
if (value === null) {
value = "null";
}
if (key && value) {
this.writePropertyName(key);
this.writer.push(`=${encodeURIComponent(value)}`);
Expand All @@ -34,31 +37,52 @@ export class FormSerializationWriter implements SerializationWriter {
private writePropertyName = (key: string): void => {
this.writer.push(encodeURIComponent(key));
};
public writeBooleanValue = (key?: string, value?: boolean): void => {
value !== null && value !== undefined && this.writeStringValue(key, `${value}`);
private shouldWriteValueOrNull = <T>(key?: string, value?: T | null): boolean => {
if (value === null) {
this.writeNullValue(key);
return false;
}
return true;
};
public writeNumberValue = (key?: string, value?: number): void => {
value && this.writeStringValue(key, `${value}`);
public writeBooleanValue = (key?: string, value?: boolean | null): void => {
if (this.shouldWriteValueOrNull(key, value)) {
value !== undefined && this.writeStringValue(key, `${value}`);
}
};
public writeGuidValue = (key?: string, value?: Guid): void => {
value && this.writeStringValue(key, `${value}`);
public writeNumberValue = (key?: string, value?: number | null): void => {
if (this.shouldWriteValueOrNull(key, value)) {
value && this.writeStringValue(key, `${value}`);
}
};
public writeDateValue = (key?: string, value?: Date): void => {
value && this.writeStringValue(key, value.toISOString());
public writeGuidValue = (key?: string, value?: Guid | null): void => {
if (this.shouldWriteValueOrNull(key, value)) {
value && this.writeStringValue(key, value.toString());
}
};
public writeDateOnlyValue = (key?: string, value?: DateOnly): void => {
value && this.writeStringValue(key, value.toString());
public writeDateValue = (key?: string, value?: Date | null): void => {
if (this.shouldWriteValueOrNull(key, value)) {
value && this.writeStringValue(key, value.toISOString());
}
};
public writeTimeOnlyValue = (key?: string, value?: TimeOnly): void => {
value && this.writeStringValue(key, value.toString());
public writeDateOnlyValue = (key?: string, value?: DateOnly | null): void => {
if (this.shouldWriteValueOrNull(key, value)) {
value && this.writeStringValue(key, value.toString());
}
};
public writeDurationValue = (key?: string, value?: Duration): void => {
value && this.writeStringValue(key, value.toString());
public writeTimeOnlyValue = (key?: string, value?: TimeOnly | null): void => {
if (this.shouldWriteValueOrNull(key, value)) {
value && this.writeStringValue(key, value.toString());
}
};
public writeDurationValue = (key?: string, value?: Duration | null): void => {
if (this.shouldWriteValueOrNull(key, value)) {
value && this.writeStringValue(key, value.toString());
}
};
public writeNullValue = (key?: string): void => {
this.writeStringValue(key, `null`);
key && this.writeStringValue(key, null);
};
public writeCollectionOfPrimitiveValues = <T>(_key?: string, _values?: T[]): void => {
public writeCollectionOfPrimitiveValues = <T>(_key?: string, _values?: T[] | null): void => {
if (_key && _values) {
_values.forEach((val) => {
this.writeAnyValue(_key, val);
Expand All @@ -69,14 +93,19 @@ export class FormSerializationWriter implements SerializationWriter {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_key?: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_values?: T[],
_values?: T[] | null,
): void => {
throw new Error(`serialization of collections is not supported with URI encoding`);
};
public writeObjectValue = <T extends Parsable>(key: string | undefined, value: T | undefined, serializerMethod: ModelSerializerFunction<T>): void => {
public writeObjectValue = <T extends Parsable>(key: string | undefined, value: T | null | undefined, serializerMethod: ModelSerializerFunction<T>): void => {
if (++this.depth > 0) {
throw new Error(`serialization of nested objects is not supported with URI encoding`);
}

if (!this.shouldWriteValueOrNull(key, value)) {
return;
}

if (value) {
if (key) {
this.writePropertyName(key);
Expand All @@ -92,7 +121,7 @@ export class FormSerializationWriter implements SerializationWriter {
key && this.writer.push(FormSerializationWriter.propertySeparator);
}
};
public writeEnumValue = <T>(key?: string | undefined, ...values: (T | undefined)[]): void => {
public writeEnumValue = <T>(key?: string | undefined, ...values: (T | null | undefined)[]): void => {
if (values.length > 0) {
const rawValues = values.filter((x) => x !== undefined).map((x) => `${x}`);
if (rawValues.length > 0) {
Expand Down Expand Up @@ -121,8 +150,12 @@ export class FormSerializationWriter implements SerializationWriter {
}
};

private writeAnyValue = (key?: string | undefined, value?: unknown | undefined): void => {
if (value !== null && value !== undefined) {
private writeAnyValue = (key?: string | undefined, value?: unknown | null | undefined): void => {
if (value === null) {
return this.writeNullValue(key);
}

if (value !== undefined) {
const valueType = typeof value;
if (valueType === "boolean") {
this.writeBooleanValue(key, value as any as boolean);
Expand All @@ -141,8 +174,6 @@ export class FormSerializationWriter implements SerializationWriter {
} else {
throw new Error(`encountered unknown ${value} value type during serialization ${valueType} for key ${key}`);
}
} else {
this.writeNullValue(key);
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ describe("FormSerializationWriter", () => {
month: 9,
day: 4,
});
testEntity.officeLocation = null;
testEntity.endWorkTime = null;
testEntity.additionalData = {};
testEntity.additionalData["mobilePhone"] = null;
testEntity.additionalData["accountEnabled"] = false;
Expand All @@ -41,12 +43,14 @@ describe("FormSerializationWriter", () => {
"birthday=2017-09-04", // Serializes dates
"workDuration=PT1H", // Serializes timespans
"startWorkTime=08%3A00%3A00.0000000", //Serializes times
"mobilePhone=null", // Serializes null values
"mobilePhone=null", // Serializes null values in additionalData
"accountEnabled=false",
"jobTitle=Author",
"createdDateTime=1970-01-01T00%3A00%3A00.000Z",
"deviceNames=device1",
"deviceNames=device2", // Serializes collections
"officeLocation=null", // Serializes null values
"endWorkTime=null", // Serializes null values
];
const arr = form.split("&");
let count = 0;
Expand Down
16 changes: 8 additions & 8 deletions packages/serialization/form/test/testEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
import type { AdditionalDataHolder, DateOnly, Duration, Parsable, ParseNode, SerializationWriter, TimeOnly } from "@microsoft/kiota-abstractions";

export interface TestEntity extends Parsable, AdditionalDataHolder {
id?: string;
birthday?: DateOnly;
createdDateTime?: Date;
workDuration?: Duration;
startWorkTime?: TimeOnly;
endWorkTime?: TimeOnly;
officeLocation?: string;
deviceNames?: string[];
id?: string | null;
birthday?: DateOnly | null;
createdDateTime?: Date | null;
workDuration?: Duration | null;
startWorkTime?: TimeOnly | null;
endWorkTime?: TimeOnly | null;
officeLocation?: string | null;
deviceNames?: string[] | null;
}
export function createTestParserFromDiscriminatorValue(parseNode: ParseNode | undefined) {
if (!parseNode) throw new Error("parseNode cannot be undefined");
Expand Down
32 changes: 16 additions & 16 deletions packages/serialization/json/src/jsonParseNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export class JsonParseNode implements ParseNode {
constructor(private readonly _jsonNode: unknown) {}
public onBeforeAssignFieldValues: ((value: Parsable) => void) | undefined;
public onAfterAssignFieldValues: ((value: Parsable) => void) | undefined;
baywet marked this conversation as resolved.
Show resolved Hide resolved
public getStringValue = () => (typeof this._jsonNode === "string" ? (this._jsonNode as string) : undefined);
public getChildNode = (identifier: string): ParseNode | undefined => (this._jsonNode && typeof this._jsonNode === "object" && (this._jsonNode as { [key: string]: any })[identifier] !== undefined ? new JsonParseNode((this._jsonNode as { [key: string]: any })[identifier]) : undefined);
public getBooleanValue = () => (typeof this._jsonNode === "boolean" ? (this._jsonNode as boolean) : undefined);
public getNumberValue = () => (typeof this._jsonNode === "number" ? (this._jsonNode as number) : undefined);
public getStringValue = () => (typeof this._jsonNode === "string" ? this._jsonNode : undefined);
public getChildNode = (identifier: string): ParseNode | undefined => (this._jsonNode && typeof this._jsonNode === "object" && (this._jsonNode as Record<string, unknown>)[identifier] !== undefined ? new JsonParseNode((this._jsonNode as Record<string, unknown>)[identifier]) : undefined);
public getBooleanValue = () => (typeof this._jsonNode === "boolean" ? this._jsonNode : undefined);
public getNumberValue = () => (typeof this._jsonNode === "number" ? this._jsonNode : undefined);
public getGuidValue = () => parseGuidString(this.getStringValue());
public getDateValue = () => (this._jsonNode ? new Date(this._jsonNode as string) : undefined);
public getDateOnlyValue = () => DateOnly.parse(this.getStringValue());
Expand Down Expand Up @@ -66,26 +66,26 @@ export class JsonParseNode implements ParseNode {
const valueType = typeof this._jsonNode;
let value: T = temp;
if (valueType === "boolean") {
value = createUntypedBoolean(this._jsonNode as boolean) as any as T;
value = createUntypedBoolean(this._jsonNode as boolean) as unknown as T;
} else if (valueType === "string") {
value = createUntypedString(this._jsonNode as string) as any as T;
value = createUntypedString(this._jsonNode as string) as unknown as T;
} else if (valueType === "number") {
value = createUntypedNumber(this._jsonNode as number) as any as T;
value = createUntypedNumber(this._jsonNode as number) as unknown as T;
} else if (Array.isArray(this._jsonNode)) {
const nodes: UntypedNode[] = [];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(this._jsonNode as any[]).forEach((x) => {
this._jsonNode.forEach((x) => {
nodes.push(new JsonParseNode(x).getObjectValue(createUntypedNodeFromDiscriminatorValue));
});
value = createUntypedArray(nodes) as any as T;
value = createUntypedArray(nodes) as unknown as T;
} else if (this._jsonNode && valueType === "object") {
const properties: Record<string, UntypedNode> = {};
Object.entries(this._jsonNode as any).forEach(([k, v]) => {
Object.entries(this._jsonNode).forEach(([k, v]) => {
properties[k] = new JsonParseNode(v).getObjectValue(createUntypedNodeFromDiscriminatorValue);
});
value = createUntypedObject(properties) as any as T;
value = createUntypedObject(properties) as unknown as T;
} else if (!this._jsonNode) {
value = createUntypedNull() as any as T;
value = createUntypedNull() as unknown as T;
}
return value;
}
Expand All @@ -104,7 +104,7 @@ export class JsonParseNode implements ParseNode {
private assignFieldValues = <T extends Parsable>(model: T, parsableFactory: ParsableFactory<T>): void => {
const fields = parsableFactory(this)(model);
if (!this._jsonNode) return;
Object.entries(this._jsonNode as any).forEach(([k, v]) => {
Object.entries(this._jsonNode).forEach(([k, v]) => {
const deserializer = fields[k];
if (deserializer) {
deserializer(new JsonParseNode(v));
Expand All @@ -114,7 +114,7 @@ export class JsonParseNode implements ParseNode {
}
});
};
public getCollectionOfEnumValues = <T>(type: any): T[] => {
public getCollectionOfEnumValues = <T>(type: unknown): T[] => {
if (Array.isArray(this._jsonNode)) {
return this._jsonNode
.map((x) => {
Expand All @@ -125,11 +125,11 @@ export class JsonParseNode implements ParseNode {
}
return [];
};
public getEnumValue = <T>(type: any): T | undefined => {
public getEnumValue = <T>(type: unknown): T | undefined => {
const rawValue = this.getStringValue();
if (!rawValue) {
return undefined;
}
return type[toFirstCharacterUpper(rawValue)];
return (type as Record<string, T>)[toFirstCharacterUpper(rawValue)];
};
}
Loading
Loading