diff --git a/src/spiffworkflow_proxy/plugin_service.py b/src/spiffworkflow_proxy/plugin_service.py index f39fcac..6f435f1 100644 --- a/src/spiffworkflow_proxy/plugin_service.py +++ b/src/spiffworkflow_proxy/plugin_service.py @@ -151,6 +151,42 @@ def param_annotation_desc(param: Parameter) -> dict: return {"id": param_id, "type": param_type_desc, "required": param_req} + @staticmethod + def param_schema(param: Parameter) -> dict: + """Parses a callable parameter's type annotation, if any, to form a ParameterSchema.""" + schema = { + "title": param.name, + } + + none_type = type(None) + supported_types_map = { + str: "string", + int: "integer", + float: "number", + dict: "object", + list: "array", + bool: "boolean", + none_type: "null", + } # https://json-schema.org/understanding-json-schema/reference/type + + annotation = param.annotation + + if annotation in supported_types_map.keys(): + supported_annotation_types = {annotation} + else: + supported_annotation_types = {t for t in typing.get_args(annotation) if t in supported_types_map.keys()} + + # only add the type keyword if at least one type is supported + if(len(supported_annotation_types)): + schema["type"] = [supported_types_map[annotation_type] for annotation_type in supported_annotation_types] + + param_req = param.default is param.empty and none_type not in supported_annotation_types + + if param.default is not param.empty: + schema["default"] = param.default + + return {"schema": schema, "required": param_req} + @staticmethod def callable_params_desc(kallable: Any) -> list[dict]: sig = inspect.signature(kallable) @@ -162,11 +198,27 @@ def callable_params_desc(kallable: Any) -> list[dict]: return params + @staticmethod + def callable_params_schema(kallable: Any) -> list[dict]: + sig = inspect.signature(kallable) + params_to_skip = ["self", "kwargs"] + sig_params = filter(lambda param: param.name not in params_to_skip, sig.parameters.values()) + params = [PluginService.param_schema(param) for param in sig_params] + schema = { + "title": "Parameters", + "type": "object", + "required": [p["schema"]["title"] for p in params if p["required"]], + "properties": {p["schema"]["title"]: p["schema"] for p in params} + } + + return schema + @staticmethod def describe_target(plugin_name: str, target_name: str, target: type) -> dict: parameters = PluginService.callable_params_desc(target.__init__) # type: ignore target_id = PluginService.target_id(plugin_name, target_name) - return {"id": target_id, "parameters": parameters} + schema = PluginService.callable_params_schema(target.__init__) + return {"id": target_id, "parameters": parameters, "schema": schema} @staticmethod def is_connector_command(module: Any) -> bool: diff --git a/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py b/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py index df93265..7509442 100644 --- a/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py +++ b/tests/mock_connectors/connector-example/src/connector_example/commands/combine_strings.py @@ -9,7 +9,7 @@ class CombineStrings(ConnectorCommand): """Takes two strings, combines them together, and returns a single string! AMAZIN!.""" def __init__( - self, arg1: str, arg2: str + self, arg1: str, arg2: str = "foo" ): """ :param arg1: The First Argument diff --git a/tests/spiffworkflow_proxy/unit/plugin_service_test.py b/tests/spiffworkflow_proxy/unit/plugin_service_test.py index bc7af4f..2421def 100644 --- a/tests/spiffworkflow_proxy/unit/plugin_service_test.py +++ b/tests/spiffworkflow_proxy/unit/plugin_service_test.py @@ -20,3 +20,43 @@ def test_plugin_for_display_name() -> None: def test_available_commands_by_plugin() -> None: commands = PluginService.available_commands_by_plugin() assert(list(commands['connector_example'].keys()) == ['CombineStrings']) + +def test_describe_target() -> None: + commands = PluginService.available_commands_by_plugin() + combine_strings = commands["connector_example"]["CombineStrings"] + description = PluginService.describe_target( + "connector_example", "CombineStrings", combine_strings + ) + assert description == { + "id": "example/CombineStrings", + "parameters": [ + { + "id": "arg1", + "required": True, + "type": "str", + }, + { + "id": "arg2", + "required": False, + "type": "str", + } + ], + "schema": { + "title": "Parameters", + "type": "object", + "required": [ + "arg1" + ], + "properties": { + "arg1": { + "title": "arg1", + "type": ["string"] + }, + "arg2": { + "title": "arg2", + "type": ["string"], + "default": "foo" + } + } + } + }