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

Feature/support enum as root payload #1117

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
21 changes: 21 additions & 0 deletions packages/abstractions/src/apiClientProxifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ function send(
metadata.responseBodyFactory as ParsableFactory<Parsable>,
metadata.errorMappings,
);
case "sendEnum":
if (!metadata.enumObject) {
throw new Error("couldn't find response body factory");
}
return requestAdapter.sendEnum(
requestInfo,
metadata.enumObject,
metadata.errorMappings,
);
case "sendCollectionOfEnum":
if (!metadata.enumObject) {
throw new Error("couldn't find response body factory");
}
return requestAdapter.sendCollectionOfEnum(
requestInfo,
metadata.enumObject,
metadata.errorMappings,
);
case "sendCollectionOfPrimitive":
if (!metadata.responseBodyFactory) {
throw new Error("couldn't find response body factory");
Expand Down Expand Up @@ -416,6 +434,7 @@ export interface RequestMetadata {
requestInformationContentSetMethod?: keyof RequestInformationSetContent;
queryParametersMapper?: Record<string, string>;
uriTemplate: string;
enumObject?: EnumObject;
}
export interface RequestsMetadata {
delete?: RequestMetadata;
Expand All @@ -435,6 +454,8 @@ export interface NavigationMetadata {
pathParametersMappings?: string[];
}

type EnumObject<T extends Record<string, unknown> = Record<string, unknown>> = T;

export type KeysToExcludeForNavigationMetadata =
| KeysOfRequestsMetadata
| "toDeleteRequestInformation"
Expand Down
28 changes: 28 additions & 0 deletions packages/abstractions/src/requestAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,34 @@ export interface RequestAdapter {
requestInfo: RequestInformation,
errorMappings: ErrorMappings | undefined,
): Promise<void>;
/**
* Executes the HTTP request specified by the given RequestInformation and returns the deserialized enum response model.
*
* @template EnumObject - The type of the enum object. Must extend Record<string, unknown>.
* @param {RequestInformation} requestInfo - The request info to execute.
* @param {EnumObject} enumObject - The Enum object expected in the response.
* @param {ErrorMappings | undefined} errorMappings - the error factories mapping to use in case of a failed request.
* @returns {Promise<EnumObject[keyof EnumObject] | undefined>} - A promise that resolves to the response of the request, or undefined if an error occurred.
*/
sendEnum<EnumObject extends Record<string, unknown>>(
requestInfo: RequestInformation,
enumObject: EnumObject,
errorMappings: ErrorMappings | undefined,
): Promise<EnumObject[keyof EnumObject] | undefined>;
/**
* Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection.
*
* @template EnumObject - The type of the enum objects. Must extend Record<string, unknown>.
* @param {RequestInformation} requestInfo - The request info to execute.
* @param {EnumObject} enumObject - The Enum object expected in the response.
* @param {ErrorMappings | undefined} errorMappings - the error factories mapping to use in case of a failed request.
* @returns {Promise<EnumObject[keyof EnumObject][] | undefined>} - with the deserialized response model collection.
*/
sendCollectionOfEnum<EnumObject extends Record<string, unknown>>(
requestInfo: RequestInformation,
enumObject: EnumObject,
errorMappings: ErrorMappings | undefined,
): Promise<EnumObject[keyof EnumObject][] | undefined>;
/**
* Enables the backing store proxies for the SerializationWriters and ParseNodes in use.
* @param backingStoreFactory the backing store factory to use.
Expand Down
68 changes: 68 additions & 0 deletions packages/http/fetch/src/fetchRequestAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,74 @@ export class FetchRequestAdapter implements RequestAdapter {
}
});
};
public sendEnum = <EnumObject extends Record<string, unknown>>(requestInfo: RequestInformation, enumObject: EnumObject, errorMappings: ErrorMappings | undefined): Promise<EnumObject[keyof EnumObject] | undefined> => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
return this.startTracingSpan(requestInfo, "sendEnum", async (span) => {
try {
const response = await this.getHttpResponseMessage(requestInfo, span);
const responseHandler = this.getResponseHandler(requestInfo);
if (responseHandler) {
span.addEvent(FetchRequestAdapter.eventResponseHandlerInvokedKey);
return await responseHandler.handleResponse(response, errorMappings);
} else {
try {
await this.throwIfFailedResponse(response, errorMappings, span);
if (this.shouldReturnUndefined(response)) return undefined;
const rootNode = await this.getRootParseNode(response);
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getEnumValue", (deserializeSpan) => {
try {
span.setAttribute(FetchRequestAdapter.responseTypeAttributeKey, "enum");
const result = rootNode.getEnumValue(enumObject);
return result as unknown as EnumObject[keyof EnumObject];
} finally {
deserializeSpan.end();
}
});
} finally {
await this.purgeResponseBody(response);
}
}
} finally {
span.end();
}
}) as Promise<EnumObject[keyof EnumObject]>;
}
public sendCollectionOfEnum = <EnumObject extends Record<string, unknown>>(requestInfo: RequestInformation, enumObject: EnumObject, errorMappings: ErrorMappings | undefined): Promise<EnumObject[keyof EnumObject][] | undefined> => {
if (!requestInfo) {
throw new Error("requestInfo cannot be null");
}
return this.startTracingSpan(requestInfo, "sendCollectionOfEnum", async (span) => {
try {
const response = await this.getHttpResponseMessage(requestInfo, span);
const responseHandler = this.getResponseHandler(requestInfo);
if (responseHandler) {
span.addEvent(FetchRequestAdapter.eventResponseHandlerInvokedKey);
return await responseHandler.handleResponse(response, errorMappings);
} else {
try {
await this.throwIfFailedResponse(response, errorMappings, span);
if (this.shouldReturnUndefined(response)) return undefined;
const rootNode = await this.getRootParseNode(response);
return trace.getTracer(this.observabilityOptions.getTracerInstrumentationName()).startActiveSpan("getCollectionOfEnumValues", (deserializeSpan) => {
try {
const result = rootNode.getCollectionOfEnumValues(enumObject);
span.setAttribute(FetchRequestAdapter.responseTypeAttributeKey, "enum[]");
return result as unknown as EnumObject[keyof EnumObject][];
} finally {
deserializeSpan.end();
}
});
} finally {
await this.purgeResponseBody(response);
}
}
} finally {
span.end();
}
});
}
public enableBackingStore = (backingStoreFactory?: BackingStoreFactory | undefined): void => {
this.parseNodeFactory = enableBackingStoreForParseNodeFactory(this.parseNodeFactory);
this.serializationWriterFactory = enableBackingStoreForSerializationWriterFactory(this.serializationWriterFactory);
Expand Down
60 changes: 60 additions & 0 deletions packages/http/fetch/test/common/fetchRequestAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,24 @@ import { HttpClient } from "../../src/httpClient";
import { getResponse } from "../testUtils";
import { createMockEntityFromDiscriminatorValue } from "./mockEntity";
import { MockParseNode, MockParseNodeFactory } from "./mockParseNodeFactory";
import { JsonParseNode, JsonParseNodeFactory } from "@microsoft/kiota-serialization-json";
import { TextParseNodeFactory } from "@microsoft/kiota-serialization-text";
import { FormParseNodeFactory } from "@microsoft/kiota-serialization-form";

// eslint-disable-next-line no-var
var Response = Response;
if (typeof Response !== "object") {
Response = getResponse();
}

const TestEnumObject = {
A: "a",
B: "b",
C: "c"
} as const;

type TestEnum = (typeof TestEnumObject)[keyof typeof TestEnumObject];

describe("FetchRequestAdapter.ts", () => {
describe("getClaimsFromResponse", () => {
it("should get claims from response header", async () => {
Expand Down Expand Up @@ -124,6 +135,55 @@ describe("FetchRequestAdapter.ts", () => {
});
}
});
describe("send enum", () => {
for (const statusCode of [200, 201, 202, 203]) {
const enumResponse = "a";
it(`should return object for status code ${statusCode}`, async () => {
const mockHttpClient = new HttpClient();
mockHttpClient.executeFetch = async (url: string, requestInit: RequestInit, requestOptions?: Record<string, RequestOption>) => {
const response = new Response(enumResponse, {
status: statusCode,
} as ResponseInit);
response.headers.set("Content-Type", "text/plain");
return Promise.resolve(response);
};
const mockFactory = new TextParseNodeFactory();//new JsonParseNodeFactory();
const requestAdapter = new FetchRequestAdapter(new AnonymousAuthenticationProvider(), mockFactory, undefined, mockHttpClient);
const requestInformation = new RequestInformation();
requestInformation.URL = "https://www.example.com";
requestInformation.httpMethod = HttpMethod.GET;
const result = await requestAdapter.sendEnum(requestInformation, TestEnumObject, undefined);
assert.isDefined(result);
assert.equal(result, TestEnumObject.A);
baywet marked this conversation as resolved.
Show resolved Hide resolved
});
}
});
describe("send and deserialize collection of enum", () => {
for (const statusCode of [200, 201, 202, 203]) {
const enumResponse = `["a","b","c","e","f"]`;
it(`should return object for status code ${statusCode}`, async () => {
const mockHttpClient = new HttpClient();
mockHttpClient.executeFetch = async (url: string, requestInit: RequestInit, requestOptions?: Record<string, RequestOption>) => {
const response = new Response(enumResponse, {
status: statusCode,
} as ResponseInit);
response.headers.set("Content-Type", "application/json");
return Promise.resolve(response);
};
const mockFactory = new JsonParseNodeFactory();
const requestAdapter = new FetchRequestAdapter(new AnonymousAuthenticationProvider(), mockFactory, undefined, mockHttpClient);
const requestInformation = new RequestInformation();
requestInformation.URL = "https://www.example.com";
requestInformation.httpMethod = HttpMethod.GET;
const result = await requestAdapter.sendCollectionOfEnum(requestInformation, TestEnumObject, undefined);
assert.isDefined(result);
assert.equal(result?.length, 3);
assert.equal(result![0], TestEnumObject.A);
assert.equal(result![1], TestEnumObject.B);
assert.equal(result![2], TestEnumObject.C);
});
}
});
describe("Throws API error", () => {
it("should throw API error", async () => {
const mockHttpClient = new HttpClient();
Expand Down