VScript - Our Internal DSL for Client Scripts

VScript logic is represented as JSON statement objects within an array. A statement object represents a single statement and may have sub statements A statement object begins with a single directive to identify it. There can be multiple keys in the root of a statement object, but the first identfies it unrecognised directives are ignored and case does not matter. Items are executed in the order they appear in the array with the exception of the BEFORE and AFTER statements which act as constructors and destructors.

The psuedo-language used by VScript is designed from the ground up to be extensible. The base language is very minimal and most functionality is implemented in our "standard" library. Script execution happens in five phases:

  1. BEFORE directive (if found) is executed.
  2. Main script is executed up until an INPUT directive is located.
  3. The Display Buffer is filled with in scope INPUT directives and execution is paused until the user performs an action.
  4. The action or actions the user chose is executed.
  5. AFTER directive (if found) is executed.

Variables

Variables in VScript can be strings, numbers, booleans or arrays. There are two classes of variable:

  • Internal to Script
  • Event State (Global)

To access or create internal variables you would use the VAR directive.

VAR

The var statement can be used to access the value or set the value of a variable. When setting a value it does not have to be a constant, you can also assign the return value of a function. Variables are created on first access, and are null before a value is assigned. Global variables are prefixed with '!'.

Example:

    //To get a variable's value:
    {"VAR": "product"} //would be null right now

    //To set a variable's value:
    {"VAR": "product", "IS": 23}
    //

    //A global variable:
    {"VAR": "!CALL_ATTEMPTS"}

LOOKUP

The LOOKUP statement allows you to retrieve values from the global event.

Example:

    {"LOOKUP": "user.name"} //returns the full name of the authorized user

Please note that any variables that refer to a product will only return valid values during the account loop.

List of Variables

See the Document titled Variables

Control Flow

BEFORE

With a BEFORE block you can run some code before a question is displayed Additionaly the special variable FROM is available with the previous question id

Example:

{
    "BEFORE": [
        ... //any action(s)
    ]
}

AFTER

With an AFTER you can run some code when a question is "exited" by any means. The special variables FROM and DEST are available with the current question id and the destination.

Example:

{
    "AFTER": [
        ... //any action(s)
    ]
}

IF..THEN..ELSE

IF takes an array with 3 values. Both the first and third arguments to the IF statement can be an array and they will be treated as boolean so you can create complex tests. If the array evaluates to true then the THEN statement is executed, otherwise the ELSE statement (if present) is executed. Both the THEN and ELSE statements can be a single statement (object) or a list of statements (array).

Example:

{
    "IF": [{"VAR":"counter"}, "EQ", {"CONSTANT":"1"}]
}

// nesting conditions
{
    "IF": [[{"VAR":"counter"},{"CONSTANT":"1"}],"AND",[{"VAR":"item"},{"CONSTANT":"Some Text"}]]
}

Here is a whiteboard more throroughly explaining the rule of three:

Whiteboard showing Rule of Three Diagram

More Complex Example

ELSE-IF

Similar to a switch you can also add alternative conditions to an IF statement. For this we use the ELSE-IF sub-directive inside an IF directive, which hold an array of tests and what to run if successful.

Example of ELSE-IF
{
    "IF": [{"VAR":"counter"},"EQ", {"CONSTANT":"1"}],
    "THEN": [
        ...
    ],
    "ELSE-IF: [{
        "CONDITION": [{"VAR":"counter"},"EQ", {"CONSTANT":"2"}],
        "THEN": [
            ...
        ]
    },{
        "CONDITION": [{"VAR":"counter"},"EQ", {"CONSTANT":"3"}],
        "THEN": [
            ...
        ]
    }],
    "ELSE": [
        ...
    ]
} 

The ELSE block will only be executed if the main IF and all ELSE-IF statements are not true.

Operators available for IF and ELSE-IF

  • GT -- The left value is Greater Than the right value
  • GTE -- The left value is Greater Than or Equal to the right value
  • LTE -- the left value is Less Than or Equal to the right value
  • LT -- the left value is Less Than the right value
  • EQ -- the left value is EQual to the right value
  • NE -- the left value is Not Equal to the right value
  • AND -- both inputs are true
  • OR -- one or both inputs are true
  • XOR -- only one input is true
  • NOR -- both inputs are false
  • NAND -- both inputs are not true
  • IN -- the left value is contained in the array in the right value
  • NOT_IN -- the left valie is not contained in the array in the right value

