Skip to content

Commit 4636946

Browse files
committed
Add tests for strongly typed function wrappers
1 parent 2501176 commit 4636946

File tree

7 files changed

+384
-47
lines changed

7 files changed

+384
-47
lines changed

src/function_registry.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
HandlerFunction,
1919
TypedHandlerFunction,
2020
InvocationFormat,
21-
JsonInvocationSerializer,
21+
JsonInvocationFormat,
2222
} from './functions';
2323
import {SignatureType} from './types';
2424

@@ -75,7 +75,7 @@ export const isValidFunctionName = (functionName: string): boolean => {
7575
export const getRegisteredFunction = (
7676
functionName: string
7777
// eslint-disable-next-line @typescript-eslint/no-explicit-any
78-
): RegisteredFunction<any> | undefined => {
78+
): RegisteredFunction<any, any> | undefined => {
7979
return registrationContainer.get(functionName);
8080
};
8181

@@ -103,15 +103,19 @@ export const cloudEvent = <T = unknown>(
103103
};
104104

105105

106-
106+
/**
107+
* Register a function that handles strongly typed invocations.
108+
* @param functionName - the name of the function
109+
* @param handler - the function to trigger.
110+
*/
107111
export const typed = <T, U>(
108112
functionName: string,
109113
handler: TypedHandlerFunction<T, U>['handler'] | TypedHandlerFunction<T, U>
110114
): void => {
111115
if (handler instanceof Function) {
112116
handler = {
113117
handler,
114-
format: new JsonInvocationSerializer<T, U>(),
118+
format: new JsonInvocationFormat<T, U>(),
115119
}
116120
}
117121
register(functionName, 'typed', handler);

src/function_wrappers.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {
2727
CloudEventFunctionWithCallback,
2828
HandlerFunction,
2929
TypedHandlerFunction,
30-
InvocationFormat,
3130
InvocationRequest,
3231
InvocationResponse
3332
} from './functions';
@@ -106,18 +105,25 @@ const parseBackgroundEvent = (req: Request): {data: {}; context: Context} => {
106105
const wrapHttpFunction = (execute: HttpFunction): RequestHandler => {
107106
return (req: Request, res: Response) => {
108107
const d = domain.create();
109-
// Catch unhandled errors originating from this request.
110-
d.on('error', err => {
108+
const errorHandler = (err: Error) => {
111109
if (res.locals.functionExecutionFinished) {
112110
console.error(`Exception from a finished function: ${err}`);
113111
} else {
114112
res.locals.functionExecutionFinished = true;
115-
sendCrashResponse({err, res});
113+
sendCrashResponse({ err, res });
116114
}
117-
});
115+
};
116+
117+
// Catch unhandled errors originating from this request.
118+
d.on('error', errorHandler);
119+
118120
d.run(() => {
119121
process.nextTick(() => {
120-
execute(req, res);
122+
const ret = execute(req, res);
123+
// Catch rejected promises if the function is async.
124+
if (ret instanceof Promise) {
125+
ret.catch(errorHandler);
126+
}
121127
});
122128
});
123129
};
@@ -216,45 +222,41 @@ const wrapTypedFunction = (
216222
}
217223

218224
class InvocationResponseImpl implements InvocationResponse {
219-
constructor(private req: Response) { }
225+
constructor(private res: Response) { }
220226

221227
setHeader(key: string, value: string): void {
222-
throw new Error('Method not implemented.');
228+
this.res.set(key, value);
223229
}
224230
write(data: string | Buffer): void {
225-
this.req.write(data);
231+
this.res.write(data);
226232
}
227233
end(data: string | Buffer): void {
228-
this.req.end(data);
234+
this.res.end(data);
229235
}
230236
}
231237

232-
const handler: HttpFunction = async (req: Request, res: Response) => {
238+
const typedHandlerWrapper: HttpFunction = async (req: Request, res: Response) => {
233239
let reqTyped: any;
234240
try {
235241
reqTyped = typedFunction.format.deserializeRequest(new InvocationRequestImpl(req));
236-
if (reqTyped instanceof Promise) {
237-
reqTyped = await reqTyped;
238-
}
239242
} catch (err) {
243+
console.log(err);
240244
sendCrashResponse({
241245
err, res,
242246
statusOverride: 400 // 400 Bad Request
243247
});
248+
return;
244249
}
245250

246251
let resTyped: any = typedFunction.handler(reqTyped);
247252
if (resTyped instanceof Promise) {
248253
resTyped = await resTyped;
249254
}
250255

251-
const maybePromise = typedFunction.format.serializeResponse(new InvocationResponseImpl(res), resTyped);
252-
if (maybePromise instanceof Promise) {
253-
await maybePromise;
254-
}
256+
typedFunction.format.serializeResponse(new InvocationResponseImpl(res), resTyped);
255257
}
256258

257-
return wrapHttpFunction(handler);
259+
return wrapHttpFunction(typedHandlerWrapper);
258260
}
259261

260262
/**
@@ -272,19 +274,21 @@ export const wrapUserFunction = <T = unknown>(
272274
return wrapHttpFunction(userFunction as HttpFunction);
273275
case 'event':
274276
// Callback style if user function has more than 2 arguments.
275-
if (userFunction!.length > 2) {
277+
if (userFunction instanceof Function && userFunction!.length > 2) {
276278
return wrapEventFunctionWithCallback(
277279
userFunction as EventFunctionWithCallback
278280
);
279281
}
280282
return wrapEventFunction(userFunction as EventFunction);
281283
case 'cloudevent':
282-
if (userFunction!.length > 1) {
284+
if (userFunction instanceof Function && userFunction!.length > 1) {
283285
// Callback style if user function has more than 1 argument.
284286
return wrapCloudEventFunctionWithCallback(
285287
userFunction as CloudEventFunctionWithCallback
286288
);
287289
}
288290
return wrapCloudEventFunction(userFunction as CloudEventFunction);
291+
case 'typed':
292+
return wrapTypedFunction(userFunction as TypedHandlerFunction);
289293
}
290294
};

src/functions.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ export type Context = CloudFunctionsContext | CloudEvent<unknown>;
152152
* Invocation request interface describes the properties of an invocation.
153153
*/
154154
export interface InvocationRequest {
155+
/** Returns the request body as either a string or a Buffer if the body is binary. */
155156
body(): string | Buffer;
157+
/** Header returns the value of the specified header */
156158
header(header: string): string | undefined;
157159
}
158160

@@ -161,14 +163,20 @@ export interface InvocationRequest {
161163
* an invocation response.
162164
*/
163165
export interface InvocationResponse {
166+
/** Sets a header on the response. */
164167
setHeader(key: string, value: string): void;
168+
/** Writes a chunk of data to the response. */
165169
write(data: string | Buffer): void;
170+
/** Ends the response, must be called once at the end of writing. */
166171
end(data: string | Buffer): void;
167172
}
168173

174+
/**
175+
* The contract for a request deserializer and response serializer.
176+
*/
169177
export interface InvocationFormat<T, U> {
170178
/**
171-
*
179+
*
172180
* @param request the request body as raw bytes
173181
* @param headers the headers received on the HTTP request as a map
174182
*/
@@ -178,20 +186,30 @@ export interface InvocationFormat<T, U> {
178186
* @param response
179187
* @param responseHeaders mutable object providing headers that will be set on the response
180188
*/
181-
serializeResponse(responseWriter: InvocationResponse, response: U): void | Promise<void>;
189+
serializeResponse(
190+
responseWriter: InvocationResponse,
191+
response: U
192+
): void | Promise<void>;
182193
}
183194

184-
export class JsonInvocationSerializer<T, U> implements InvocationFormat<T, U> {
195+
/**
196+
* Default invocation format for JSON requests.
197+
*/
198+
export class JsonInvocationFormat<T, U> implements InvocationFormat<T, U> {
185199
deserializeRequest(request: InvocationRequest): T {
186-
if (!(typeof request.body === 'string')) {
187-
throw new Error('Request Content-Type or encoding unsupported');
200+
let body = request.body();
201+
if (typeof body !== 'string') {
202+
throw new Error("Unsupported Content-Type, expected application/json");
203+
}
204+
try {
205+
return JSON.parse(body);
206+
} catch (e) {
207+
throw new Error("Failed to parse malformatted JSON in request: " + (e as SyntaxError).message)
188208
}
189-
if
190-
return JSON.parse(request.body);
191209
}
192210

193211
serializeResponse(responseWriter: InvocationResponse, response: U): void {
194212
responseWriter.setHeader('content-type', 'application/json');
195213
responseWriter.end(JSON.stringify(response));
196214
}
197-
}
215+
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ export * from './functions';
2020
/**
2121
* @public
2222
*/
23-
export {http, cloudEvent} from './function_registry';
23+
export {http, cloudEvent, typed} from './function_registry';

src/server.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,20 @@ export function getServer(
8585
};
8686

8787
// Apply middleware
88-
app.use(bodyParser.json(cloudEventsBodySavingOptions));
89-
app.use(bodyParser.json(defaultBodySavingOptions));
90-
app.use(bodyParser.text(defaultBodySavingOptions));
88+
if (functionSignatureType === 'typed') {
89+
app.use(bodyParser.text({
90+
limit: requestLimit,
91+
type: '*/json',
92+
}))
93+
app.use(bodyParser.text({
94+
limit: requestLimit,
95+
type: 'text/*',
96+
}))
97+
} else {
98+
app.use(bodyParser.json(cloudEventsBodySavingOptions));
99+
app.use(bodyParser.json(defaultBodySavingOptions));
100+
app.use(bodyParser.text(defaultBodySavingOptions));
101+
}
91102
app.use(bodyParser.urlencoded(urlEncodedOptions));
92103
// The parser will process ALL content types so MUST come last.
93104
// Subsequent parsers will be skipped when one is matched.

0 commit comments

Comments
 (0)