From d1e442670673e18bea0a341af73a51f3678f020f Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Tue, 26 Nov 2024 13:05:19 -0500 Subject: [PATCH] feat(bindgen): support string choices Future todo: handle these as string enums. --- .../itk-wasm/src/bindgen/canonical-type.js | 5 +- .../bindgen/python/function-module-args.js | 19 +++--- .../python/wasi/wasi-function-module.js | 62 ++++++++++++------- .../src/bindgen/typescript/function-module.js | 14 ++++- .../input-output-json-test.cxx | 3 + 5 files changed, 69 insertions(+), 34 deletions(-) diff --git a/packages/core/typescript/itk-wasm/src/bindgen/canonical-type.js b/packages/core/typescript/itk-wasm/src/bindgen/canonical-type.js index c3701398c..b6ecab2e0 100644 --- a/packages/core/typescript/itk-wasm/src/bindgen/canonical-type.js +++ b/packages/core/typescript/itk-wasm/src/bindgen/canonical-type.js @@ -1,7 +1,8 @@ function canonicalType(parameterType) { // Strip extras - const canonical = parameterType.split(' ')[0] + let canonical = parameterType.split(' ')[0] + canonical = canonical.split(':')[0] return canonical } -export default canonicalType \ No newline at end of file +export default canonicalType diff --git a/packages/core/typescript/itk-wasm/src/bindgen/python/function-module-args.js b/packages/core/typescript/itk-wasm/src/bindgen/python/function-module-args.js index 9414f8c12..9ee168b7f 100644 --- a/packages/core/typescript/itk-wasm/src/bindgen/python/function-module-args.js +++ b/packages/core/typescript/itk-wasm/src/bindgen/python/function-module-args.js @@ -5,13 +5,15 @@ import interfaceJsonTypeToInterfaceType from '../interface-json-type-to-interfac import canonicalType from '../canonical-type.js' function functionModuleArgs(interfaceJson) { - let functionArgs = "" + let functionArgs = '' interfaceJson['inputs'].forEach((value) => { const canonical = canonicalType(value.type) const pythonType = interfaceJsonTypeToPythonType.get(canonical) functionArgs += ` ${snakeCase(value.name)}: ${pythonType},\n` }) - const outputFiles = interfaceJson.outputs.filter(o => { return o.type.includes('FILE') }) + const outputFiles = interfaceJson.outputs.filter((o) => { + return o.type.includes('FILE') + }) outputFiles.forEach((output) => { const isArray = output.itemsExpectedMax > 1 const optionName = `${output.name}` @@ -22,19 +24,19 @@ function functionModuleArgs(interfaceJson) { } }) interfaceJson['parameters'].forEach((value) => { - if (value.name === "memory-io" || value.name === "version") { + if (value.name === 'memory-io' || value.name === 'version') { return } const canonical = canonicalType(value.type) const pythonType = interfaceJsonTypeToPythonType.get(canonical) if (interfaceJsonTypeToInterfaceType.has(value.type)) { - if(value.required && value.itemsExpectedMax > 1) { + if (value.required && value.itemsExpectedMax > 1) { functionArgs += ` ${snakeCase(value.name)}: List[${pythonType}] = [],\n` } else { functionArgs += ` ${snakeCase(value.name)}: Optional[${pythonType}] = None,\n` } } else { - if(value.itemsExpectedMax > 1) { + if (value.itemsExpectedMax > 1) { if (value.required) { functionArgs += ` ${snakeCase(value.name)}: List[${pythonType}]` } else { @@ -42,17 +44,16 @@ function functionModuleArgs(interfaceJson) { } } else { functionArgs += ` ${snakeCase(value.name)}: ${pythonType}` - } - if(value.type === "BOOL") { + if (value.type === 'BOOL') { functionArgs += ` = False,\n` - } else if(value.type === "TEXT") { + } else if (value.type.startsWith('TEXT')) { if (value.default) { functionArgs += ` = "${value.default}",\n` } else { functionArgs += ` = "",\n` } - } else if(value.required && value.itemsExpectedMax > 1) { + } else if (value.required && value.itemsExpectedMax > 1) { functionArgs += ` = [],\n` } else { if (value.itemsExpectedMax > 1) { diff --git a/packages/core/typescript/itk-wasm/src/bindgen/python/wasi/wasi-function-module.js b/packages/core/typescript/itk-wasm/src/bindgen/python/wasi/wasi-function-module.js index 322225957..ec3f3f407 100644 --- a/packages/core/typescript/itk-wasm/src/bindgen/python/wasi/wasi-function-module.js +++ b/packages/core/typescript/itk-wasm/src/bindgen/python/wasi/wasi-function-module.js @@ -53,8 +53,8 @@ from itkwasm import ( const interfaceType = interfaceJsonTypeToInterfaceType.get(output.type) const isArray = output.itemsExpectedMax > 1 switch (interfaceType) { - case "TextFile": - case "BinaryFile": + case 'TextFile': + case 'BinaryFile': if (isArray) { haveArray = true pipelineOutputs += ` *${snakeCase(output.name)}_pipeline_outputs,\n` @@ -97,12 +97,12 @@ from itkwasm import ( if (interfaceJsonTypeToInterfaceType.has(input.type)) { const interfaceType = interfaceJsonTypeToInterfaceType.get(input.type) switch (interfaceType) { - case "TextFile": - case "BinaryFile": + case 'TextFile': + case 'BinaryFile': pipelineInputs += ` PipelineInput(InterfaceTypes.${interfaceType}, ${interfaceType}(PurePosixPath(${snakeCase(input.name)}))),\n` break - case "TextStream": - case "BinaryStream": + case 'TextStream': + case 'BinaryStream': pipelineInputs += ` PipelineInput(InterfaceTypes.${interfaceType}, ${interfaceType}(${snakeCase(input.name)})),\n` break default: @@ -113,7 +113,7 @@ from itkwasm import ( let args = ` args: List[str] = ['--memory-io',]\n` let inputCount = 0 - args += " # Inputs\n" + args += ' # Inputs\n' interfaceJson.inputs.forEach((input) => { const snakeName = snakeCase(input.name) if (interfaceJsonTypeToInterfaceType.has(input.type)) { @@ -122,7 +122,9 @@ from itkwasm import ( args += ` if not Path(${snakeName}).exists():\n` args += ` raise FileNotFoundError("${snakeName} does not exist")\n` } - const name = interfaceType.includes('File') ? `str(PurePosixPath(${snakeName}))` : `'${inputCount.toString()}'` + const name = interfaceType.includes('File') + ? `str(PurePosixPath(${snakeName}))` + : `'${inputCount.toString()}'` args += ` args.append(${name})\n` inputCount++ } else { @@ -131,7 +133,7 @@ from itkwasm import ( }) let outputCount = 0 - args += " # Outputs\n" + args += ' # Outputs\n' interfaceJson.outputs.forEach((output) => { const snake = snakeCase(output.name) if (interfaceJsonTypeToInterfaceType.has(output.type)) { @@ -158,7 +160,7 @@ from itkwasm import ( } }) - args += " # Options\n" + args += ' # Options\n' args += ` input_count = len(pipeline_inputs)\n` interfaceJson.parameters.forEach((parameter) => { if (parameter.name === 'memory-io' || parameter.name === 'version') { @@ -166,7 +168,7 @@ from itkwasm import ( return } const snake = snakeCase(parameter.name) - if (parameter.type === "BOOL") { + if (parameter.type === 'BOOL') { args += ` if ${snake}:\n` args += ` args.append('--${parameter.name}')\n` } else if (parameter.itemsExpectedMax > 1) { @@ -177,7 +179,9 @@ from itkwasm import ( args += ` args.append('--${parameter.name}')\n` args += ` for value in ${snake}:\n` if (interfaceJsonTypeToInterfaceType.has(parameter.type)) { - const interfaceType = interfaceJsonTypeToInterfaceType.get(parameter.type) + const interfaceType = interfaceJsonTypeToInterfaceType.get( + parameter.type + ) if (interfaceType.includes('File')) { // for files args += ` input_file = str(PurePosixPath(value))\n` @@ -195,12 +199,19 @@ from itkwasm import ( args += ` input_count += 1\n` } } else { + if (parameter.type.startsWith('TEXT:{')) { + const choices = parameter.type.split('{')[1].split('}')[0].split(',') + args += ` if ${snake} not in (${choices.map((c) => `'${c}'`).join(',')}):\n` + args += ` raise ValueError(f'${snake} must be one of ${choices.join(', ')}')\n` + } args += ` args.append(str(value))\n` } } else { if (interfaceJsonTypeToInterfaceType.has(parameter.type)) { args += ` if ${snake} is not None:\n` - const interfaceType = interfaceJsonTypeToInterfaceType.get(parameter.type) + const interfaceType = interfaceJsonTypeToInterfaceType.get( + parameter.type + ) if (interfaceType.includes('File')) { // for files args += ` input_file = str(PurePosixPath(${snakeCase(parameter.name)}))\n` @@ -222,6 +233,11 @@ from itkwasm import ( } } else { args += ` if ${snake}:\n` + if (parameter.type.startsWith('TEXT:{')) { + const choices = parameter.type.split('{')[1].split('}')[0].split(',') + args += ` if ${snake} not in (${choices.map((c) => `'${c}'`).join(',')}):\n` + args += ` raise ValueError(f'${snake} must be one of ${choices.join(', ')}')\n` + } args += ` args.append('--${parameter.name}')\n` args += ` args.append(str(${snake}))\n` } @@ -234,23 +250,23 @@ from itkwasm import ( const canonical = canonicalType(type) const pythonType = interfaceJsonTypeToPythonType.get(canonical) switch (pythonType) { - case "os.PathLike": + case 'os.PathLike': return `Path(${value}.data.path)` - case "str": + case 'str': if (type === 'TEXT') { return `${value}` } else { return `${value}.data.data` } - case "bytes": + case 'bytes': return `${value}.data.data` - case "int": + case 'int': return `int(${value})` - case "bool": + case 'bool': return `bool(${value})` - case "float": + case 'float': return `float(${value})` - case "Any": + case 'Any': return `${value}.data` default: return `${value}.data` @@ -258,10 +274,12 @@ from itkwasm import ( } outputCount = 0 const jsonOutputs = interfaceJson['outputs'] - const numOutputs = interfaceJson.outputs.filter(o => !o.type.includes('FILE')).length + const numOutputs = interfaceJson.outputs.filter( + (o) => !o.type.includes('FILE') + ).length if (numOutputs > 1) { postOutput += ' result = (\n' - } else if (numOutputs === 1){ + } else if (numOutputs === 1) { postOutput = ' result = ' } diff --git a/packages/core/typescript/itk-wasm/src/bindgen/typescript/function-module.js b/packages/core/typescript/itk-wasm/src/bindgen/typescript/function-module.js index 316b0f488..4d61c3d6f 100644 --- a/packages/core/typescript/itk-wasm/src/bindgen/typescript/function-module.js +++ b/packages/core/typescript/itk-wasm/src/bindgen/typescript/function-module.js @@ -478,7 +478,13 @@ function functionModule( functionContent += ' args.push(inputCountString)\n\n' } } else { - functionContent += ' args.push(value.toString())\n\n' + if (parameter.type.startsWith('TEXT:{')) { + const choices = parameter.type.split('{')[1].split('}')[0].split(',') + functionContent += ` if (![${choices.map((c) => `'${c}'`).join(', ')}].includes(options.${camel})) {\n` + functionContent += ` throw new Error('"${parameter.name}" option must be one of ${choices.join(', ')}')\n` + functionContent += ' }\n' + } + functionContent += ' args.push(value.toString())\n' } functionContent += forNode ? ' })\n' : ' }))\n' } else { @@ -518,6 +524,12 @@ function functionModule( functionContent += ` args.push('--${parameter.name}', inputCountString)\n\n` } } else { + if (parameter.type.startsWith('TEXT:{')) { + const choices = parameter.type.split('{')[1].split('}')[0].split(',') + functionContent += ` if (![${choices.map((c) => `'${c}'`).join(', ')}].includes(options.${camel})) {\n` + functionContent += ` throw new Error('"${parameter.name}" option must be one of ${choices.join(', ')}')\n` + functionContent += ' }\n' + } functionContent += ` args.push('--${parameter.name}', options.${camel}.toString())\n\n` } } diff --git a/packages/core/typescript/itk-wasm/test/pipelines/input-output-json-pipeline/input-output-json-test.cxx b/packages/core/typescript/itk-wasm/test/pipelines/input-output-json-pipeline/input-output-json-test.cxx index 644b10a68..bee896b6f 100644 --- a/packages/core/typescript/itk-wasm/test/pipelines/input-output-json-pipeline/input-output-json-test.cxx +++ b/packages/core/typescript/itk-wasm/test/pipelines/input-output-json-pipeline/input-output-json-test.cxx @@ -28,6 +28,9 @@ int main( int argc, char * argv[] ) itk::wasm::InputTextStream inputJson; pipeline.add_option("input-json", inputJson, "The input json")->type_name("INPUT_JSON"); + std::string stringChoice = "valuea"; + pipeline.add_option("--string-choice", stringChoice, "A string choice, one of: valuea, valueb, or valuec")->check(CLI::IsMember({"valuea", "valueb", "valuec"})); + itk::wasm::OutputTextStream outputJson; pipeline.add_option("output-json", outputJson, "The output json")->type_name("OUTPUT_JSON");