text: // Variables syntax parser, for a string input returns AST-like meta object
  
  'use strict';
  
  const ServerlessError = require('../../serverless-error');
  
  const alphaChars = new Set('abcdefghijklmnopqrstuvwxyz');
  const alphaNumericChars = new Set(
    'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-'
  );
  const isType = RegExp.prototype.test.bind(/^[a-z][a-zA-Z0-9]*$/);
  
  let value;
  let index;
  let char;
  
  let state;
  let stringContext;
  let variables;
  let sources;
  let contexts;
  let currentSource;
  
  let contextStart;
  let contextVariables;
  let escapeVarStart;
  let variableStart;
  let sourceStart;
  let paramStart;
  let addressStart;
  
  const finalizeSource = () => {
    if (!sources) sources = [];
    sources.push(currentSource);
    currentSource = null;
    state = 'maybeNextSourceStart';
  };
  
  const finalizeVariable = () => {
    finalizeSource();
    if (!variables) variables = [];
    variables.push({ start: variableStart - contextStart, end: index + 1 - contextStart, sources });
    sources = null;
    releaseContext();
  };
  
  const registerVarEscape = () => {
    if (!variables) variables = [];
    variables.push({
      start: escapeVarStart - contextStart,
      end: index - contextStart,
      value: '\\'.repeat(Math.ceil((index - 1 - escapeVarStart - 1) / 2)) + value[index - 1],
    });
    escapeVarStart = null;
  };
  
  const registerSlashEscape = () => {
    if (!variables) variables = [];
    variables.push({
      start: escapeVarStart - contextStart,
      end: index - 1 - contextStart,
      value: '\\'.repeat(Math.ceil((index - 1 - escapeVarStart - 1) / 2)),
    });
    escapeVarStart = null;
  };
  
  const registerContext = (newContextStart = contextStart) => {
    if (!contexts) contexts = [];
    if (!variables) variables = [];
    if (newContextStart && !contextVariables) contextVariables = [];
    contexts.push({
      state,
      variableStart,
      contextStart,
      variables,
      contextVariables,
      currentSource,
      sources,
    });
  
    state = 'maybeVariableOpen';
    variableStart = index;
    sources = null;
    if (newContextStart) {
      contextStart = newContextStart;
      variables = contextVariables;
    }
  };
  
  const releaseContext = () => {
    const context = contexts && contexts.pop();
    if (!context) {
      state = 'out';
      return;
    }
    if (!contexts.length) contexts = null;
    ({ state, variableStart, variables, contextVariables, currentSource, sources } = context);
    if (state === 'param') paramStart = contextStart;
    if (state === 'address') addressStart = contextStart;
    ({ contextStart } = context);
  };
  
  const normalizeMetaVariables = (metaValue, metaVariables) => {
    switch (metaVariables.length) {
      case 0:
        return null;
      case 1: {
        const [variable] = metaVariables;
        if (!variable.start && variable.end === metaValue.length) {
          delete variable.start;
          delete variable.end;
        }
      }
      // fallthrough
      default:
        return metaVariables;
    }
  };
  
  const generateMeta = (metaValue, metaVariables) => {
    const meta = { value: metaValue || null };
    if (!metaVariables || !metaVariables.length) return meta;
    meta.variables = normalizeMetaVariables(metaValue, metaVariables);
    return meta;
  };
  
  const parseString = (start) => {
    if (value[start] === "'") return value.slice(start, index).trim().slice(1, -1);
    try {
      return JSON.parse(value.slice(start, index));
    } catch (error) {
      throw new ServerlessError(
        `Invalid string literal at index ${start} in "${value}": ${error.message}`,
        'INVALID_VARIABLE_STRING_LITERAL'
      );
    }
  };
  
  const parseLiteralSource = (literal, shouldFallbackToInput = false) => {
    switch (literal) {
      case 'true':
        return true;
      case 'false':
        return false;
      case 'null':
        return null;
      default: {
        const numberValue = Number(literal);
        if (!isNaN(literal)) return numberValue;
        if (shouldFallbackToInput) return literal;
        throw new ServerlessError(
          `Invalid literal source "${literal}" at index ${sourceStart} in "${value}"`,
          'INVALID_VARIABLE_LITERAL_SOURCE'
        );
      }
    }
  };
  
  module.exports = (inputValue) => {
    // State machine design pattern
    try {
      value = inputValue;
      state = 'out';
      contextStart = 0;
      contextVariables = null;
  
      for (index = 0; (char = value[index]); ++index) {
        switch (state) {
          case 'out':
            if (char === '\\') {
              if (escapeVarStart == null) escapeVarStart = index;
              state = 'escapeVar';
            } else if (char === '$') {
              variableStart = index;
              state = 'maybeVariableOpen';
            } else {
              escapeVarStart = null;
            }
            break;
          case 'escapeVar':
            if (char === '$') {
              state = 'escapeVarDollar';
            } else {
              if (char !== '\\') escapeVarStart = null;
              state = 'out';
            }
            break;
          case 'escapeVarDollar':
            if (char === '{') {
              registerVarEscape();
              state = 'out';
            } else {
              escapeVarStart = null;
              if (char === '\\') {
                escapeVarStart = index;
                state = 'escapeVar';
              } else if (char === '$') {
                variableStart = index;
                state = 'maybeVariableOpen';
              } else {
                state = 'out';
              }
            }
            break;
          case 'maybeVariableOpen':
            if (char === '{') {
              if (escapeVarStart != null) registerSlashEscape();
              state = 'maybeTypeStart';
            } else {
              escapeVarStart = null;
              if (char !== '$') releaseContext();
            }
            break;
          case 'maybeTypeStart':
            if (alphaChars.has(char)) {
              sourceStart = index;
              state = 'maybeType';
            } else if (contexts && contexts.some((context) => context.state !== 'foreign')) {
              throw new ServerlessError(
                `Invalid variable type at index ${index} in "${value}"`,
                'INVALID_VARIABLE_TYPE'
              );
            } else if (char === '}') {
              releaseContext();
            } else {
              state = 'foreign';
              if (char === '$') registerContext();
            }
            break;
          case 'maybeNextSourceStart':
            if (char === ' ') continue;
            sourceStart = index;
            if (alphaNumericChars.has(char)) {
              state = 'typeOrSourceLiteral';
            } else if (char === '"') {
              stringContext = 'afterSourceLiteralString';
              state = 'doubleQuotedString';
            } else if (char === "'") {
              stringContext = 'afterSourceLiteralString';
              state = 'singleQuotedString';
            } else {
              throw new ServerlessError(
                `Invalid variable source at index ${index} in "${value}"`,
                'INVALID_VARIABLE_SOURCE'
              );
            }
            break;
          case 'maybeType':
            if (char === ':') {
              currentSource = { type: value.slice(sourceStart, index) };
              state = 'maybeAddressStart';
            } else if (char === '(') {
              currentSource = { type: value.slice(sourceStart, index), params: [] };
              state = 'paramStart';
            } else if (!alphaNumericChars.has(char)) {
              if (contexts && contexts.some((context) => context.state !== 'foreign')) {
                throw new ServerlessError(
                  `Invalid variable type at index ${sourceStart} in "${value}"`,
                  'INVALID_VARIABLE_TYPE'
                );
              }
              if (char === '}') {
                releaseContext();
              } else {
                state = 'foreign';
                if (char === '$') registerContext();
              }
            }
            break;
          case 'typeOrSourceLiteral':
            if (char === ' ') continue;
            if (char === ':') {
              const type = value.slice(sourceStart, index);
              if (!isType(type)) {
                throw new ServerlessError(
                  `Invalid variable source at index ${sourceStart} in "${value}"`,
                  'INVALID_VARIABLE_SOURCE'
                );
              }
              currentSource = { type };
              state = 'maybeAddressStart';
            } else if (char === '(') {
              const type = value.slice(sourceStart, index);
              if (!isType(type)) {
                throw new ServerlessError(
                  `Invalid variable source at index ${sourceStart} in "${value}"`,
                  'INVALID_VARIABLE_SOURCE'
                );
              }
              currentSource = { type, params: [] };
              state = 'paramStart';
            } else if (char === '}') {
              currentSource = { value: parseLiteralSource(value.slice(sourceStart, index).trim()) };
              finalizeVariable();
            } else if (!alphaNumericChars.has(char)) {
              throw new ServerlessError(
                `Invalid variable source at index ${sourceStart} in "${value}"`,
                'INVALID_VARIABLE_SOURCE'
              );
            }
            break;
          case 'paramStart':
            if (char === ' ') continue;
            if (char === ')') {
              state = 'maybeAddressOpen';
            } else if (char === ',') {
              currentSource.params.push({ value: null });
            } else if (char === '}') {
              throw new ServerlessError(
                `Invalid variable param at index ${index} in "${value}"`,
                'INVALID_VARIABLE_PARAM'
              );
            } else {
              paramStart = index;
              if (char === '"') {
                stringContext = 'afterParamString';
                state = 'doubleQuotedString';
              } else if (char === "'") {
                stringContext = 'afterParamString';
                state = 'singleQuotedString';
              } else {
                state = 'param';
                contextVariables = null;
                if (char === '$') registerContext(paramStart);
              }
            }
            break;
          case 'param':
            if (char === ')') {
              let paramValue = value.slice(paramStart, index).trim();
              if (paramValue) {
                if (!contextVariables) paramValue = parseLiteralSource(paramValue, true);
                currentSource.params.push(generateMeta(paramValue, contextVariables));
              }
              state = 'maybeAddressOpen';
            } else if (char === ',') {
              let paramValue = value.slice(paramStart, index).trim();
              if (!contextVariables) paramValue = parseLiteralSource(paramValue, true);
              currentSource.params.push(generateMeta(paramValue, contextVariables));
              state = 'paramStart';
            } else if (char === '}') {
              throw new ServerlessError(
                `Invalid variable param at index ${paramStart} in "${value}"`,
                'INVALID_VARIABLE_PARAM'
              );
            } else if (char === '$') {
              registerContext(paramStart);
            }
            break;
          case 'afterParamString':
            if (char === ' ') continue;
            if (char === ')') {
              currentSource.params.push({ value: parseString(paramStart) });
              state = 'maybeAddressOpen';
            } else if (char === ',') {
              currentSource.params.push({ value: parseString(paramStart) });
              state = 'paramStart';
            } else {
              throw new ServerlessError(
                `Invalid variable param at index ${paramStart} in "${value}"`,
                'INVALID_VARIABLE_PARAM'
              );
            }
            break;
          case 'maybeAddressOpen':
            if (char === ' ') continue;
            if (char === ':') {
              state = 'maybeAddressStart';
            } else if (char === '}') {
              finalizeVariable();
            } else if (char === ',') {
              finalizeSource();
            } else {
              throw new ServerlessError(
                `Invalid variable address at index ${index} in "${value}"`,
                'INVALID_VARIABLE_ADDRESS'
              );
            }
            break;
          case 'maybeAddressStart':
            if (char === ' ') continue;
            if (char === '}') {
              finalizeVariable();
            } else if (char === ',') {
              finalizeSource();
            } else {
              addressStart = index;
              if (char === ':') {
                if (
                  !currentSource.params &&
                  (!contexts || contexts.every((context) => context.state === 'foreign'))
                ) {
                  currentSource = null;
                  state = 'foreign';
                } else {
                  throw new ServerlessError(
                    `Invalid variable address at index ${addressStart} in "${value}"`,
                    'INVALID_VARIABLE_ADDRESS'
                  );
                }
              } else if (char === '"') {
                stringContext = 'afterAddressString';
                state = 'doubleQuotedString';
              } else if (char === "'") {
                stringContext = 'afterAddressString';
                state = 'singleQuotedString';
              } else {
                state = 'address';
                contextVariables = null;
                if (char === '$') registerContext(addressStart);
              }
            }
            break;
          case 'address':
            if (char === '}') {
              currentSource.address = generateMeta(
                value.slice(addressStart, index).trim(),
                contextVariables
              );
              finalizeVariable();
            } else if (char === '$') {
              registerContext(addressStart);
            } else if (char === ',') {
              currentSource.address = generateMeta(
                value.slice(addressStart, index).trim(),
                contextVariables
              );
              finalizeSource();
            }
            break;
          case 'afterAddressString':
            if (char === ' ') continue;
            if (char === '}') {
              currentSource.address = { value: parseString(addressStart) };
              finalizeVariable();
            } else if (char === ',') {
              currentSource.address = { value: parseString(addressStart) };
              finalizeSource();
            } else {
              throw new ServerlessError(
                `Invalid variable address at index ${addressStart} in "${value}"`,
                'INVALID_VARIABLE_ADDRESS'
              );
            }
            break;
          case 'afterSourceLiteralString':
            if (char === ' ') continue;
            if (char === '}') {
              currentSource = { value: parseString(sourceStart) };
              finalizeVariable();
            } else {
              throw new ServerlessError(
                `Invalid variable source at index ${sourceStart} in "${value}"`,
                'INVALID_VARIABLE_SOURCE'
              );
            }
            break;
          case 'foreign':
            if (char === '}') {
              releaseContext();
            } else if (char === '$') {
              registerContext();
            }
            break;
          case 'escapeString':
            state = 'doubleQuotedString';
            break;
          case 'doubleQuotedString':
            if (char === '"') {
              state = stringContext;
            } else if (char === '\\') {
              state = 'escapeString';
            }
            break;
          case 'singleQuotedString':
            if (char === "'") state = stringContext;
            break;
          /* istanbul ignore next */
          default:
            throw new Error(`Unexpected state ${state}`);
        }
      }
  
      if (currentSource || sources) {
        throw new ServerlessError(
          `Missing variable closing bracket in ${value}`,
          'UNTERMINATED_VARIABLE'
        );
      }
  
      if (!variables) return null;
      return normalizeMetaVariables(value, variables);
    } finally {
      escapeVarStart = null;
      variables = null;
      sources = null;
      contexts = null;
      currentSource = null;
    }
  };
  