function parseCommand()

in src/parse.ts [113:259]


function parseCommand(source: string, options: ParseOptions) {
  source = source
    // removes comments tags, such as `<1>`
    .replace(/<([\S\s])>/g, "")
    // removes comments, such as `// optional`
    .replace(/\s*\/\/\s.+/g, "")
    // trimp whitespace
    .trim();

  const data: ParsedRequest = {
    source: source,
    service: "es",
    params: {},
    method: "",
    url: "",
    path: "",
    rawPath: "",
  };

  const len = source.length;
  let index = 0;
  // identify the method
  for (const method of httpMethods) {
    if (source.slice(index, len).startsWith(method)) {
      data.method = method;
      index += method.length;
      break;
    }
  }
  /* istanbul ignore if */
  if (!data.method) {
    if (options?.ignoreErrors) {
      return data;
    }
    throw new Error("Invalid request method");
  }

  // identify the url and query
  skip(" ", "\n");
  const urlStart = index;
  until("{", "\n");
  if (source[index] == "{" && source[index + 1].match(/[a-z]/i)) {
    // this is a placeholder element inside the URL (as used in doc examples),
    // so we continue scanning
    index++;
    until("{", "\n");
  }

  data.url = source.slice(urlStart, index).trim();
  if (data.url.indexOf(":/") >= 0) {
    [data.service, data.url] = data.url.split(":/", 2);
    data.url = "/" + data.url;
  }
  if (data.url[0] != "/") {
    data.url = "/" + data.url;
  }
  data.url = data.url
    // replaces { with %7B (braces in many doc examples are not URIencoded)
    .replace(/{/g, "%7B")
    // replaces } with %7D
    .replace(/}/g, "%7D");
  const parsedUrl = new URL(`http://localhost${data.url}`);
  data.rawPath =
    parsedUrl.pathname != "/"
      ? parsedUrl.pathname.replace(/\/$/, "")
      : parsedUrl.pathname;
  data.path = decodeURIComponent(data.rawPath);

  if (parsedUrl.search.length) {
    const parsedQuery = new URLSearchParams(parsedUrl.search.slice(1));
    data.query = {};
    for (const [key, value] of parsedQuery) {
      data.query[key] = value || "true";
    }
  }

  // TODO: this should be an issue in the docs,
  // the correct url is `<index/_mapping`
  if (data.path.endsWith("_mappings")) {
    data.path = data.path.slice(0, -1);
    data.rawPath = data.rawPath.slice(0, -1);
    data.url = data.url.replace(data.path + "s", data.path);
  }

  // identify the body
  const body = removeTrailingCommas(
    collapseLiteralStrings(source.slice(index)),
  );

  if (body != "") {
    try {
      // json body
      data.body = JSON.parse(body) as JSONObject;
    } catch (err) {
      try {
        // ndjson body
        const ndbody = body.split("\n").filter(Boolean) as string[];
        data.body = ndbody.map((b) => JSON.parse(b));
      } catch (err) {
        if (options?.ignoreErrors) {
          data.body = body;
        } else {
          throw new Error("body cannot be parsed");
        }
      }
    }
  }

  return data;

  // some commands have three double quotes `"""` in the body
  // this utility removes them and makes the string a valid json
  function collapseLiteralStrings(data: string) {
    const splitData = data.split('"""');
    for (let idx = 1; idx < splitData.length - 1; idx += 2) {
      splitData[idx] = JSON.stringify(splitData[idx]);
    }
    return splitData.join("");
  }

  // remove any trailing commas to prevent JSON parser failures
  function removeTrailingCommas(data: string) {
    return data.replace(/,([ |\t|\n]+[}|\]|)])/g, "$1");
  }

  // proceeds until it finds a character not present
  // in the list passed as input
  function skip(...args: string[]) {
    if (index == len) return;
    if (!args.includes(source[index])) {
      return;
    }
    index += 1;
    skip(...args);
  }

  // proceeds until it finds a character present
  // in the list passed as input
  function until(...args: string[]) {
    if (index == len) return;
    if (args.includes(source[index])) {
      return;
    }
    index += 1;
    until(...args);
  }
}