Operators can be nested to achieve a greater level of specificity.

##### Operator Nesting Example javascript { "IF": [ [{"LOOKUP":"value"}, "GT", {"CONSTANT":3}], "AND", [{"LOOKUP":"value"}, "LTE", {"CONSTANT":7}] ] }

This will evaluate to true if the {LOOKUP:value} is a number from 4 to 7.

WHILE Loop

The while loop allows a group of statements to be repeated

Example:

{
    "WHILE": "truth statement like IF",
    "DO": [
        "statements to repeat"
    ]
}

SWITCH

Similar to a IF statement, the SWITCH statement performs an action or actions based on the value of a variable. You can specify an action to run if none of the provided choices match the variable's value by adding a default section to the SWITCH directive. Adding a default is optional.

Example:

[{
    "SWITCH": {
        "variable": "product",
        "default": [
            {
                "move-to": "1.3.1"
            }
        ],
        "choices": [
            {
                "value": 1,
                "action": {
                    "move-to": "1.4.1"
                }
            },{
                "value": 2,
                "actions": [
                    {
                        "VAR": "some_variable",
                        "IS": "some_value"
                    },
                    {
                        "move-to": "1.5.1"
                    }
                ]
            }
        ]
    }
}]

Reusable Code

Defining Functions

The DEFFUN directive allows the user to create reusable functions.

Example:

{
    "DEFFUN": "new-function-name",
    "parameters": [{"name":"first_param","type":"text"}, {"name":"second_param","type":"integer", "default": 0}],
    "returns": "type or null for void",
    "body": [
        ... //statements here, to return a value assign the special variable RETURN a value
    ]
}

Calling Functions

The FUNC directive is used to call a predefined function, it takes the name of the function to call and optionally an array of parameters.

Example:

{
    "FUNC": "empty", 
    "parameters": [""]
}

Defining External APIs

The DEFAPI directive defines an external api. Support for formats other than JSON is limited at this point. JSON return values are collected using JSONPath to select the required fields. JSONPath is similar in nature to XPath and the documentation can be found here: http://goessner.net/articles/JsonPath/

Example:

    {
        "DEFAPI": "my-api-name",
        "base": "https://your.service.url/api/v1/",
        "global_headers": {
            "X-Requested-By": "DataExchange",
            "X-Vendor": {"var": "VENDOR_NAME"}
        },
        "endpoints": [
            {
                "name": "method-name",
                "url": "/validate-rate",
                "method": "POST",
                "format": "json",
                "parameters": ["SELECTED_RATE","SERVICE_ADDRESS_1","SERVICE_ADDRESS_2","SERVICE_CITY","SERVICE_ZIP","SERVICE_STATE"],
                "data": [
                    {
                        "type": "object",
                        "name": "data",
                        "value": [{
                            "type": "value",
                            "name": "rate",
                            "value": "SELECTED_RATE"
                        },{
                            "type": "array",
                            "name": "service_addr",
                            "value": ["SERVICE_ADDRESS_1","SERVICE_ADDRESS_2","SERVICE_CITY","SERVICE_ZIP","SERVICE_STATE"]
                        }]
                    }
                ],
                "return": {
                    "format": "json", //can be json, xml, raw
                    "error_path": "$.error", //depends on format, jsonpath, xmlpath or a search string i.e. ERROR
                    "return_value": {
                        "variable": "RATE_IS_VALID",
                        "path": "$.rate.is_valid"
                    }
                }
            }
        ]
    }

Calling an API

The CALL statement is used call a previously defined API.

{ //using the API defined above
    "CALL": {
        "api": "my-api-name",
        "method": "my-api-method",
        "parameters": [...] //list of parameters
    }
}
//This would sent a post request of a json document like:
{
    "data": {
        "rate": "The rate name or code",
        "service_addr": ["1234 Main St", "Apt 2", "Englewood", "07631", "NJ"]
    }
}

//And expect a response like:

{
    "error": "null if no error or a message",
    "rate": {
        "is_valid": true
    }
}

Input Directives

When an input directive is encountered in a scope the program will finish processing the current scope and then display any requested input directives. These are provided by the view layer and may not all be supported. Remember that unsupported directives are not an error, they are simply ignored.

