Skip to content

Commit

Permalink
Merge pull request #1248 from Kindest13/fix/process-null-values
Browse files Browse the repository at this point in the history
fix: process null values
  • Loading branch information
baywet authored Aug 13, 2024
2 parents b208177 + 79c1c8e commit 0e10522
Show file tree
Hide file tree
Showing 48 changed files with 828 additions and 397 deletions.
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;
/**
* 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;
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

0 comments on commit 0e10522

Please sign in to comment.