# nextemplate

Template parser library used for dynamic values interpolation in [Bright DAST app](https://app.brightsec.com).

## Template syntax

The template syntax in `nextemplate` utilizes double curly braces `{{` and `}}` as delimiters for interpolation expressions.

Two types of interpolation expressions are supported within these curly braces: **variable** expressions and **yield** expressions (prefixed with `$`).

### Variable expressions

> ⚠️ Variable expressions must be prefixed with a context name. Currently, the only supported contexts are `auth_object` and `entrypoint`. Additional supported contexts may be added in the future.

> ⚠️ Each context currently has a limited set of supported references. Under the `auth_object` context, you can currently use stage references and OTP token references. Under the `entrypoint` context, currently only parameter references are supported.

#### Stage references

Example: `{{ auth_object.stages.custom_stage_name1.response.body }}`

A stage reference consists of the following components:

- context (`auth_object`)
- special word (`stages`)
- stage name:
  - any word (\w), number (\d) or underscore characters
  - or special `any` literal
- source (`request` or `response`)
- location in the source (`url`, `headers`, or `body`).

For data transformation, a pipe operator (`|`) is available, supporting parameters and chaining.

Example: `{{ auth_object.stages.custom_stage_name1.response.headers | get: '/Set-Cookie' | match: /sid=(.+)/ : 1 }}`

#### OTP token references

Example: `{{ auth_object.otps.custom_otp_token_name1 }}`

OTP token references do not support nested references.

The pipes are supported, e.g.: `{{ auth_object.otps.custom_otp_token_name1 | match: /^.{1}(.)/ : 1 }}`

#### Entrypoint parameters references

Example: `{{ entrypoint.params.some_param_name }}`

A parameter name should start with a letter and can be followed by letters, digits, and underscores.

Entrypoint parameters references do not support nested references or pipes.

### Yield expressions

> ⚠️ Yield expressions other than $faker could be supported in the future, but currently parser will produce an error.

#### $faker references

Example: `{{ $faker.datatype.uuid }}`

Inspired by [@faker-js/faker](https://www.npmjs.com/package/@faker-js/faker).

The first part is predefined `$faker` literal, the second part - faker dataset name (`datatype` is single supported atm),
the third - function name from given dataset (ony `uuid` and `number` are supported).

### Supported pipes

#### get

Returns the value associated with the XPath, or undefined if there is none.

_Format_: `{{ stage_reference | get : xpath }}`

_Parameters_:

- `xpath` - xpath string

_Example_: `{{ auth_object.stages.custom_stage_name1.response.headers | get: '/Set-Cookie' }}`

#### match

Retrieves the result of matching a string against a regular expression.

_Format_: `{{ stage_reference | match : regexp : group }}`

_Parameters_:

- `regexp` - regular expression,
- `group` - number of the capture group (optional, default 1)

_Example_: `{{ auth_object.stages.custom_stage_name1.response.body | match: /sid=(.+)/ : 1 }}`

#### encode

Encodes the value to some format.

_Format_: `{{ stage_reference | encode : format }}`

_Parameters_:

- `format ` - `base64`, `url`, `html` or `none` (optional, default `none`)

_Example_: `{{ auth_object.stages.custom_stage_name1.response.body | encode: 'base64' }}`

#### decode

Decodes the value from some format.

_Format_: `{{ stage_reference | decode : format }}`

_Parameters_:

- `format ` - `base64`, `url`, `html` or `none` (optional, default `none`)

_Example_: `{{ auth_object.stages.custom_stage_name1.response.body | decode: 'base64' }}`

## Install 🚀

`npm i --save @neuralegion/nextemplate`

## API

Main parser function:

`parse(template: string): ParseResult`

Auxiliary `faker`-like generator that consumes generation template (like `faker.datatype.uuid`) as string or `source` from `ParsedFakerExpression` from parser output:

`fakerFn(fakerTemplate: string | { source: string } & any): string`

where

```ts
type ParseResult = (string | ParsedOtpTokenExpression | ParsedStageVariableExpression | ParsedFakerExpression)[];

interface ParsedExpression {
  type: 'variable' | 'yield';
  source: string;
  raw: string;
}

interface ParsedVariableExpression extends ParsedExpression {
  context: 'auth_object' | 'entrypoint';
  type: 'variable';
}

interface ParsedOtpTokenExpression extends ParsedVariableExpression {
  context: 'auth_object';
}

interface ParsedStageVariableExpression extends ParsedVariableExpression {
  context: 'auth_object';
  pipes: ParsedPipe[];
}

interface ParsedEntrypointParamsVariableExpression extends ParsedVariableExpression {
  context: 'entrypoint';
  source: 'params';
  name: string;
}

interface ParsedPipe {
  name: 'get' | 'match' | 'encode' | 'decode;
  args: (string | number)[];
}

interface ParsedYieldExpression extends ParsedExpression {
  type: 'yield';
  value: string;
}

interface ParsedFakerExpression extends ParsedYieldExpression {
}
```

## Sample output

#### Input template string

`prefix {{ auth_object.stages.custom_stage_name1.response.headers | get : '/Set-Cookie' | match : /sid=(.+)/ | encode : 'base64' }} {{ $faker.datatype.uuid }} {{ auth_object.otps.custom_otp_token_name1 }} {{ entrypoint.params.some_param_name }} suffix`

#### Parser output

```json
[
  "prefix ",
  {
    "context": "auth_object",
    "type": "variable",
    "source": "stages.custom_stage_name1.response.headers",
    "pipes": [
      {
        "name": "get",
        "args": ["/Set-Cookie"]
      },
      {
        "name": "match",
        "args": ["/sid=(.+)/", 1]
      },
      {
        "name": "encode",
        "args": ["base64"]
      }
    ],
    "raw": "{{ auth_object.stages.custom_stage_name1.response.headers | get : '/Set-Cookie' | match : /sid=(.+)/ | encode : 'base64' }}"
  },
  " ",
  {
    "type": "yield",
    "source": "faker.datatype.uuid",
    "value": "c6c27519-acb4-4875-91d9-298b921b5104",
    "raw": "{{ $faker.datatype.uuid }}"
  },
  " ",
  {
    "context": "auth_object",
    "type": "variable",
    "source": "otps.custom_otp_token_name1",
    "pipes": [],
    "raw": "{{ auth_object.otps.custom_otp_token_name1 }}"
  },
  " ",
  {
    "context": "entrypoint",
    "type": "variable",
    "source": "params",
    "name": "some_param_name",
    "raw": "{{ entrypoint.params.some_param_name }}"
  },
  " suffix"
]
```

#### fakerFn usage examples

```
> fakerFn('faker.datatype.uuid')
48b0504d-b146-40f7-8fc2-fe19b7b9dc7b

> fakerFn({ source: 'faker.datatype.uuid' })`
b81b54de-735f-401d-aa77-ebd69d4293c2
```

## Usage

<details>
<summary>ECMAScript 2015, Typescript modules</summary>

```
import { parse } from '@neuralegion/nextemplate';

console.log(parse('some_template'));
```

</details>

<details>
<summary>NodeJS (CommonJS module)</summary>

```
const parser = require('@neuralegion/nextemplate');

console.log(parser.parse('some_template'));
```

</details>

<details>

<summary>NodeJS (experimental ESM support)</summary>

`usage.mjs` file:

```
import parser from '@neuralegion/nextemplate';

console.log(parser.parse('some_template'));
```

Running: `node --experimental-modules ./usage.mjs`

</details>

<details>
<summary>Browser (globals from umd bundle)</summary>

```
<script src="./node_modules/@neuralegion/nextemplate/dist/bundle.umd.js"></script>
<script>
  alert(nextemplate.parse('some_template'));
</script>
```

</details>

<details>
<summary>Browser (ES modules)</summary>

```
<script type="module">
  import { parse } from './node_modules/@neuralegion/nextemplate/dist/bundle.es.js';
  alert(parse('some_template'));
</script>
```

</details>

## Development 🛠

Issues and pull requests are highly welcome. 👍

Please, don't forget to lint (`npm run lint`) and test (`npm t`) the code.

## License

Copyright © 2023 [Bright Security](https://brightsec.com).

This project is licensed under the MIT License - see the [LICENSE file](LICENSE) for details.