BUTTON

The button directive is used to add simple buttons to a question. It takes the text of the button, an action statement(s) and an optional type. Types map to Bootstrap style, positive to "btn-success" and negative to "btn-danger".

Custom EzTPV Support

Not Supported at this time.

Simple yes/no example

[
    {
        "BUTTON": {
            "text": "Yes",
            "action": {
                "move-to": "1.2.2"
            },
            "type": "positive"
        }
    },
    {
        "BUTTON": {
            "text": "No",
            "action": {
                "move-to": "END_CALL",
                "disposition": "DID_NOT_AGREE"
            },
            "type": "negative"
        }
    }
]

INPUT

Request input from the user and perform actions based on it. Supported types are: text, digits, number, custom (regex in validate key). Supports min/max limits, string length for text/digits/custom, value for number.

Custom EzTPV Support

Supported. In this mode only the "label" and "variable" parameters are supported. In addition, the "label" parameter is slightly changed to support mutliple languages, see Custom EzTPV Example below.

Example:

{
    "INPUT": {
        "type": "text",
        "label": "placeholder text",
        "validate": "custom put the regex here",
        "limits": {
            "min": 1,
            "max": 10
        },
        "messages": {
            "length": "Must be between 1 and 10 letters long",
            "invalid": "Please enter a valid account number"
        },
        "variable": "where to store result / get initial value",
        "action": {
            ... //action or actions to run once variable is filled
        }
    }
}

Custom EzTPV Example:

{
    "INPUT": {
        "label": {
            "english": "placeholder text",
            "spanish": "placeholder text in spanish"
        },
        "variable": "where to store result / get initial value",
    }
}

IDENTIFICATION

Present an editor for the tpv agent to enter/verify identification(s) provided by the customer

Custom EzTPV Support

Not supported at this time.

Example:

{
    "IDENTIFICATION": {
        "action": {
            ... // action or actions to run once id(s) are valid, saved and continue clicked
        }
    }
}

CHOOSE

Request input from the user in the form of a dropdown or list select. The optional multiple key controls whether the interface is shown as a dropdown or list.

Custom EzTPV Support

Supported. In this mode only the "options" and "variable" parameters are supported

Example:

{
    "CHOOSE": {
        "multiple": false,
        "options": [
            {
                "text": "Option 1",
                "value": 1
            },
            {
                "text": "Option 2",
                "value": 2
            }
        ],
        "variable": "where to store / get initial",
        "action": {
            //action
        }
    }
}

DIALOG

The dialog directive is used to display a dialog to the end user. it takes an optional title, the dialog text and an array of buttons. Buttons consist of a text field and an action field, the action may either be a statement or an array of statements. The dialog is closed after button press regardless.

Custom EzTPV Support

Not Supported at this time.

Example:

{
    "DIALOG": {
        "text": "Unable to locate record id {RECORD_ID}",
        "buttons": [
            {
                "text": "Retry",
                "action": "CLOSE_DIALOG"
            },
            {
                "text": "Skip",
                "action": {
                    "move-to": "1.2.1"
                }
            }
        ]
    }
}

ADDRESS

The address directive is used to display an address to the end user with the ability to edit it. It you do not want the address to be edited you must use one of the address variables available for the script and not this direcive. Address directive consists of a title which should be the simplest description for this address, i.e. Service or Billing and the LOOKUP variable to get and store the address to and the action to perform once the address is saved or verified.

Custom EzTPV Support

Supported, only the "variable" parameter is supported in this environment.

Example

{
    "ADDRESS": {
        "title": "Billing",
        "variable": "account.bill_address",
        "action": [
            {
                "move-to": "1.2.1"
            }
        ]
    }
}

Array Directives

Array:Create

Creates an empty array.

{
    "VAR": "!myArray", 
    "IS": {"ARRAY:CREATE": null}
}

Array:Length

Returns the number of elements in the passed array

