Skip to content

Add options to invocation context #54

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

Merged
merged 3 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
52 changes: 47 additions & 5 deletions src/InvocationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
// Licensed under the MIT License.

import * as types from '@azure/functions';
import { InvocationContextInit, LogHandler, RetryContext, TraceContext, TriggerMetadata } from '@azure/functions';
import {
EffectiveFunctionOptions,
InvocationContextInit,
LogHandler,
RetryContext,
TraceContext,
TriggerMetadata,
} from '@azure/functions';

export class InvocationContext implements types.InvocationContext {
invocationId: string;
Expand All @@ -12,17 +19,29 @@ export class InvocationContext implements types.InvocationContext {
retryContext?: RetryContext;
traceContext?: TraceContext;
triggerMetadata?: TriggerMetadata;
options: EffectiveFunctionOptions;
#userLogHandler: LogHandler;

constructor(init: InvocationContextInit) {
this.invocationId = init.invocationId;
this.functionName = init.functionName;
constructor(init?: InvocationContextInit) {
init = init || {};
const fallbackString = 'unknown';
this.invocationId = init.invocationId || fallbackString;
this.functionName = init.functionName || fallbackString;
this.extraInputs = new InvocationContextExtraInputs();
this.extraOutputs = new InvocationContextExtraOutputs();
this.retryContext = init.retryContext;
this.traceContext = init.traceContext;
this.triggerMetadata = init.triggerMetadata;
this.#userLogHandler = init.logHandler;
this.options = {
trigger: init.options?.trigger || {
name: fallbackString,
type: fallbackString,
},
return: init.options?.return,
extraInputs: init.options?.extraInputs || [],
extraOutputs: init.options?.extraOutputs || [],
};
this.#userLogHandler = init.logHandler || fallbackLogHandler;
}

log(...args: unknown[]): void {
Expand Down Expand Up @@ -73,3 +92,26 @@ class InvocationContextExtraOutputs implements types.InvocationContextExtraOutpu
this.#outputs[name] = value;
}
}

function fallbackLogHandler(level: types.LogLevel, ...args: unknown[]): void {
switch (level) {
case 'trace':
console.trace(...args);
break;
case 'debug':
console.debug(...args);
break;
case 'information':
console.info(...args);
break;
case 'warning':
console.warn(...args);
break;
case 'critical':
case 'error':
console.error(...args);
break;
default:
console.log(...args);
}
}
2 changes: 2 additions & 0 deletions src/InvocationModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@azure/functions-core';
import { format } from 'util';
import { returnBindingKey } from './constants';
import { fromRpcBindings } from './converters/fromRpcBindings';
import { fromRpcRetryContext, fromRpcTraceContext } from './converters/fromRpcContext';
import { fromRpcTriggerMetadata } from './converters/fromRpcTriggerMetadata';
import { fromRpcTypedData } from './converters/fromRpcTypedData';
Expand Down Expand Up @@ -53,6 +54,7 @@ export class InvocationModel implements coreTypes.InvocationModel {
retryContext: fromRpcRetryContext(req.retryContext),
traceContext: fromRpcTraceContext(req.traceContext),
triggerMetadata: fromRpcTriggerMetadata(req.triggerMetadata, this.#triggerType),
options: fromRpcBindings(this.#bindings),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My biggest issue with this implementation is that this now requires the options as input to the constuctor of InvocationContext, making the unit testing scenario harder. This code you have in the Wiki here would no longer work:

const testInvocationContext = new InvocationContext({
  functionName: 'testFunctionName',
  invocationId: 'testInvocationId',
  logHandler: (_level, ...args) => console.log(...args)
});

Is there a way around this? Could we have the options object initialized inside the constructor, or set here after the context = new InvocationContext({}) line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this feedback applies to all properties on InvocationContextInit? I've updated so in testing scenarios you can just do new InvocationContext()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's great! Yeah, I was gonna suggest that too but in a separate PR 😛

});

const inputs: unknown[] = [];
Expand Down
40 changes: 40 additions & 0 deletions src/converters/fromRpcBindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { EffectiveFunctionOptions, FunctionInput, FunctionOutput, FunctionTrigger } from '@azure/functions';
import { RpcBindingInfo } from '@azure/functions-core';
import { returnBindingKey } from '../constants';
import { isTrigger } from '../utils/isTrigger';
import { nonNullProp, nonNullValue } from '../utils/nonNull';

export function fromRpcBindings(bindings: Record<string, RpcBindingInfo> | null | undefined): EffectiveFunctionOptions {
let trigger: FunctionTrigger | undefined;
let returnBinding: FunctionOutput | undefined;
const extraInputs: FunctionInput[] = [];
const extraOutputs: FunctionOutput[] = [];
for (const [name, binding] of Object.entries(nonNullValue(bindings, 'bindings'))) {
if (isTrigger(binding.type)) {
trigger = fromRpcBinding(name, binding);
} else if (name === returnBindingKey) {
returnBinding = fromRpcBinding(name, binding);
} else if (binding.direction === 'in') {
extraInputs.push(fromRpcBinding(name, binding));
} else if (binding.direction === 'out') {
extraOutputs.push(fromRpcBinding(name, binding));
}
}
return {
trigger: nonNullValue(trigger, 'trigger'),
return: returnBinding,
extraInputs,
extraOutputs,
};
}

function fromRpcBinding(name: string, binding: RpcBindingInfo): FunctionTrigger | FunctionInput | FunctionOutput {
return {
...binding,
type: nonNullProp(binding, 'type'),
name,
};
}
59 changes: 53 additions & 6 deletions types/InvocationContext.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CosmosDBInput, CosmosDBOutput } from './cosmosDB';
import { EventGridOutput, EventGridPartialEvent } from './eventGrid';
import { EventHubOutput } from './eventHub';
import { HttpOutput, HttpResponse } from './http';
import { FunctionInput, FunctionOutput } from './index';
import { FunctionInput, FunctionOutput, FunctionTrigger } from './index';
import { ServiceBusQueueOutput, ServiceBusTopicOutput } from './serviceBus';
import { StorageBlobInput, StorageBlobOutput, StorageQueueOutput } from './storage';

Expand All @@ -16,7 +16,7 @@ export declare class InvocationContext {
/**
* For testing purposes only. This will always be constructed for you when run in the context of the Azure Functions runtime
*/
constructor(init: InvocationContextInit);
constructor(init?: InvocationContextInit);

/**
* A unique guid specific to this invocation
Expand Down Expand Up @@ -90,6 +90,12 @@ export declare class InvocationContext {
* For example, this will be undefined for http and timer triggers because you can find that information on the request & timer object instead
*/
triggerMetadata?: TriggerMetadata;

/**
* The options used when registering the function
* NOTE: This value may differ slightly from the original because it has been validated and defaults may have been explicitly added
*/
options: EffectiveFunctionOptions;
}

/**
Expand Down Expand Up @@ -246,24 +252,65 @@ export interface TraceContext {
attributes?: Record<string, string>;
}

/**
* The options used when registering the function, as passed to a specific invocation
* NOTE: This value may differ slightly from the original because it has been validated and defaults may have been explicitly added
*/
export interface EffectiveFunctionOptions {
/**
* Configuration for the primary input to the function, aka the reason it will be triggered
* This is the only input that is passed as an argument to the function handler during invocation
*/
trigger: FunctionTrigger;

/**
* Configuration for the optional primary output of the function
* This is the main output that you should set as the return value of the function handler during invocation
*/
return?: FunctionOutput;

/**
* Configuration for an optional set of secondary inputs
* During invocation, get these values with `context.extraInputs.get()`
*/
extraInputs: FunctionInput[];

/**
* Configuration for an optional set of secondary outputs
* During invocation, set these values with `context.extraOutputs.set()`
*/
extraOutputs: FunctionOutput[];
}

/**
* For testing purposes only. This will always be constructed for you when run in the context of the Azure Functions runtime
*/
export interface InvocationContextInit {
/**
* A unique guid for this invocation
* Defaults to 'unknown' if not specified
*/
invocationId: string;
invocationId?: string;

functionName: string;
/**
* Defaults to 'unknown' if not specified
*/
functionName?: string;

logHandler: LogHandler;
/**
* Defaults to Node.js console if not specified
*/
logHandler?: LogHandler;

traceContext?: TraceContext;

retryContext?: RetryContext;

triggerMetadata?: TriggerMetadata;

/**
* Defaults to a trigger with 'unknown' type and name if not specified
*/
options?: Partial<EffectiveFunctionOptions>;
}

export type LogHandler = (level: LogLevel, ...args: unknown[]) => void;
Expand Down