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/multipart #771

Merged
merged 12 commits into from
Aug 2, 2023
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ updates:
# Prefix all commit messages with "npm"
prefix: "auto dependabot"

- package-ecosystem: npm
directory: "/packages/serialization/multipart"
schedule:
interval: daily
open-pull-requests-limit: 10
commit-message:
# Prefix all commit messages with "npm"
prefix: "auto dependabot"

- package-ecosystem: npm
directory: "/packages/serialization/json"
schedule:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/build_test_validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
packages/abstractions/dist
packages/serialization/form/dist
packages/serialization/json/dist
packages/serialization/multipart/dist
packages/serialization/text/dist
packages/http/fetch/dist
packages/authentication/azure/dist
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@
"packages/sample-app/",
"packages/abstractions",
"packages/http/*",
"packages/serialization/form/",
"packages/serialization/json/",
"packages/serialization/text/",
"packages/serialization/*/",
"packages/authentication/*"
],
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/abstractions/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@microsoft/kiota-abstractions",
"version": "1.0.0-preview.24",
"version": "1.0.0-preview.25",
"description": "Core abstractions for kiota generated libraries in TypeScript and JavaScript",
"main": "dist/cjs/src/index.js",
"module": "dist/es/src/index.js",
Expand Down
1 change: 1 addition & 0 deletions packages/abstractions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./dateOnly";
export * from "./duration";
export * from "./getPathParameters";
export * from "./httpMethod";
export * from "./multipartBody";
export * from "./nativeResponseHandler";
export * from "./nativeResponseWrapper";
export * from "./requestAdapter";
Expand Down
181 changes: 181 additions & 0 deletions packages/abstractions/src/multipartBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { Guid } from "guid-typescript";

import type { RequestAdapter } from "./requestAdapter";
import type {
ModelSerializerFunction,
ParseNode,
SerializationWriter,
} from "./serialization";
import type { Parsable } from "./serialization/parsable";
/**
* Defines an interface for a multipart body for request or response.
*/
export class MultipartBody implements Parsable {
private readonly _boundary: string;
private readonly _parts: Record<string, MultipartEntry> = {};
public requestAdapter?: RequestAdapter;
/**
* Instantiates a new MultipartBody.
*/
public constructor() {
this._boundary = Guid.create().toString().replace(/-/g, "");
}
/**
* Adds or replaces a part with the given name, content type and content.
* @param partName the name of the part to add or replace.
* @param partContentType the content type of the part to add or replace.
* @param content the content of the part to add or replace.
* @param serializationCallback the serialization callback to use when serializing the part.
*/
public addOrReplacePart<T>(
partName: string,
partContentType: string,
content: T,
serializationCallback?: ModelSerializerFunction<Parsable>,
): void {
if (!partName) throw new Error("partName cannot be undefined");
if (!partContentType) {
throw new Error("partContentType cannot be undefined");
}
if (!content) throw new Error("content cannot be undefined");
const normalizePartName = this.normalizePartName(partName);
this._parts[normalizePartName] = {
contentType: partContentType,
content,
originalName: partName,
serializationCallback,
};
}
/**
* Gets the content of the part with the given name.
* @param partName the name of the part to get the content for.
* @returns the content of the part with the given name.
*/
public getPartValue<T>(partName: string): T | undefined {
if (!partName) throw new Error("partName cannot be undefined");
const normalizePartName = this.normalizePartName(partName);
const candidate = this._parts[normalizePartName];
if (!candidate) return undefined;
return candidate.content as T;
}
/**
* Removes the part with the given name.
* @param partName the name of the part to remove.
* @returns true if the part was removed, false if it did not exist.
*/
public removePart(partName: string): boolean {
if (!partName) throw new Error("partName cannot be undefined");
const normalizePartName = this.normalizePartName(partName);
if (!this._parts[normalizePartName]) return false;
delete this._parts[normalizePartName];
return true;
}
/**
* Gets the boundary used to separate each part.
* @returns the boundary value.
*/
public getBoundary(): string {
return this._boundary;
}

private normalizePartName(original: string): string {
return original.toLocaleLowerCase();
}
/**
* Lists all the parts in the multipart body.
* WARNING: meant for internal use only
* @returns the list of parts in the multipart body.
*/
public listParts(): Record<string, MultipartEntry> {
return this._parts;
}
}
interface MultipartEntry {
contentType: string;
content: any;
originalName: string;
serializationCallback?: ModelSerializerFunction<Parsable>;
}