{
    "ARRAY:LENGTH": {"VAR":"!myArray}
}

Array:Push

Pushes a value onto the end of the specified array.

{
    "ARRAY:PUSH": "Test",
    "ARRAY": {"VAR": "!myArray}
}

Array:Pop

Returns and removes the last item from the array.

{
    "ARRAY:POP": {"VAR": "!myArray"}
}

Array:At

Allows reading and writing arrays at a specified index.

// write
{
    "ARRAY:AT": {"VAR": "!myArray"},
    "INDEX": 0, // array indexing starts at 0 for the first element
    "IS": "Test"
}
//read
{
    "ARRAY:AT": {"VAR": "!myArray"},
    "INDEX": 0, // array indexing starts at 0 for the first element
}

Array:Concat

Combines the passed arrays into a single array and returns it.

{
    "ARRAY:CONCAT": [[1, 2, 3], [4, 5]]
} // returns [1,2,3,4,5]

Array:Unique

Returns a copy of the passed array with duplicate items removed.

{
    "ARRAY:UNIQUE": [1,2, 2, 3, 4, 5, 5]
} // returns [1,2,3,4,5]

Array:Slice

Returns a portion of an array.

{
    "ARRAY:SLICE": [1, 2, 3, 4, 5],
    "START": 1,
    "END": 3
} // returns [2,3]

{
    "ARRAY:SLICE": [1, 2, 3, 4, 5],
    // The start index parameter is optional
    "END": 3
} // returns [1,2,3]

Array:Contains

Returns true if the specified array contains the specified item.

{
    "ARRAY:CONTAINS": 3,
    "ARRAY": [1,2,3]
} // true

{
    "ARRAY:CONTAINS": 4,
    "ARRAY": [1,2,3]
} // false

Array:Join

Joins the elements of the array with the specified glue string.

{
    "ARRAY:JOIN": ["Hello", "World"],
    "WITH": ", "
} // returns "Hello, World"

Array:Map

{
    "ARRAY:MAP": [1, 2, 3],
    "DO": [
        {"*": [{"VAR": "!!value"}, 2]}
    ]
} // returns [2, 4, 6]

Array:Reduce

{
    "ARRAY:REDUCE": [1, 2, 3],
    "DO": [
        {"+": [{"VAR":"!!accumulator"}, {"VAR":"!!currentValue"}]}
    ]
} // returns 6

Array:Reverse

Returns a copy of the array that is reversed.

{
    "ARRAY:REVERSE": [1,2,3]
} // returns [3,2,1]

Date Directives

NOW

This directive returns the current Date and Time.

{
    "NOW": null
}

DATE-PARSE

This directive validates the passed date/time and returns it. If the date is not valid this directive will return false.

{
    "DATE-PARSE": "2020-13-01"
}
// returns false because there is no 13th month 

DAY-OF-WEEK

This directive returns a numerical value representing what day of the week the passed date is from 1 to 7 with Monday = 1 and Sunday = 7.

{
    "DAY-OF-WEEK": "2020-02-14"
}
// returns 5 because Valentine's day 2020 was on a Friday

DATE-ADD

This directive allows manipulating the passed date.

{
    "DATE-ADD": "2020-01-01",
    "UNIT": "days",
    "COUNT": 9
}
// would return 2020-01-10
// accepted values for UNIT are: years, months, weeks, business-days, days, hours, minutes, seconds

Note: the business-days option only confirms the resulting date is not on a weekend.

DATE-SUB

This directive allows manipulating the passed date.

{
    "DATE-SUB": "2020-01-10",
    "UNIT": "days",
    "COUNT": 9
}
// would return 2020-01-01
// accepted values for UNIT are: years, months, weeks, business-days, days, hours, minutes, seconds

Note: the business-days option only confirms the resulting date is not on a weekend.

DATE-IS-HOLIDAY

This directive will return true if the specified date is a holiday on the provided list.

Valid lists currently are: federal

The LIST option is optional and defaults to federal

{
    "DATE-IS-HOLIDAY": "2020-02-14",
    "LIST": "federal"
}

DATE-FORMAT

This directive transforms the passed date into the format specified. Format string is specified in the table here.

{
    "DATE-FORMAT": "2020-05-20",
    "AS": "MM-DD-YYYY"
}

DATE-DIFF

This directive returns the difference between two dates in days.

{
    "DATE-DIFF": {
        "FROM": "2020-01-01",
        "TO": "2020-01-10"
    }
}
// returns -9 because the FROM date is 9 days before the TO date

DATE-IS-SAME

This directive compares two dates and returns true if they are the same day.

{
    "DATE-IS-SAME": {
        "A": "2020-02-01",
        "B": "2020-02-01"
    }
} // true
{
    "DATE-IS-SAME": {
        "A": "2020-02-01",
        "B": "2020-02-02"
    }
} // false

DATE-IS-BEFORE

This directive compares two dates and returns true if A comes before B.

{
    "DATE-IS-BEFORE": {
        "A": "2020-02-01",
        "B": "2020-02-02"
    }
} // true
{
    "DATE-IS-BEFORE": {
        "A": "2020-02-11",
        "B": "2020-02-02"
    }
} // false

DATE-IS-AFTER

This directive compares two dates and returns true if A comes after B

{
    "DATE-IS-AFTER": {
        "A": "2020-02-01",
        "B": "2020-02-02"
    }
} // false
{
    "DATE-IS-AFTER": {
        "A": "2020-02-10",
        "B": "2020-02-02"
    }
} // true

DATE-IS-SAME-OR-BEFORE

This directive compares two dates and returns true if A is the same day as B or if A comes before B

{
    "DATE-IS-SAME-OR-BEFORE": {
        "A": "2020-02-01",
        "B": "2020-02-02"
    }
} //true
{
    "DATE-IS-SAME-OR-BEFORE": {
        "A": "2020-02-01",
        "B": "2020-02-01"
    }
} // true
{
    "DATE-IS-SAME-OR-BEFORE": {
        "A": "2020-02-30",
        "B": "2020-02-01
    }
} // false

DATE-IS-SAME-OR-AFTER

This directive compares two dates and returns true if A is the same day as B or A comes after B

{
    "DATE-IS-SAME-OR-AFTER": {
        "A": "2020-02-01",
        "B": "2020-02-01"
    }
} // true
{
    "DATE-IS-SAME-OR-AFTER": {
        "A": "2020-02-02",
        "B": "2020-02-01"
    }
} // true
{
    "DATE-IS-SAME-OR-AFTER": {
        "A": "2020-02-01",
        "B": "2020-02-02"
    }
} //false

Other Directives

IS-EMPTY

Reduces it's parameter and returns true if it is an empty type/value, i.e. null, undefined, [], {}, '', 0

NOT-EMPTY

Reduces it's parameter and returns false if it is an empty type/value, i.e. null, undefined, [], {}, '', 0

MOVE-TO

The move-to directive's value is the num-dot-num-dot-num question id to go to.

DO-CREDIT-CHECK

This directive initiates the brand specific actions necessary to perform a credit check and then runs its "THEN" directives.

{
    "DO-CREDIT-CHECK": null,
    "THEN": [
        ...
    ]
}

START-WARM-TRANSFER

This directive converts the current call into a conference and attempts to add a third party to that conference.

{
    "START-WARM-TRANSFER": "number to call"
}

CALL-END

The call-end directive's value is the id of a preset disposition or null to allow choosing an option. This occurs without prompting the user.

CALL-RETRY

This directive is intended to be used during disposition resolution. The value of the directive is ignored and will cause the callback to retry.

CALL-HANGUP

This directive takes no parameter (pass null) and simply causes the phone system to disconnect the call after prompting the user.

CALL-INITIATE

This directive's value should be a phone number in E.164 format. The phone system will disconnect any active call and then dial the passed number.

DISPOSITION

The disposition directive is currently the same as the CALL-END directive with the exception that is prompts the user before disconnecting the call and the disposition parameter is the disposition id NOT the name.

REGEX-TEST

The regex-test directive checks whether the target string matches the provided regular expression. It returns a Boolean value. The FLAGS option is optional and can be left out entirely, recognized values are:

  • g - global match; find all matches rather than stopping after the first match
  • i - ignore case
  • m - multiline; treat beginning and end characters (^ and $) as working over multiple lines (i.e., match the beginning or end of each line (delimited by \n or \r), not only the very beginning or end of the whole input string)
  • u - unicode; treat pattern as a sequence of unicode code points
  • y - sticky; matches only from the index of the last match of this regular expression in the target string (and does not attempt to match from any later indexes).

Example:

{
    "REGEX-TEST": "regular expression",
    "FLAGS": "i",
    "TARGET": {"var": "product"}
}

Math

All these math functions take an array as a parameter and will work with any number of parameters. Given the array [1, 2, 3, 4], you can expect the following to occur:

(((1 op 2) op 3) op 4)

All values or statements must resolve to a number or you will get NaN as a result.

Example:

    {"+": array} // add the array items together
    {"-": array} // subtract the array items from the others
    {"*": array} // multiply the array items in order
    {"/": array} // divide the array items in order
    {"%": array} // return the modulus of the array in order
    {"^": array} // Exponent operator, raise the first array item by the second.

These unary operators take the name of a variable as the only parameter.

//Increment Value by 1
{"inc": varname}

//Decrement Value by 1
{"dec": varname}

String Directives

String Coerce

All parameters will be fully resolved and converted to strings then they will be joined by a space. The array [1,2,3] would return the string "1 2 3". The seperator param is optional and defaults to space, leading and trailing whitespace is removed after coercion.

Example:

    {"$": array, sep: ' '}

Trim

Removes extra whitespace from the ends of the specified string

Example:

    {"string:trim": "test string "}
    // will return "test string"

To Uppercase

Converts all characters in a string to their uppercase equivalents.

Example:

    {"string:uppercase": "test"}
    // will return "TEST"

To Lowercase

Converts all characters in a string to their lowercase equivalents.

Example:

    {"string:lowercase": "Test"}
    // will return "test"

Get String Length

Counts the number of characters in a string

Example:

    {"string:length": "test"}
    // will return 4

Starts With

Tests if a string begins with another string

Example:

    {"string:starts": "test string", "with": "test"}
    // will return true
    {"string:starts": "test string", "with": "jest"}
    // will return false

Ends With

Tests if a string ends with another string

Example:

    {"string:ends": "test string", "with": "string"}
    // will return true
    {"string:ends": "test string", "with": "strung"}
    // will return false

Replace

Search for and replace text in a string

Example:

    {"string:replace": "test", "in": "test string", "with": "jest"}
    // will return "jest string"

Includes Text

Tests if a string includes some text anywhere in the string

Example:

    {"string:includes": "test", "in":"this is a test string"}
    // will return true

Template

Build a more complex string from a template and parameters

Example:

    {"string:template": "{0} your test {1}", "with": ["Billy", "passed"]}
    // will return "Billy your test passed"

Custom Field Validation Functions

[ // This simple example simply checks if the input is empty and shows how to return if it is valid or not
    {
        "IF": [
            {
                "IS-EMPTY": {
                    "VAR": "!input"
                }
            },
            "eq",
            true
        ],
        "THEN": [
            {
                "VAR": "!valid",
                "IS": false
            }
        ],
        "ELSE": [
            {
                "VAR": "!valid",
                "IS": true
            }
        ]
    }
]

Example: Disposition Resolution to Call Back Customer

Disposition resolutions should return true to end the interaction.

  • !CALL_ATTEMPTS is a special variable indicating the number of outbound call attempts in the current session.
  • !READY_FOR_CX is a special variable indicating if the agent portion of the call is complete.
[
   {
      "IF":[
         [{ "VAR":"!CALL_ATTEMPTS" }, "LT", 4],
         'AND',
         { "VAR": "!READY_FOR_CX" }
      ],
      "THEN":[
         {
            "CALL-HANGUP":true
         },
         {
            "CALL-RETRY":true
         },
         {
            "VAR":"RETURN",
            "IS":false
         }
      ],
      "ELSE":[
         {
            "VAR":"RETURN",
            "IS":true
         }
      ]
   }
]

Advanced Example: Fibonacci Number Finder

This function takes a single parameter which is the digit number to return. It is not fast, expect large values to take some time or potentially overflow the execution stack.

[
   {
      "deffun":"fib",
      "p":[
         {
            "name":"n"
         }
      ],
      "r":"number",
      "b":[
         {
            "if":[
               {
                  "var":"n"
               },
               "LTE",
               2
            ],
            "then":{
               "var":"RETURN",
               "is":1
            },
            "else":{
               "var":"RETURN",
               "is":{
                  "function":"add",
                  "p":[
                     {
                        "function":"fib",
                        "p":[
                           {
                              "function":"sub",
                              "p":[
                                 {
                                    "var":"n"
                                 },
                                 1
                              ]
                           }
                        ]
                     },
                     {
                        "function":"fib",
                        "p":[
                           {
                              "function":"sub",
                              "p":[
                                 {
                                    "var":"n"
                                 },
                                 2
                              ]
                           }
                        ]
                     }
                  ]
               }
            }
         }
      ]
   }
]