FormatTemplate

Build text from a template that can include DT expressions.

python · Filters And Transformers

Source

from collections.abc import Callable
from typing import Any

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401


class ContextData:
    def __init__(
        self,
        context: dict[str, Any] | None = None,
        inputs: dict[str, Any] | None = None,
        incident: dict[str, Any] | None = None,
        value: dict[str, Any] | None = None,
    ):
        self.__context = context
        self.__value = value
        self.__specials = {
            "inputs": inputs if isinstance(inputs, dict) else {},
            "incident": incident if isinstance(incident, dict) else {},
        }

    def get(self, key: str | None = None) -> Any:
        """Get the context value

        :param key: The dt expressions (string within ${}).
        :return: The value.
        """
        if not key:
            return None

        dx = self.__context
        if key != "." and not key.startswith(".=") and key.startswith("."):
            dx = self.__value
            key = key[1:]
        else:
            for prefix in self.__specials:
                if prefix == key or (key.startswith(prefix) and key[len(prefix) : len(prefix) + 1] in (".", "(", "=")):
                    dx = self.__specials
                    break
        return demisto.dt(dx, key)


class Formatter:
    def __init__(self, start_marker: str, end_marker: str, keep_symbol_to_null: bool):
        if not start_marker:
            raise ValueError("start-marker is required.")

        self.__start_marker = start_marker
        self.__end_marker = end_marker
        self.__keep_symbol_to_null = keep_symbol_to_null

    @staticmethod
    def __is_end_mark(source: str, ci: int, end_marker: str) -> bool:
        if end_marker:
            return source[ci : ci + len(end_marker)] == end_marker
        else:
            c = source[ci]
            if c.isspace():
                return True
            elif c.isascii():
                return c != "_" and not c.isalnum()
            else:
                return False

    def __extract(
        self,
        source: str,
        extractor: Callable[[str, ContextData | None], Any] | None,
        dx: ContextData | None,
        si: int,
        markers: tuple[str, str] | None,
    ) -> tuple[Any, int | None]:
        """Extract a template text, or an enclosed value within starting and ending marks

        :param source: The template text, or the enclosed value starts with the next charactor of a start marker
        :param extractor: The function to extract an enclosed value as DT
        :param dx: The context data
        :param si: The index of `source` to start extracting
        :param markers: The start and end marker to find an end position for parsing an enclosed value.
                        It must be None when the template text is given to `source`.
        :return: The extracted value and index of `source` when parsing ended.
                 The index is the next after the end marker when extracting the enclosed value.
        """
        out = None
        ci = si
        while ci < len(source):
            if markers is not None and Formatter.__is_end_mark(source, ci, markers[1]):
                key = source[si:ci] if out is None else str(out) + source[si:ci]
                if extractor:
                    if (xval := extractor(key, dx)) is None and self.__keep_symbol_to_null:
                        xval = markers[0] + key + markers[1]
                else:
                    xval = key
                return xval, ci + len(markers[1])
            elif extractor and source[ci : ci + len(self.__start_marker)] == self.__start_marker:
                xval, ei = self.__extract(
                    source, extractor, dx, ci + len(self.__start_marker), (self.__start_marker, self.__end_marker)
                )
                if si != ci:
                    out = source[si:ci] if out is None else str(out) + source[si:ci]

                if ei is None:
                    xval = self.__start_marker
                    ei = ci + len(self.__start_marker)

                if out is None:
                    out = xval
                elif xval is not None:
                    out = str(out) + str(xval)
                si = ci = ei
            elif markers is None:
                ci += 1
            elif endc := {"(": ")", "{": "}", "[": "]", '"': '"', "'": "'"}.get(source[ci]):
                _, ei = self.__extract(source, None, dx, ci + 1, (source[ci], endc))
                ci = ci + 1 if ei is None else ei
            elif source[ci] == "\\":
                ci += 2
            else:
                ci += 1

        if markers is not None:
            # unbalanced braces, brackets, quotes, etc.
            return None, None
        elif not extractor:
            return None, ci
        elif si >= len(source):
            return out, ci
        elif out is None:
            return source[si:], ci
        else:
            return str(out) + source[si:], ci

    def build(self, template: Any, extractor: Callable[[str, ContextData | None], Any] | None, dx: ContextData | None) -> Any:
        """Format a text from a template including DT expressions

        :param template: The template.
        :param extractor: The extractor to get real value within ${dt}.
        :param dx: The context instance.
        :return: The text built from the template.
        """
        if isinstance(template, dict):
            return {self.build(k, extractor, dx): self.build(v, extractor, dx) for k, v in template.items()}
        elif isinstance(template, list):
            return [self.build(v, extractor, dx) for v in template]
        elif isinstance(template, str):
            return self.__extract(template, extractor, dx, 0, None)[0] if template else ""
        else:
            return template


def extract_dt(dtstr: str, dx: ContextData | None) -> Any:
    """Extract dt expression

    :param dtstr: The dt expressions (string within ${}).
    :param dx: The context instance.
    :return: The value extracted.
    """
    try:
        return dx.get(dtstr) if dx else dtstr
    except Exception:
        return None


