Skip to content

Commit 2501176

Browse files
committed
feat: implement typed function signature
1 parent 5f1f7a9 commit 2501176

File tree

6 files changed

+161
-13
lines changed

6 files changed

+161
-13
lines changed

src/function_registry.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,34 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import {HttpFunction, CloudEventFunction, HandlerFunction} from './functions';
15+
import {
16+
HttpFunction,
17+
CloudEventFunction,
18+
HandlerFunction,
19+
TypedHandlerFunction,
20+
InvocationFormat,
21+
JsonInvocationSerializer,
22+
} from './functions';
1623
import {SignatureType} from './types';
1724

18-
interface RegisteredFunction<T> {
25+
interface RegisteredFunction<T, U> {
1926
signatureType: SignatureType;
20-
userFunction: HandlerFunction<T>;
27+
userFunction: HandlerFunction<T, U>;
2128
}
2229

2330
/**
2431
* Singleton map to hold the registered functions
2532
*/
2633
// eslint-disable-next-line @typescript-eslint/no-explicit-any
27-
const registrationContainer = new Map<string, RegisteredFunction<any>>();
34+
const registrationContainer = new Map<string, RegisteredFunction<any, any>>();
2835

2936
/**
3037
* Helper method to store a registered function in the registration container
3138
*/
32-
const register = <T = unknown>(
39+
const register = <T = unknown, U = unknown>(
3340
functionName: string,
3441
signatureType: SignatureType,
35-
userFunction: HandlerFunction<T>
42+
userFunction: HandlerFunction<T, U>
3643
): void => {
3744
if (!isValidFunctionName(functionName)) {
3845
throw new Error(`Invalid function name: ${functionName}`);
@@ -94,3 +101,18 @@ export const cloudEvent = <T = unknown>(
94101
): void => {
95102
register(functionName, 'cloudevent', handler);
96103
};
104+
105+
106+
107+
export const typed = <T, U>(
108+
functionName: string,
109+
handler: TypedHandlerFunction<T, U>['handler'] | TypedHandlerFunction<T, U>
110+
): void => {
111+
if (handler instanceof Function) {
112+
handler = {
113+
handler,
114+
format: new JsonInvocationSerializer<T, U>(),
115+
}
116+
}
117+
register(functionName, 'typed', handler);
118+
};

src/function_wrappers.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import {
2626
CloudEventFunction,
2727
CloudEventFunctionWithCallback,
2828
HandlerFunction,
29+
TypedHandlerFunction,
30+
InvocationFormat,
31+
InvocationRequest,
32+
InvocationResponse
2933
} from './functions';
3034
import {CloudEvent} from './functions';
3135
import {SignatureType} from './types';
@@ -176,7 +180,7 @@ const wrapEventFunction = (userFunction: EventFunction): RequestHandler => {
176180
};
177181

178182
/**
179-
* Wraps an callback style event function in an express RequestHandler.
183+
* Wraps a callback style event function in an express RequestHandler.
180184
* @param userFunction User's function.
181185
* @return An Express hander function that invokes the user function.
182186
*/
@@ -191,6 +195,68 @@ const wrapEventFunctionWithCallback = (
191195
return wrapHttpFunction(httpHandler);
192196
};
193197

198+
/**
199+
* Wraps a typed function in an express style RequestHandler.
200+
* @param userFunction User's function
201+
* @return An Express handler function that invokes the user function
202+
*/
203+
const wrapTypedFunction = (
204+
typedFunction: TypedHandlerFunction
205+
): RequestHandler => {
206+
class InvocationRequestImpl implements InvocationRequest {
207+
constructor(private req: Request) {}
208+
209+
body(): string | Buffer {
210+
return this.req.body;
211+
}
212+
213+
header(header: string): string | undefined {
214+
return this.req.header(header);
215+
}
216+
}
217+
218+
class InvocationResponseImpl implements InvocationResponse {
219+
constructor(private req: Response) { }
220+
221+
setHeader(key: string, value: string): void {
222+
throw new Error('Method not implemented.');
223+
}
224+
write(data: string | Buffer): void {
225+
this.req.write(data);
226+
}
227+
end(data: string | Buffer): void {
228+
this.req.end(data);
229+
}
230+
}
231+
232+
const handler: HttpFunction = async (req: Request, res: Response) => {
233+
let reqTyped: any;
234+
try {
235+
reqTyped = typedFunction.format.deserializeRequest(new InvocationRequestImpl(req));
236+
if (reqTyped instanceof Promise) {
237+
reqTyped = await reqTyped;
238+
}
239+
} catch (err) {
240+
sendCrashResponse({
241+
err, res,
242+
statusOverride: 400 // 400 Bad Request
243+
});
244+
}
245+
246+
let resTyped: any = typedFunction.handler(reqTyped);
247+
if (resTyped instanceof Promise) {
248+
resTyped = await resTyped;
249+
}
250+
251+
const maybePromise = typedFunction.format.serializeResponse(new InvocationResponseImpl(res), resTyped);
252+
if (maybePromise instanceof Promise) {
253+
await maybePromise;
254+
}
255+
}
256+
257+
return wrapHttpFunction(handler);
258+
}
259+
194260
/**
195261
* Wraps a user function with the provided signature type in an express
196262
* RequestHandler.

src/functions.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,26 @@ export interface CloudEventFunction<T = unknown> {
7474
export interface CloudEventFunctionWithCallback<T = unknown> {
7575
(cloudEvent: CloudEvent<T>, callback: Function): any;
7676
}
77+
78+
/**
79+
* A Typed function handler that may return a value or a promise.
80+
*/
81+
export interface TypedHandlerFunction<T = unknown, U = unknown> {
82+
handler: (req: T) => U | Promise<U>;
83+
format: InvocationFormat<T, U>;
84+
}
85+
7786
/**
7887
* A function handler.
7988
* @public
8089
*/
81-
export type HandlerFunction<T = unknown> =
90+
export type HandlerFunction<T = unknown, U = unknown> =
8291
| HttpFunction
8392
| EventFunction
8493
| EventFunctionWithCallback
8594
| CloudEventFunction<T>
86-
| CloudEventFunctionWithCallback<T>;
95+
| CloudEventFunctionWithCallback<T>
96+
| TypedHandlerFunction<T, U>;
8797

8898
/**
8999
* A legacy event.
@@ -137,3 +147,51 @@ export interface CloudFunctionsContext {
137147
* @public
138148
*/
139149
export type Context = CloudFunctionsContext | CloudEvent<unknown>;
150+
151+
/**
152+
* Invocation request interface describes the properties of an invocation.
153+
*/
154+
export interface InvocationRequest {
155+
body(): string | Buffer;
156+
header(header: string): string | undefined;
157+
}
158+
159+
/**
160+
* InvocationResponse interface describes the properties that can be set on
161+
* an invocation response.
162+
*/
163+
export interface InvocationResponse {
164+
setHeader(key: string, value: string): void;
165+
write(data: string | Buffer): void;
166+
end(data: string | Buffer): void;
167+
}
168+
169+
export interface InvocationFormat<T, U> {
170+
/**
171+
*
172+
* @param request the request body as raw bytes
173+
* @param headers the headers received on the HTTP request as a map
174+
*/
175+
deserializeRequest(request: InvocationRequest): T | Promise<T>;
176+
/**
177+
*
178+
* @param response
179+
* @param responseHeaders mutable object providing headers that will be set on the response
180+
*/
181+
serializeResponse(responseWriter: InvocationResponse, response: U): void | Promise<void>;
182+
}
183+
184+
export class JsonInvocationSerializer<T, U> implements InvocationFormat<T, U> {
185+
deserializeRequest(request: InvocationRequest): T {
186+
if (!(typeof request.body === 'string')) {
187+
throw new Error('Request Content-Type or encoding unsupported');
188+
}
189+
if
190+
return JSON.parse(request.body);
191+
}
192+
193+
serializeResponse(responseWriter: InvocationResponse, response: U): void {
194+
responseWriter.setHeader('content-type', 'application/json');
195+
responseWriter.end(JSON.stringify(response));
196+
}
197+
}

src/logger.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ export function sendCrashResponse({
2828
callback,
2929
silent = false,
3030
statusHeader = 'crash',
31+
statusOverride = 500
3132
}: {
3233
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3334
err: Error | any;
3435
res: express.Response | null;
3536
callback?: Function;
3637
silent?: boolean;
3738
statusHeader?: string;
39+
statusOverride?: number;
3840
}) {
3941
if (!silent) {
4042
console.error(err.stack || err);
@@ -48,10 +50,10 @@ export function sendCrashResponse({
4850
res.set(FUNCTION_STATUS_HEADER_FIELD, statusHeader);
4951

5052
if (process.env.NODE_ENV !== 'production') {
51-
res.status(500);
53+
res.status(statusOverride);
5254
res.send((err.message || err) + '');
5355
} else {
54-
res.sendStatus(500);
56+
res.sendStatus(statusOverride);
5557
}
5658
}
5759
if (callback) {

src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export function getServer(
119119
}
120120

121121
if (functionSignatureType === 'http') {
122-
app.use('/favicon.ico|/robots.txt', (req, res) => {
122+
app.use('/favicon.ico|/robots.txt', (req: express.Request, res) => {
123123
// Neither crawlers nor browsers attempting to pull the icon find the body
124124
// contents particularly useful, so we send nothing in the response body.
125125
res.status(404).send(null);

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const FUNCTION_STATUS_HEADER_FIELD = 'X-Google-Status';
1919
/**
2020
* List of function signature types that are supported by the framework.
2121
*/
22-
export const SignatureType = ['http', 'event', 'cloudevent'] as const;
22+
export const SignatureType = ['http', 'event', 'cloudevent', 'typed'] as const;
2323

2424
/**
2525
* Union type of all valid function SignatureType values.

0 commit comments

Comments
 (0)