From b96fa784816ecfcb434b6e85b389509c39031c3e Mon Sep 17 00:00:00 2001 From: Daniel Chambers Date: Wed, 6 Dec 2023 13:28:24 +1100 Subject: [PATCH] Added tests for prepare_arguments --- src/connector.ts | 43 ++-- src/test/prepare_arguments_test.ts | 319 +++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+), 25 deletions(-) create mode 100644 src/test/prepare_arguments_test.ts diff --git a/src/connector.ts b/src/connector.ts index 0ff865b..bbeee5f 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -139,18 +139,18 @@ export function getProgramSchema(cmdObj: InferenceConfig): ProgramSchema { * @param payload * @returns the result of invocation with no wrapper */ -async function invoke(functions: RuntimeFunctions, function_definitions: FunctionDefinitions, object_type_definitions: ObjectTypeDefinitions, payload: Payload): Promise { - const func = functions[payload.function]; - const args = prepare_arguments(function_definitions, object_type_definitions, payload); +async function invoke(function_name: string, args: Record, functions: RuntimeFunctions, program_schema: ProgramSchema): Promise { + const func = functions[function_name]; + const prepared_args = prepare_arguments(function_name, args, program_schema.functions, program_schema.object_types); try { - let result = func.apply(undefined, args); + let result = func.apply(undefined, prepared_args); if (typeof result === "object" && 'then' in result && typeof result.then === "function") { result = await result; } return result; } catch (e) { - throw new sdk.InternalServerError(`Error encountered when invoking function ${func}`, { message: e.message, stack: e.stack }); + throw new sdk.InternalServerError(`Error encountered when invoking function ${function_name}`, { message: e.message, stack: e.stack }); } } @@ -162,22 +162,22 @@ async function invoke(functions: RuntimeFunctions, function_definitions: Functio * @param payload * @returns An array of the function's arguments in the definition order */ -function prepare_arguments(function_definitions: FunctionDefinitions, object_type_definitions: ObjectTypeDefinitions, payload: Payload): unknown[] { - const function_definition = function_definitions[payload.function]; +export function prepare_arguments(function_name: string, args: Record, function_definitions: FunctionDefinitions, object_type_definitions: ObjectTypeDefinitions): unknown[] { + const function_definition = function_definitions[function_name]; if(!function_definition) { - throw new sdk.InternalServerError(`Couldn't find function ${payload.function} in schema.`); + throw new sdk.InternalServerError(`Couldn't find function ${function_name} in schema.`); } return function_definition.arguments - .map(argDef => coerce_argument_value(payload.args[argDef.argument_name], argDef.type, [argDef.argument_name], object_type_definitions)); + .map(argDef => coerce_argument_value(args[argDef.argument_name], argDef.type, [argDef.argument_name], object_type_definitions)); } function coerce_argument_value(value: unknown, type: TypeDefinition, value_path: string[], object_type_definitions: ObjectTypeDefinitions): unknown { switch (type.type) { case "array": if (!isArray(value)) - throw new sdk.BadRequest("Unexpected value in function arguments. Expected an array.", { value_path }); + throw new sdk.BadRequest(`Unexpected value in function arguments. Expected an array at '${value_path.join(".")}'.`); return value.map((element, index) => coerce_argument_value(element, type.element_type, [...value_path, `[${index}]`], object_type_definitions)) case "nullable": @@ -199,16 +199,13 @@ function coerce_argument_value(value: unknown, type: TypeDefinition, value_path: } else { const object_type_definition = object_type_definitions[type.name]; if (!object_type_definition) - throw new sdk.InternalServerError(`Couldn't find object type ${type.name} in the schema`); + throw new sdk.InternalServerError(`Couldn't find object type '${type.name}' in the schema`); if (value === null || typeof value !== "object") { - throw new sdk.BadRequest(`Unexpected value in function arguments. Expected an object.`, { value_path }); + throw new sdk.BadRequest(`Unexpected value in function arguments. Expected an object at '${value_path.join(".")}'.`); } - return Object.fromEntries(Object.entries(value).map(([prop_name, prop_value]) => { - const property_definition = object_type_definition.properties.find(def => def.property_name === prop_name); - if (!property_definition) - throw new sdk.BadRequest(`Unexpected property '${prop_name}' on object in function arguments.`, { value_path }); - - return [prop_name, coerce_argument_value(prop_value, property_definition.type, [...value_path, prop_name], object_type_definitions)] + return Object.fromEntries(object_type_definition.properties.map(property_definition => { + const prop_value = (value as Record)[property_definition.property_name]; + return [property_definition.property_name, coerce_argument_value(prop_value, property_definition.type, [...value_path, property_definition.property_name], object_type_definitions)] })); } default: @@ -248,16 +245,12 @@ function pruneFields(func: string, fields: Struct | null | undefined, async function query( configuration: Configuration, state: State, - func: string, + functionName: string, requestArgs: Struct, requestFields?: { [k: string]: sdk.Field; } | null | undefined ): Promise { - const payload: Payload = { - function: func, - args: requestArgs - }; - const result = await invoke(state.functions, configuration.programSchema.functions, configuration.programSchema.object_types, payload); - return pruneFields(func, requestFields, result); + const result = await invoke(functionName, requestArgs, state.functions, configuration.programSchema); + return pruneFields(functionName, requestFields, result); } function resolveArguments( diff --git a/src/test/prepare_arguments_test.ts b/src/test/prepare_arguments_test.ts new file mode 100644 index 0000000..69f675e --- /dev/null +++ b/src/test/prepare_arguments_test.ts @@ -0,0 +1,319 @@ +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as connector from '../connector.ts'; +import { FunctionDefinitions, FunctionNdcKind, NullOrUndefinability, ObjectTypeDefinitions } from "../infer.ts"; + +Deno.test("argument ordering", () => { + const function_name = "test_fn" + const function_definitions: FunctionDefinitions = { + [function_name]: { + ndc_kind: FunctionNdcKind.Function, + description: null, + arguments: [ + { + argument_name: "c", + description: null, + type: { + type: "named", + kind: "scalar", + name: "Float" + } + }, + { + argument_name: "a", + description: null, + type: { + type: "named", + kind: "scalar", + name: "Float" + } + }, + { + argument_name: "b", + description: null, + type: { + type: "named", + kind: "scalar", + name: "Float" + } + }, + ], + result_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + const object_types: ObjectTypeDefinitions = {} + const args = { + b: 1, + a: 2, + c: 3, + } + + const prepared_args = connector.prepare_arguments(function_name, args, function_definitions, object_types); + + test.assertEquals(prepared_args, [ 3, 2, 1 ]); +}) + +Deno.test("nullable type coercion", async t => { + const function_name = "test_fn" + const function_definitions: FunctionDefinitions = { + [function_name]: { + ndc_kind: FunctionNdcKind.Function, + description: null, + arguments: [ + { + argument_name: "nullOnlyArg", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + argument_name: "undefinedOnlyArg", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + argument_name: "nullOrUndefinedArg", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsEither, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + argument_name: "objectArg", + description: null, + type: { + type: "named", + kind: "object", + name: "MyObject", + } + }, + { + argument_name: "nullOnlyArrayArg", + description: null, + type: { + type: "array", + element_type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + }, + { + argument_name: "undefinedOnlyArrayArg", + description: null, + type: { + type: "array", + element_type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + }, + { + argument_name: "nullOrUndefinedArrayArg", + description: null, + type: { + type: "array", + element_type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsEither, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + }, + ], + result_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + const object_types: ObjectTypeDefinitions = { + "MyObject": { + properties: [ + { + property_name: "nullOnlyProp", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + property_name: "undefinedOnlyProp", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + property_name: "nullOrUndefinedProp", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsEither, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + ] + } + } + const test_cases = [ + { + name: "all nulls", + args: { + nullOnlyArg: null, + undefinedOnlyArg: null, + nullOrUndefinedArg: null, + objectArg: { + nullOnlyProp: null, + undefinedOnlyProp: null, + nullOrUndefinedProp: null, + }, + nullOnlyArrayArg: [null, null], + undefinedOnlyArrayArg: [null, null], + nullOrUndefinedArrayArg: [null, null], + }, + expected: [ + null, + undefined, + null, + { nullOnlyProp: null, undefinedOnlyProp: undefined, nullOrUndefinedProp: null }, + [null, null], + [undefined, undefined], + [null, null], + ] + }, + { + name: "all undefineds", + args: { + nullOnlyArg: undefined, + undefinedOnlyArg: undefined, + nullOrUndefinedArg: undefined, + objectArg: { + nullOnlyProp: undefined, + undefinedOnlyProp: undefined, + nullOrUndefinedProp: undefined, + }, + nullOnlyArrayArg: [undefined, undefined], + undefinedOnlyArrayArg: [undefined, undefined], + nullOrUndefinedArrayArg: [undefined, undefined], + }, + expected: [ + null, + undefined, + undefined, + { nullOnlyProp: null, undefinedOnlyProp: undefined, nullOrUndefinedProp: undefined }, + [null, null], + [undefined, undefined], + [undefined, undefined], + ] + }, + { + name: "all missing", + args: { + objectArg: {}, + nullOnlyArrayArg: [], + undefinedOnlyArrayArg: [], + nullOrUndefinedArrayArg: [], + }, + expected: [ + null, + undefined, + undefined, + { nullOnlyProp: null, undefinedOnlyProp: undefined, nullOrUndefinedProp: undefined }, + [], + [], + [], + ] + }, + { + name: "all valued", + args: { + nullOnlyArg: "a", + undefinedOnlyArg: "b", + nullOrUndefinedArg: "c", + objectArg: { + nullOnlyProp: "d", + undefinedOnlyProp: "e", + nullOrUndefinedProp: "f", + }, + nullOnlyArrayArg: ["g", "h"], + undefinedOnlyArrayArg: ["i", "j"], + nullOrUndefinedArrayArg: ["k", "l"], + }, + expected: [ + "a", + "b", + "c", + { nullOnlyProp: "d", undefinedOnlyProp: "e", nullOrUndefinedProp: "f" }, + ["g", "h"], + ["i", "j"], + ["k", "l"], + ] + }, + ]; + + await Promise.all(test_cases.map(test_case => t.step({ + name: test_case.name, + fn: () => { + const prepared_args = connector.prepare_arguments(function_name, test_case.args, function_definitions, object_types); + test.assertEquals(prepared_args, test_case.expected); + }, + sanitizeOps: false, + sanitizeResources: false, + sanitizeExit: false, + }))); + +})