diff --git a/docs/custom_search_commands.md b/docs/custom_search_commands.md index 0d367dda0a..193d080d55 100644 --- a/docs/custom_search_commands.md +++ b/docs/custom_search_commands.md @@ -62,12 +62,15 @@ python.version = python3 | arguments\* | array[objects] | Arguments which can be passed to custom search command. | | requiredSearchAssistant | boolean | Specifies whether search assistance is required for the custom search command. Default: false. | | usage | string | Defines the usage of custom search command. It can be one of `public`, `private` and `deprecated`. | -| description | string | Provide description of the custom search command. | -| syntax | string | Provide syntax for custom search command | +| description | string or array[string] | Provide description of the custom search command. To increase the readability of a comprehensive description in json, it is possible to split it in an array of strings. | +| shortdesc | string | A one sentence description of the search command, used for searchbnf.conf | +| syntax | string | Syntax for custom search commands will be automatically generated based on the command name and the parameters. If the syntax attribute is specified, the provided string is used instead. | +| tags | string | One or more words that users might type into the search bar which are similar to the command name. | +| examples | array[objects] | List of example search strings, used for searchbnf.conf | To generate a custom search command, the following attributes must be defined in globalConfig: `commandName`, `commandType`, `fileName`, and `arguments`. Based on the provided commandType, UCC will generate a template Python file and integrate the user-defined logic into it. -If `requiredSearchAssistant` is set to True, the `syntax`, `description`, and `usage` attributes are mandatory, as they are essential for generating `searchbnf.conf`. For more information about these attributes please refer to the [searchbnf.conf docs](https://docs.splunk.com/Documentation/Splunk/9.4.2/Admin/Searchbnfconf) +If `requiredSearchAssistant` is set to True, `description`, and `usage` attributes are mandatory, as they are essential for generating `searchbnf.conf`. The command syntax is automatically derived from the command specification. For more information about these attributes please refer to the [searchbnf.conf docs](https://docs.splunk.com/Documentation/Splunk/9.4.2/Admin/Searchbnfconf) **NOTE:** The user-defined Python file must include specific functions based on the command type: @@ -83,9 +86,11 @@ If `requiredSearchAssistant` is set to True, the `syntax`, `description`, and `u | name\* | string | Name of the argument | | defaultValue | string/number | Default value of the argument. | | required | boolean | Specify if the argument is required or not. | -| validate | object | Specify validation for the argument. It can be any of `Integer`, `Float`, `Boolean`, `RegularExpression` or `FieldName`. | +| validate | object | Specify validation for the argument. It can be any of `Integer`, `Float`, `Boolean`, `RegularExpression`, `FieldName`, `Set`, `Match`, `List`, `Map`, `Duration`. | +| syntax | string | Syntax for arguments is automatically generated based on the validation. If the syntax attribute for an argument is specified, the syntax value is used for the parameter value instead. The syntax string must only specify the value not the argument name. | +| syntaxGeneration | boolean | Specifies if the parameter should be added to the syntax. If `syntaxGeneration` is false, the parameter is omitted. Default: true. | -UCC currently supports five types of validations provided by `splunklib` library: +UCC currently supports some types of validations provided by `splunklib` library: - IntegerValidator + you can optionally define `minimum` and `maximum` properties. @@ -95,10 +100,26 @@ UCC currently supports five types of validations provided by `splunklib` library + no additional properties required. - RegularExpressionValidator + no additional properties required. + + validates if the argument value is a valid regex expression. - FieldnameValidator + no additional properties required. +- SetValidator + + the property `values` is required, which is a list of allowed strings. + + validates if the values list contains the argument value. +- MatchValidator + + the properties `name` and `pattern` is required, where the name is only used for error messages and the pattern must be a valid regex pattern. + + validates of the argument value matches the specified regex expression. +- ListValidator + + no additional properties required. + + validates if the argument value is a valid list and passes the parsed list to the property. +- MapValidator + + the property `map` is required, where the map must be a dictionary of key value pairs where the key must be a string and the value must either be a string, a number or a boolean. + + validates if the argument matches a key of the dictionary and passes the corresponding value to the property. +- DurationValidator + + no additional properties required. -For more information, refer [splunklib API docs](https://splunk-python-sdk.readthedocs.io/en/latest/searchcommands.html) + +For more information, refer [splunklib API docs](https://splunk-python-sdk.readthedocs.io/en/latest/searchcommands.html) or [validators.py source](https://github.com/splunk/splunk-sdk-python/blob/develop/splunklib/searchcommands/validators.py). For example: @@ -126,11 +147,61 @@ For example: "validate": { "type": "Float", "minimum": "85.5" + }, + "syntaxGeneration": false + }, + { + "name": "animals", + "validate": { + "type": "Set", + "values": [ + "cat", + "dog", + "wombat" + ] + } + }, + { + "name": "last", + "validate": { + "type": "Match", + "name": "Day duration", + "pattern": "^[0-9]+(d|m|y)?$" + }, + "syntax": "(d|m|y)?" + }, + { + "name": "urgency", + "validate": { + "type": "Map", + "map": { + "high": 3, + "medium": 2, + "low": 1 + } } - } ] +``` +## Examples (for search command usage) + +| Property | Type | Description | +| ------------------------------------------------ | ------ | ------------------------------------------------ | +| search\* | string | Example search command | +| comment\* | string | Provide description of the example search string | + +Each search command can have multiple examples, which are shown displayed in the search assistant. The Compact mode, only shows the first example. In the Full mode, the top three examples are displayed. + +For example: + +```json +"examples": [ + { + "search": "generatetextcommand count=5 text=\"Hallo There\"", + "comment": "Generates 5 \"Hallo There\" events enumerated starting by 1" + } +] ``` ## Example @@ -161,6 +232,12 @@ For example: "name": "text", "required": true } + ], + "examples": [ + { + "search": "generatetextcommand count=5 text=\"Hallo There\"", + "comment": "Generates 5 \"Hallo There\" events enumerated starting by 1" + } ] }, ], diff --git a/splunk_add_on_ucc_framework/generators/conf_files/create_searchbnf_conf.py b/splunk_add_on_ucc_framework/generators/conf_files/create_searchbnf_conf.py index 00e63c7f61..7c78ca6f62 100644 --- a/splunk_add_on_ucc_framework/generators/conf_files/create_searchbnf_conf.py +++ b/splunk_add_on_ucc_framework/generators/conf_files/create_searchbnf_conf.py @@ -31,11 +31,66 @@ def __init__( if global_config.has_custom_search_commands(): for command in global_config.custom_search_commands: if command.get("requiredSearchAssistant", False): + if "syntax" in command: + syntax = command["syntax"] + else: + params_syntax = [] + for param in command["arguments"]: + if param.get("syntaxGeneration", True): + validator = param.get("validate", {}).get("type", None) + if "syntax" in param: + param_syntax = f"{param['name']}={param['syntax']}" + elif validator and validator in ( + "Set", + "Integer", + "Float", + "Boolean", + "List", + "Duration", + "Map", + ): + if validator in ("Integer", "Float", "Duration"): + param_syntax = f"{param['name']}=" + if validator == "Boolean": + param_syntax = f"{param['name']}=" + if validator == "Set": + param_syntax = f"{param['name']}=({'|'.join(param['validate']['values'])})" + if validator == "List": + param_syntax = ( + f"{param['name']}=(,)*" + ) + if validator == "Map": + param_syntax = f"{param['name']}=({'|'.join(param['validate']['map'].keys())})" + else: + param_syntax = f"{param['name']}=" + if param.get("required", False): + params_syntax.append(param_syntax) + else: + params_syntax.append(f"({param_syntax})?") + + syntax = f"{command['commandName']} {' '.join(params_syntax)}" + if len(syntax) > 120: + syntax = syntax.split(" ") + syntax_lines = [syntax[0]] + for part in syntax[1:]: + if len(syntax_lines[-1]) < 100: + syntax_lines[-1] += f" {part}" + else: + syntax_lines.append(part) + syntax = " \\\n".join(syntax_lines) + + description = command["description"] + if isinstance(description, list): + description = " \\\n".join(description) + searchbnf_dict = { "command_name": command["commandName"], - "description": command["description"], - "syntax": command["syntax"], + "description": description, + "shortdesc": command.get("shortdesc", None), + "syntax": syntax, "usage": command["usage"], + "tags": command.get("tags", None), + "examples": command.get("examples", []), } self.searchbnf_info.append(searchbnf_dict) diff --git a/splunk_add_on_ucc_framework/generators/python_files/create_custom_command_python.py b/splunk_add_on_ucc_framework/generators/python_files/create_custom_command_python.py index 40d7c7a5fd..c4cebdbf19 100644 --- a/splunk_add_on_ucc_framework/generators/python_files/create_custom_command_python.py +++ b/splunk_add_on_ucc_framework/generators/python_files/create_custom_command_python.py @@ -58,12 +58,16 @@ def __init__( "default": argument.get("defaultValue"), } self.argument_generator(argument_list, argument_dict) + + description = command.get("description") + if description and isinstance(description, list): + description = "\n ".join(description) self.commands_info.append( { "imported_file_name": imported_file_name, "file_name": command["commandName"], "class_name": command["commandName"].title(), - "description": command.get("description"), + "description": description, "syntax": command.get("syntax"), "template": template, "list_arg": argument_list, @@ -92,6 +96,18 @@ def argument_generator( if args else f", validate=validators.{validate_type}()" ) + elif validate_type == "Set": + allowed_values = validate.get("values") + validate_str = ( + f", validate=validators.Set({str(allowed_values).strip('[]')})" + ) + elif validate_type == "Map": + option_map = validate.get("map") + validate_str = f", validate=validators.Map(**{str(option_map)})" + elif validate_type == "Match": + name = validate.get("name") + pattern = validate.get("pattern") + validate_str = f", validate=validators.Match('{name}', '{pattern}')" else: validate_str = f", validate=validators.{validate_type}()" @@ -108,6 +124,7 @@ def argument_generator( f"{validate_str}, " f"default='{arg.get('default', '')}')" ) + argument_list.append(arg_str) return argument_list diff --git a/splunk_add_on_ucc_framework/global_config_validator.py b/splunk_add_on_ucc_framework/global_config_validator.py index bbbee491f0..53b3821ee8 100644 --- a/splunk_add_on_ucc_framework/global_config_validator.py +++ b/splunk_add_on_ucc_framework/global_config_validator.py @@ -730,17 +730,18 @@ def _validate_custom_search_commands(self) -> None: if (command.get("requiredSearchAssistant", False) is False) and ( command.get("description") + or command.get("shortdesc") or command.get("usage") or command.get("syntax") + or command.get("tags") + or command.get("examples") ): logger.warning( "requiredSearchAssistant is set to false " "but attributes required for 'searchbnf.conf' is defined which is not required." ) if (command.get("requiredSearchAssistant", False) is True) and not ( - command.get("description") - and command.get("usage") - and command.get("syntax") + command.get("description") and command.get("usage") ): raise GlobalConfigValidatorException( "One of the attributes among `description`, `usage`, `syntax`" diff --git a/splunk_add_on_ucc_framework/schema/schema.json b/splunk_add_on_ucc_framework/schema/schema.json index 6e17fa0893..cc8e1b345c 100644 --- a/splunk_add_on_ucc_framework/schema/schema.json +++ b/splunk_add_on_ucc_framework/schema/schema.json @@ -458,12 +458,25 @@ "description": "Specifies if search assistant is required or not. If yes then searchbnf.conf will be generated." }, "description": { - "type": "string", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], "description": "Description of the custom search command. It is an required attribute for searchbnf.conf." }, + "shortdesc": { + "type": "string", + "description": "Short description or the custom search command, used as shortdesc in searchbnf.conf" + }, "syntax": { "type": "string", - "maxLength": 100, "description": "Syntax for the custom search command. It is an required attribute for searchbnf.conf." }, "usage": { @@ -475,12 +488,23 @@ "deprecated" ] }, + "tags": { + "type": "string", + "description": "One or more words that users might type into the search bar which are similar to the command name." + }, "arguments": { "type": "array", "items": { "$ref": "#/definitions/arguments" }, "minItems": 1 + }, + "examples": { + "type": "array", + "items": { + "$ref": "#/definitions/searchExample" + }, + "minItems": 1 } }, "required": [ @@ -519,6 +543,15 @@ ] }, "description": "Provide default value to the arguments passed for custom search command" + }, + "syntaxGeneration": { + "type": "boolean", + "default": true, + "description": "Generate parameter syntax" + }, + "syntax": { + "type": "string", + "description": "Syntax string for the value of the parameter" } }, "required": [ @@ -526,6 +559,25 @@ ], "additionalProperties": false }, + "searchExample": { + "type": "object", + "description": "Search example used for searchbnf.conf", + "properties": { + "search": { + "type": "string", + "description": "Example search string" + }, + "comment": { + "type": "string", + "description": "Comment for the search string which is used for searchbnf.conf" + } + }, + "required": [ + "search", + "comment" + ], + "additionalProperties": false + }, "CustomSearchCommandValidator": { "type": "object", "description": "It is used to validate the values of arguments for custom search command", @@ -544,6 +596,21 @@ }, { "$ref": "#/definitions/CustomBooleanValidator" + }, + { + "$ref": "#/definitions/CustomSetValidator" + }, + { + "$ref": "#/definitions/CustomMatchValidator" + }, + { + "$ref": "#/definitions/CustomListValidator" + }, + { + "$ref": "#/definitions/CustomMapValidator" + }, + { + "$ref": "#/definitions/CustomDurationValidator" } ] }, @@ -633,6 +700,111 @@ ], "additionalProperties": false }, + "CustomSetValidator": { + "type": "object", + "properties": { + "type": { + "const": "Set", + "type": "string", + "description": "Validates value against a set of allowed values." + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of allowed values." + } + }, + "required": [ + "type", + "values" + ], + "additionalProperties": false + }, + "CustomMatchValidator": { + "type": "object", + "properties": { + "type": { + "const": "Match", + "type": "string", + "description": "Validates option values by regex pattern" + }, + "name": { + "type": "string", + "description": "Name for the pattern, which is used for the error message." + }, + "pattern": { + "type": "string", + "description": "Regular expression pattern to validate against." + } + }, + "required": [ + "type", + "pattern" + ], + "additionalProperties": false + }, + "CustomListValidator": { + "type": "object", + "properties": { + "type": { + "const": "List", + "type": "string", + "description": "Validates a list of strings." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "CustomMapValidator": { + "type": "object", + "properties": { + "type": { + "const": "Map", + "type": "string", + "description": "Validates map option values where the value must be a valid key which is replaced by the value.." + }, + "map": { + "type": "object", + "description": "Map which is used to validate and translate option values.", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + } + }, + "required": [ + "type", + "map" + ], + "additionalProperties": false + }, + "CustomDurationValidator": { + "type": "object", + "properties": { + "type": { + "const": "Duration", + "type": "string", + "description": "Validates duration option values." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, "ConfigurationPage": { "type": "object", "properties": { diff --git a/splunk_add_on_ucc_framework/templates/conf_files/searchbnf_conf.template b/splunk_add_on_ucc_framework/templates/conf_files/searchbnf_conf.template index 5896c68794..2cd06743c0 100644 --- a/splunk_add_on_ucc_framework/templates/conf_files/searchbnf_conf.template +++ b/splunk_add_on_ucc_framework/templates/conf_files/searchbnf_conf.template @@ -2,5 +2,15 @@ [{{info["command_name"]}}-command] syntax = {{info["syntax"]}} description = {{info["description"]}} +{% if info["shortdesc"]%} +shortdesc = {{info["shortdesc"]}} +{% endif %} usage = {{info["usage"]}} +{% if info["tags"]%} +tags = {{info["tags"]}} +{% endif %} +{% for example in info["examples"] %} +example{{ loop.index }} = {{ example["search"] }} +comment{{ loop.index }} = {{ example["comment"] }} +{% endfor -%} {% endfor -%} \ No newline at end of file diff --git a/tests/unit/generators/conf_files/test_create_searchbnf_conf.py b/tests/unit/generators/conf_files/test_create_searchbnf_conf.py index 25b18cfe96..1ca0428613 100644 --- a/tests/unit/generators/conf_files/test_create_searchbnf_conf.py +++ b/tests/unit/generators/conf_files/test_create_searchbnf_conf.py @@ -14,6 +14,88 @@ def custom_search_command_without_search_assistance(): ] +@fixture +def custom_search_command_without_optional_search_assistance_params(): + return [ + { + "commandName": "generatetextcommand", + "commandType": "generating", + "fileName": "generatetext.py", + "requiredSearchAssistant": True, + "description": "This command generates COUNT occurrences of a TEXT string.", + "syntax": "generatetextcommand count= text=", + "usage": "public", + } + ] + + +@fixture +def custom_search_command_syntax_autogeneration(): + return [ + { + "commandName": "generatetextcommand", + "commandType": "generating", + "fileName": "generatetext.py", + "requiredSearchAssistant": True, + "description": "This command generates COUNT occurrences of a TEXT string.", + "usage": "public", + "arguments": [ + { + "name": "count", + "required": True, + "validate": {"type": "Integer", "minimum": 1, "maximum": 10}, + "default": 5, + }, + {"name": "test", "required": True, "validate": {"type": "Fieldname"}}, + { + "name": "percent", + "validate": {"type": "Float", "minimum": "85.5"}, + "syntaxGeneration": False, + }, + { + "name": "animals", + "validate": {"type": "Set", "values": ["cat", "dog", "wombat"]}, + }, + { + "name": "last", + "validate": { + "type": "Match", + "name": "Day duration", + "pattern": "^[0-9]+(d|m|y)?$", + }, + "syntax": "(d|m|y)?", + }, + { + "name": "urgency", + "validate": { + "type": "Map", + "map": {"high": 3, "medium": 2, "low": 1}, + }, + }, + ], + } + ] + + +@fixture +def custom_search_command_with_multiline_description(): + return [ + { + "commandName": "generatetextcommand", + "commandType": "generating", + "fileName": "generatetext.py", + "requiredSearchAssistant": True, + "description": [ + "This command generates COUNT occurrences of a TEXT string.", + "This might be additional information.", + "Here we can mention something like wombats are cool.", + ], + "syntax": "generatetextcommand count= text=", + "usage": "public", + } + ] + + def test_init_without_custom_command( global_config_only_configuration, input_dir, @@ -42,8 +124,20 @@ def test_init( { "command_name": "generatetextcommand", "description": "This command generates COUNT occurrences of a TEXT string.", + "shortdesc": "Command for generating string events.", "syntax": "generatetextcommand count= text=", "usage": "public", + "tags": "text generator", + "examples": [ + { + "search": '| generatetextcommand count=5 text="example string"', + "comment": 'Generates 5 events with text="example string"', + }, + { + "search": '| generatetextcommand count=10 text="another example string"', + "comment": 'Generates 10 events with text="another example string"', + }, + ], } ] @@ -65,6 +159,64 @@ def test_init_without_search_assistance( assert searchbnf_conf.searchbnf_info == [] +def test_init_without_optional_search_assistance_params( + global_config_all_json, + input_dir, + output_dir, + custom_search_command_without_optional_search_assistance_params, +): + global_config_all_json._content["customSearchCommand"] = ( + custom_search_command_without_optional_search_assistance_params + ) + searchbnf_conf = SearchbnfConf( + global_config_all_json, + input_dir, + output_dir, + ) + assert searchbnf_conf.searchbnf_info == [ + { + "command_name": "generatetextcommand", + "description": "This command generates COUNT occurrences of a TEXT string.", + "syntax": "generatetextcommand count= text=", + "usage": "public", + "examples": [], + "shortdesc": None, + "tags": None, + } + ] + + +def test_init_search_command_syntax_autogeneration( + global_config_all_json, + input_dir, + output_dir, + custom_search_command_syntax_autogeneration, +): + global_config_all_json._content["customSearchCommand"] = ( + custom_search_command_syntax_autogeneration + ) + searchbnf_conf = SearchbnfConf( + global_config_all_json, + input_dir, + output_dir, + ) + assert searchbnf_conf.searchbnf_info == [ + { + "command_name": "generatetextcommand", + "description": "This command generates COUNT occurrences of a TEXT string.", + "syntax": ( + "generatetextcommand count= test= " + "(animals=(cat|dog|wombat))? (last=(d|m|y)?)? " + "(urgency=(high|medium|low))?" + ), + "usage": "public", + "examples": [], + "shortdesc": None, + "tags": None, + } + ] + + def test_generate_conf_without_custom_command( global_config_only_configuration, input_dir, @@ -95,6 +247,117 @@ def test_generate_conf(global_config_all_json, input_dir, output_dir): [generatetextcommand-command] syntax = generatetextcommand count= text= description = This command generates COUNT occurrences of a TEXT string. + shortdesc = Command for generating string events. + usage = public + tags = text generator + example1 = | generatetextcommand count=5 text="example string" + comment1 = Generates 5 events with text="example string" + example2 = | generatetextcommand count=10 text="another example string" + comment2 = Generates 10 events with text="another example string" + """ + ).lstrip() + assert output == [ + { + "file_name": exp_fname, + "file_path": f"{output_dir}/{ta_name}/default/{exp_fname}", + "content": expected_content, + } + ] + + +def test_generate_conf_without_optional_search_assistance_params( + global_config_all_json, + input_dir, + output_dir, + custom_search_command_without_optional_search_assistance_params, +): + global_config_all_json._content["customSearchCommand"] = ( + custom_search_command_without_optional_search_assistance_params + ) + ta_name = global_config_all_json.product + searchbnf_conf = SearchbnfConf( + global_config_all_json, + input_dir, + output_dir, + ) + output = searchbnf_conf.generate() + exp_fname = "searchbnf.conf" + expected_content = dedent( + """ + [generatetextcommand-command] + syntax = generatetextcommand count= text= + description = This command generates COUNT occurrences of a TEXT string. + usage = public + """ + ).lstrip() + assert output == [ + { + "file_name": exp_fname, + "file_path": f"{output_dir}/{ta_name}/default/{exp_fname}", + "content": expected_content, + } + ] + + +def test_generate_conf_with_search_command_syntax_autogeneration( + global_config_all_json, + input_dir, + output_dir, + custom_search_command_syntax_autogeneration, +): + global_config_all_json._content["customSearchCommand"] = ( + custom_search_command_syntax_autogeneration + ) + ta_name = global_config_all_json.product + searchbnf_conf = SearchbnfConf( + global_config_all_json, + input_dir, + output_dir, + ) + output = searchbnf_conf.generate() + exp_fname = "searchbnf.conf" + expected_content = dedent( + """ + [generatetextcommand-command] + syntax = generatetextcommand count= test= """ + + """(animals=(cat|dog|wombat))? (last=(d|m|y)?)? (urgency=(high|medium|low))? + description = This command generates COUNT occurrences of a TEXT string. + usage = public + """ + ).lstrip() + assert output == [ + { + "file_name": exp_fname, + "file_path": f"{output_dir}/{ta_name}/default/{exp_fname}", + "content": expected_content, + } + ] + + +def test_generate_conf_with_multiline_description( + global_config_all_json, + input_dir, + output_dir, + custom_search_command_with_multiline_description, +): + global_config_all_json._content["customSearchCommand"] = ( + custom_search_command_with_multiline_description + ) + ta_name = global_config_all_json.product + searchbnf_conf = SearchbnfConf( + global_config_all_json, + input_dir, + output_dir, + ) + output = searchbnf_conf.generate() + exp_fname = "searchbnf.conf" + expected_content = dedent( + """ + [generatetextcommand-command] + syntax = generatetextcommand count= text= + description = This command generates COUNT occurrences of a TEXT string. \\ + This might be additional information. \\ + Here we can mention something like wombats are cool. usage = public """ ).lstrip() diff --git a/tests/unit/generators/python_files/test_create_custom_command_python.py b/tests/unit/generators/python_files/test_create_custom_command_python.py index 400ec2b9c8..054d6ba75d 100644 --- a/tests/unit/generators/python_files/test_create_custom_command_python.py +++ b/tests/unit/generators/python_files/test_create_custom_command_python.py @@ -66,6 +66,90 @@ def transforming_custom_search_command(): ] +@pytest.fixture +def transforming_custom_search_command_with_multiline_description(): + return [ + { + "commandName": "generatetext", + "commandType": "generating", + "fileName": "generatetextcommand.py", + "description": [ + "This is a transforming command", + "This might be additional information.", + "Here we can mention something like wombats are cool.", + ], + "syntax": "generatetext action=", + "arguments": [ + { + "name": "action", + "required": True, + "validate": {"type": "Fieldname"}, + }, + { + "name": "test", + }, + ], + } + ] + + +@pytest.fixture +def custom_search_command_validators(): + return [ + { + "commandName": "testcommand", + "commandType": "generating", + "fileName": "test.py", + "requiredSearchAssistant": True, + "description": "This is test command", + "syntax": "testcommand count= text=", + "usage": "public", + "arguments": [ + { + "name": "count", + "required": True, + "validate": {"type": "Integer", "minimum": 5, "maximum": 10}, + }, + { + "name": "max_word", + "validate": {"type": "Integer", "maximum": 100}, + }, + { + "name": "age", + "validate": {"type": "Integer", "minimum": 18}, + }, + {"name": "text", "required": True, "defaultValue": "test_text"}, + {"name": "contains"}, + {"name": "fieldname", "validate": {"type": "Fieldname"}}, + { + "name": "animals", + "validate": {"type": "Set", "values": ["cat", "dog", "wombat"]}, + }, + { + "name": "name", + "validate": { + "type": "Match", + "name": "Name pattern", + "pattern": "^[A-Z][a-z]+$", + }, + }, + { + "name": "urgency", + "validate": { + "type": "Map", + "map": {"high": 3, "medium": 2.2, "low": "one"}, + }, + }, + { + "name": "volume", + "validate": {"type": "Float", "minimum": 2.2, "maximum": 197.45}, + "required": True, + }, + ], + } + ] + + def test_for_transforming_command_with_error( transforming_custom_search_command, global_config_all_json, @@ -147,6 +231,37 @@ def test_for_transforming_command_without_map( ] +def test_for_search_command_validators( + global_config_all_json, + input_dir, + output_dir, + custom_search_command_validators, +): + global_config_all_json._content["customSearchCommand"] = ( + custom_search_command_validators + ) + custom_command_py = CustomCommandPy( + global_config_all_json, + input_dir, + output_dir, + ) + + assert custom_command_py.commands_info[0]["list_arg"] == [ + "count = Option(name='count', require=True, " + "validate=validators.Integer(minimum=5, maximum=10))", + "max_word = Option(name='max_word', require=False, validate=validators.Integer(maximum=100))", + "age = Option(name='age', require=False, validate=validators.Integer(minimum=18))", + "text = Option(name='text', require=True, default='test_text')", + "contains = Option(name='contains', require=False)", + "fieldname = Option(name='fieldname', require=False, validate=validators.Fieldname())", + "animals = Option(name='animals', require=False, validate=validators.Set('cat', 'dog', 'wombat'))", + "name = Option(name='name', require=False, validate=validators.Match('Name pattern', '^[A-Z][a-z]+$'))", + "urgency = Option(name='urgency', require=False, " + "validate=validators.Map(**{'high': 3, 'medium': 2.2, 'low': 'one'}))", + "volume = Option(name='volume', require=True, validate=validators.Float(minimum=2.2, maximum=197.45))", + ] + + def test_init_without_custom_command( global_config_only_configuration, input_dir, @@ -240,6 +355,9 @@ class GeneratetextcommandCommand(GeneratingCommand): """ count = Option(name='count', require=True, validate=validators.Integer(minimum=5, maximum=10)) text = Option(name='text', require=True) + animals = Option(name='animals', require=False, validate=validators.Set('cat', 'dog', 'wombat')) + name = Option(name='name', require=False, validate=validators.Match('Name pattern', '^[A-Z][a-z]+$')) + urgency = Option(name='urgency', require=False, validate=validators.Map(**{'high': 3, 'medium': 2.2, 'low': 'one'})) def generate(self): return generate(self) @@ -250,3 +368,57 @@ def generate(self): assert normalize_code(output[0]["content"]) == normalize_code(expected_content) assert output[0]["file_name"] == exp_fname assert output[0]["file_path"] == f"{output_dir}/{ta_name}/bin/{exp_fname}" + + +def test_generate_python_with_multiline_description( + global_config_all_json, + input_dir, + output_dir, + transforming_custom_search_command_with_multiline_description, +): + exp_fname = "generatetext.py" + ta_name = global_config_all_json.meta["name"] + + global_config_all_json._content["customSearchCommand"] = ( + transforming_custom_search_command_with_multiline_description + ) + custom_command_py = CustomCommandPy( + global_config_all_json, + input_dir, + output_dir, + ) + output = custom_command_py.generate() + expected_content = ''' +import sys +import import_declare_test + +from splunklib.searchcommands import \\ + dispatch, GeneratingCommand, Configuration, Option, validators +from generatetextcommand import generate + +@Configuration() +class GeneratetextCommand(GeneratingCommand): + """ + + ##Syntax + generatetext action= + + ##Description + This is a transforming command + This might be additional information. + Here we can mention something like wombats are cool. + + """ + action = Option(name='action', require=True, validate=validators.Fieldname()) + test = Option(name='test', require=False) + + + def generate(self): + return generate(self) + +dispatch(GeneratetextCommand, sys.argv, sys.stdin, sys.stdout, __name__) + ''' + assert output is not None + assert normalize_code(output[0]["content"]) == normalize_code(expected_content) + assert output[0]["file_name"] == exp_fname + assert output[0]["file_path"] == f"{output_dir}/{ta_name}/bin/{exp_fname}" diff --git a/tests/unit/test_global_config.py b/tests/unit/test_global_config.py index 64744a82e0..63a3088e8e 100644 --- a/tests/unit/test_global_config.py +++ b/tests/unit/test_global_config.py @@ -76,8 +76,10 @@ def test_global_config_custom_search_commands(global_config_all_json): "commandType": "generating", "requiredSearchAssistant": True, "description": "This command generates COUNT occurrences of a TEXT string.", + "shortdesc": "Command for generating string events.", "syntax": "generatetextcommand count= text=", "usage": "public", + "tags": "text generator", "arguments": [ { "name": "count", @@ -85,6 +87,35 @@ def test_global_config_custom_search_commands(global_config_all_json): "validate": {"type": "Integer", "minimum": 5, "maximum": 10}, }, {"name": "text", "required": True}, + { + "name": "animals", + "validate": {"type": "Set", "values": ["cat", "dog", "wombat"]}, + }, + { + "name": "name", + "validate": { + "type": "Match", + "name": "Name pattern", + "pattern": "^[A-Z][a-z]+$", + }, + }, + { + "name": "urgency", + "validate": { + "type": "Map", + "map": {"high": 3, "medium": 2.2, "low": "one"}, + }, + }, + ], + "examples": [ + { + "search": '| generatetextcommand count=5 text="example string"', + "comment": 'Generates 5 events with text="example string"', + }, + { + "search": '| generatetextcommand count=10 text="another example string"', + "comment": 'Generates 10 events with text="another example string"', + }, ], } ] diff --git a/tests/unit/testdata/valid_config.json b/tests/unit/testdata/valid_config.json index 2857bb8913..1b6399ff5f 100644 --- a/tests/unit/testdata/valid_config.json +++ b/tests/unit/testdata/valid_config.json @@ -1377,8 +1377,10 @@ "commandType": "generating", "requiredSearchAssistant": true, "description": "This command generates COUNT occurrences of a TEXT string.", + "shortdesc": "Command for generating string events.", "syntax": "generatetextcommand count= text=", "usage": "public", + "tags": "text generator", "arguments": [ { "name": "count", @@ -1392,6 +1394,46 @@ { "name": "text", "required": true + }, + { + "name": "animals", + "validate": { + "type": "Set", + "values": [ + "cat", + "dog", + "wombat" + ] + } + }, + { + "name": "name", + "validate": { + "type": "Match", + "name": "Name pattern", + "pattern": "^[A-Z][a-z]+$" + } + }, + { + "name": "urgency", + "validate": { + "type": "Map", + "map": { + "high": 3, + "medium": 2.2, + "low": "one" + } + } + } + ], + "examples": [ + { + "search": "| generatetextcommand count=5 text=\"example string\"", + "comment": "Generates 5 events with text=\"example string\"" + }, + { + "search": "| generatetextcommand count=10 text=\"another example string\"", + "comment": "Generates 10 events with text=\"another example string\"" } ] }