export function serializeMultipartBody(
writer: SerializationWriter,
multipartBody: MultipartBody | undefined,
): void {
if (!writer) {
throw new Error("writer cannot be undefined");
}
if (!multipartBody) {
throw new Error("multipartBody cannot be undefined");
}
const parts = multipartBody.listParts();
if (Object.keys(parts).length === 0) {
throw new Error("multipartBody cannot be empty");
}
const boundary = multipartBody.getBoundary();
let first = true;
for (const partName in parts) {
if (first) {
first = false;
} else {
writer.writeStringValue(undefined, "");
}
writer.writeStringValue(undefined, "--" + boundary);
const part = parts[partName];
writer.writeStringValue("Content-Type", part.contentType);
writer.writeStringValue(
"Content-Disposition",
'form-data; name="' + part.originalName + '"',
);
writer.writeStringValue(undefined, "");
if (typeof part.content === "string") {
writer.writeStringValue(undefined, part.content);
} else if (part.content instanceof ArrayBuffer) {
writer.writeByteArrayValue(undefined, new Uint8Array(part.content));
} else if (part.content instanceof Uint8Array) {
writer.writeByteArrayValue(undefined, part.content);
} else if (part.serializationCallback) {
if (!multipartBody.requestAdapter) {
throw new Error("requestAdapter cannot be undefined");
}
const serializationWriterFactory =
multipartBody.requestAdapter.getSerializationWriterFactory();
if (!serializationWriterFactory) {
throw new Error("serializationWriterFactory cannot be undefined");
}
const partSerializationWriter =
serializationWriterFactory.getSerializationWriter(part.contentType);
if (!partSerializationWriter) {
throw new Error(
"no serialization writer factory for content type: " +
part.contentType,
);
}
partSerializationWriter.writeObjectValue(
undefined,
part.content as Parsable,
part.serializationCallback,
);
const partContent = partSerializationWriter.getSerializedContent();
writer.writeByteArrayValue(undefined, new Uint8Array(partContent));
} else {
throw new Error(
"unsupported content type for multipart body: " + typeof part.content,
);
}
}
writer.writeStringValue(undefined, "");
writer.writeStringValue(undefined, "--" + boundary + "--");
}