def main():
    args = assign_params(**demisto.args())
    try:
        value = args.get("value")
        template = args.get("template")
        template_type = args.get("template_type", "raw")
        variable_markers = argToList(args.get("variable_markers", "${,}"))
        if not variable_markers or not variable_markers[0]:
            raise ValueError("variable_markers must have a start marker.")
        elif len(variable_markers) >= 3:
            raise ValueError("too many values for variable_markers.")
        elif len(variable_markers) == 1:
            variable_markers = variable_markers + [""]

        dx = args.get("ctx_data")
        if dx and isinstance(dx, str):
            dx = json.loads(dx)

        if not template:
            template = value
            value = None
        elif template_type == "json":
            template = json.loads(template)
        elif template_type != "raw":
            raise DemistoException(f"Invalid template type: {template_type}")

        dx = ContextData(context=dx, inputs=args.get("ctx_inputs"), incident=args.get("ctx_inc"), value=value)

        formatter = Formatter(variable_markers[0], variable_markers[1], argToBoolean(args.get("keep_symbol_to_null", False)))
        output = formatter.build(template, extract_dt, dx)
        output = [] if output is None else output
    except Exception as err:
        # Don't return an error by return_error() as this is transformer.
        raise DemistoException(str(err))

    return_results(output)


if __name__ in ("__builtin__", "builtins", "__main__"):
    main()

README

Build text from a template that can include DT expressions.

Script Data


Name Description
Script Type python3
Tags transformer, general
Cortex XSOAR Version 6.5.0

Inputs


Argument Name Description
value The context to refer a value with ${.xxx} if `template` is not empty, otherwise the template text.
template The template text
template_type The template type
ctx_data Context Data: Input . (single dot) on `From previous tasks` to enable to extract the context data.
ctx_inputs `inputs` context: Input ‘inputs’ (no quotation) on `From previous tasks` to enable ${inputs.} expression in DT.
ctx_inc `demisto` context: Input ‘incident’ (no quotation) on `From previous tasks` to enable ${incident.} expression in DT.
variable_markers The pair of start and end markers to bracket a variable name.
keep_symbol_to_null Set to true not to replace a value if the variable is null, otherwise false.

Outputs


There are no outputs for this script.

Getting Started


The transformer builds a text from a template text which includes variables just like:
This is a test message for ${user_name}.

The template will be formatted to This is a test message for John Doe. by replacing the variable parameters.

By default, a variable name starts with ${ and ends with } . You can change the start marker and end marker by specifying the variable_markers parameter.

Examples


Replace variables in a text based on the context data

Parameters

Argument Name Value Note
value   Any value
template My name is ${first_name} ${last_name}.  
template_type    
ctx_data . Make sure that From previous tasks is selected
ctx_inputs    
ctx_inc    
variable_markers    
keep_symbol_to_null    

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

My name is John Doe.

Replace variables in a text based on the value

Parameters

Argument Name Value Note
value See the value  
template My name is ${.first_name} ${.last_name}.  
template_type    
ctx_data    
ctx_inputs    
ctx_inc    
variable_markers    
keep_symbol_to_null    

value (structured)

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

My name is John Doe.

Format the template given to the value

Parameters

Argument Name Value Note
value My name is ${first_name} ${last_name}.  
template    
template_type    
ctx_data . Make sure that From previous tasks is selected
ctx_inputs    
ctx_inc    
variable_markers    
keep_symbol_to_null    

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

My name is John Doe.

Change the variable start and end marker to the windows command shell style such as %name%

Parameters

Argument Name Value Note
value   Any value
template My name is %first_name% %last_name%.  
template_type    
ctx_data . Make sure that From previous tasks is selected
ctx_inputs    
ctx_inc    
variable_markers %,%  
keep_symbol_to_null    

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

My name is John Doe.

Change the variable start and end marker to the UNIX shell style such as $name

Parameters

Argument Name Value Note
value   Any value
template My name is $first_name $last_name.  
template_type    
ctx_data . Make sure that From previous tasks is selected
ctx_inputs    
ctx_inc    
variable_markers $  
keep_symbol_to_null    

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

My name is John Doe.

Keep variable names if they are missing in the context

Parameters

Argument Name Value Note
value   Any value
template My name is ${first_name} ${last_name}.  
template_type    
ctx_data . Make sure that From previous tasks is selected
ctx_inputs    
ctx_inc    
variable_markers    
keep_symbol_to_null true  

Context Data

{
  "first_name": "John"
}

Output

My name is John ${last_name}

Use DTs to build variables

Parameters

Argument Name Value Note
value   Any value
template My name is ${first_name=val.toUpperCase()} ${last_name=val.toUpperCase()}.  
template_type    
ctx_data . Make sure that From previous tasks is selected
ctx_inputs    
ctx_inc    
variable_markers    
keep_symbol_to_null    

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

My name is JOHN DOE.

Convert all the values in a structured data

Parameters

Argument Name Value Note
value   Any value
template See the template  
template_type    
ctx_data . Make sure that From previous tasks is selected
ctx_inputs    
ctx_inc    
variable_markers    
keep_symbol_to_null    

template (structured)

{
  "1": "First name is ${first_name}",
  "2": "Last name is ${last_name}",
  "3": [
    "First name is ${first_name}",
    "Last name is ${last_name}"
  ]
}

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

{
  "1": "First name is John",
  "2": "Last name is Doe",
  "3": [
    "First name is John",
    "Last name is Doe"
  ]
}

Convert all the values in a JSON text

Parameters

Argument Name Value Note
value   Any value
template See the template  
template_type json  
ctx_data . Make sure that From previous tasks is selected
ctx_inputs    
ctx_inc    
variable_markers    
keep_symbol_to_null    

template

{
  "1": "First name is ${first_name}",
  "2": "Last name is ${last_name}",
  "3": [
    "First name is ${first_name}",
    "Last name is ${last_name}"
  ]
}

Context Data

{
  "first_name": "John",
  "last_name": "Doe"
}

Output

{
  "1": "First name is John",
  "2": "Last name is Doe",
  "3": [
    "First name is John",
    "Last name is Doe"
  ]
}