export function deserializeIntoMultipartBody(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_: MultipartBody | undefined = new MultipartBody(),
): Record<string, (node: ParseNode) => void> {
throw new Error("Not implemented");
}
export function createMessageFromDiscriminatorValue(
parseNode: ParseNode | undefined,
) {
if (!parseNode) throw new Error("parseNode cannot be undefined");
return deserializeIntoMultipartBody;
}
32 changes: 20 additions & 12 deletions packages/abstractions/src/requestInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as Template from "uri-template-lite";
import { DateOnly } from "./dateOnly";
import { Duration } from "./duration";
import { HttpMethod } from "./httpMethod";
import { MultipartBody } from "./multipartBody";
import { createRecordWithCaseInsensitiveKeys } from "./recordWithCaseInsensitiveKeys";
import { RequestAdapter } from "./requestAdapter";
import { RequestOption } from "./requestOption";
import {
Expand All @@ -12,14 +14,14 @@ import {
SerializationWriter,
} from "./serialization";
import { TimeOnly } from "./timeOnly";
import { createRecordWithCaseInsensitiveKeys } from "./recordWithCaseInsensitiveKeys";

/** This class represents an abstract HTTP request. */
export class RequestInformation {
/** The URI of the request. */
private uri?: string;
/** The path parameters for the request. */
public pathParameters: Record<string, unknown> = createRecordWithCaseInsensitiveKeys<unknown>();
public pathParameters: Record<string, unknown> =
createRecordWithCaseInsensitiveKeys<unknown>();
/** The URL template for the request */
public urlTemplate?: string;
/** Gets the URL of the request */
Expand Down Expand Up @@ -70,10 +72,13 @@ export class RequestInformation {
public queryParameters: Record<
string,
string | number | boolean | undefined
> = createRecordWithCaseInsensitiveKeys<string | number | boolean | undefined>();
> = createRecordWithCaseInsensitiveKeys<
string | number | boolean | undefined
>();
/** The Request Headers. */
public headers: Record<string, string[]> = {};
private _requestOptions: Record<string, RequestOption> = createRecordWithCaseInsensitiveKeys<RequestOption>();
private _requestOptions: Record<string, RequestOption> =
createRecordWithCaseInsensitiveKeys<RequestOption>();
/** Gets the request options for the request. */
public getRequestOptions() {
return this._requestOptions;
Expand Down Expand Up @@ -114,7 +119,7 @@ export class RequestInformation {
requestAdapter?: RequestAdapter | undefined,
contentType?: string | undefined,
value?: T[] | T,
modelSerializerFunction?: ModelSerializerFunction<T>
modelSerializerFunction?: ModelSerializerFunction<T>,
): void => {
trace
.getTracer(RequestInformation.tracerKey)
Expand All @@ -123,8 +128,11 @@ export class RequestInformation {
const writer = this.getSerializationWriter(
requestAdapter,
contentType,
value
value,
);
if (value instanceof MultipartBody) {
contentType += "; boundary=" + value.getBoundary();
}
if (!this.headers) {
this.headers = {};
}
Expand All @@ -135,7 +143,7 @@ export class RequestInformation {
undefined,
value,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
modelSerializerFunction!
modelSerializerFunction!,
);
} else {
span.setAttribute(RequestInformation.requestTypeKey, "object");
Expand All @@ -149,7 +157,7 @@ export class RequestInformation {
};
private setContentAndContentType = (
writer: SerializationWriter,
contentType?: string | undefined
contentType?: string | undefined,
) => {
if (contentType) {
this.headers[RequestInformation.contentTypeHeader] = [contentType];
Expand Down Expand Up @@ -180,7 +188,7 @@ export class RequestInformation {
public setContentFromScalar = <T>(
requestAdapter?: RequestAdapter | undefined,
contentType?: string | undefined,
value?: T[] | T
value?: T[] | T,
): void => {
trace
.getTracer(RequestInformation.tracerKey)
Expand All @@ -189,7 +197,7 @@ export class RequestInformation {
const writer = this.getSerializationWriter(
requestAdapter,
contentType,
value
value,
);
if (!this.headers) {
this.headers = {};
Expand Down Expand Up @@ -221,7 +229,7 @@ export class RequestInformation {
writer.writeCollectionOfPrimitiveValues(undefined, value);
} else {
throw new Error(
`encountered unknown value type during serialization ${valueType}`
`encountered unknown value type during serialization ${valueType}`,
);
}
}
Expand All @@ -246,7 +254,7 @@ export class RequestInformation {
* @param parameters the parameters.
*/
public setQueryStringParametersFromRawObject = (
q: object | undefined
q: object | undefined,
): void => {
if (!q) return;
Object.entries(q).forEach(([k, v]) => {
Expand Down
14 changes: 9 additions & 5 deletions packages/abstractions/src/serialization/parseNode.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Guid } from "guid-typescript";

import { DateOnly } from "../dateOnly";
import { Duration } from "../duration";
import { TimeOnly } from "../timeOnly";
import { Parsable } from "./parsable";
import { ParsableFactory } from "./parsableFactory";
import { Guid } from "guid-typescript";

/**
* Interface for a deserialization node in a parse tree. This interface provides an abstraction layer over serialization formats, libraries and implementations.
Expand Down Expand Up @@ -65,16 +66,14 @@ export interface ParseNode {
* @return the collection of object values of the node.
*/
getCollectionOfObjectValues<T extends Parsable>(
parsableFactory: ParsableFactory<T>
parsableFactory: ParsableFactory<T>,
): T[] | undefined;

/**
* Gets the model object value of the node.
* @return the model object value of the node.
*/
getObjectValue<T extends Parsable>(
parsableFactory: ParsableFactory<T>
): T;
getObjectValue<T extends Parsable>(parsableFactory: ParsableFactory<T>): T;

/**
* Gets the Enum values of the node.
Expand All @@ -96,4 +95,9 @@ export interface ParseNode {
* @return the callback called after the node is deserialized.
*/
onAfterAssignFieldValues: ((value: Parsable) => void) | undefined;
/**
* Gets the byte array value of the node.
* @return the byte array value of the node.
*/
getByteArrayValue(): ArrayBuffer | undefined;
}
Loading