lopidio/jsonPlaceholderReplacer

View on GitHub
src/library.ts

Summary

Maintainability
A
55 mins
Test Coverage
const defaultDelimiterTags = [
  {
    begin: "{{",
    end: "}}",
  },
  {
    begin: "<<",
    end: ">>",
  },
];

const defaultSeparator = ":";

export interface DelimiterTag {
  begin: string;
  end: string;
  escapedBeginning?: string;
  escapedEnding?: string;
}

export interface Configuration {
  delimiterTags: DelimiterTag[];
  defaultValueSeparator: string;
}

export class JsonPlaceholderReplacer {
  private readonly variablesMap: Array<Record<string, unknown>> = [];
  private readonly configuration: Configuration;
  private readonly delimiterTagsRegex: RegExp;

  public constructor(options?: Partial<Configuration>) {
    this.configuration = this.initializeOptions(options);
    const escapeRegExp = (text: string): string =>
      text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
    this.configuration.delimiterTags = this.configuration.delimiterTags.map(
      (tag) => ({
        ...tag,
        escapedBeginning: escapeRegExp(tag.begin),
        escapedEnding: escapeRegExp(tag.end),
      }),
    );
    const delimiterTagsRegexes = this.configuration.delimiterTags
      .map(
        (delimiterTag) =>
          `^${delimiterTag.begin}[^${delimiterTag.end}]+${delimiterTag.end}$`,
      )
      .join("|");
    this.delimiterTagsRegex = new RegExp(delimiterTagsRegexes);
  }

  public addVariableMap(
    variableMap: Record<string, unknown> | string,
  ): JsonPlaceholderReplacer {
    if (typeof variableMap === "string") {
      this.variablesMap.push(JSON.parse(variableMap));
    } else {
      this.variablesMap.push(variableMap);
    }
    return this;
  }

  public replace(json: object): object {
    return this.replaceChildren(json);
  }

  private initializeOptions(options?: Partial<Configuration>): Configuration {
    let delimiterTags = defaultDelimiterTags;
    let defaultValueSeparator = defaultSeparator;
    if (options !== undefined) {
      if (
        options.delimiterTags !== undefined &&
        options.delimiterTags.length > 0
      ) {
        delimiterTags = options.delimiterTags;
      }
      if (options.defaultValueSeparator !== undefined) {
        defaultValueSeparator = options.defaultValueSeparator;
      }
    }
    return { defaultValueSeparator, delimiterTags };
  }

  private replaceChildren(node: any): any {
    for (const key in node) {
      const attribute = node[key];
      if (typeof attribute === "object") {
        node[key] = this.replaceChildren(attribute);
      } else if (attribute !== undefined) {
        node[key] = this.replaceValue(attribute.toString());
      }
    }
    return node;
  }

  private replaceValue(node: string): string {
    const placeHolderIsInsideStringContext =
      !this.delimiterTagsRegex.test(node);
    const output = this.configuration.delimiterTags.reduce(
      (acc, delimiterTag) => {
        const regex = new RegExp(
          `(${delimiterTag.escapedBeginning}[^${delimiterTag.escapedEnding}]+${delimiterTag.escapedEnding})`,
          "g",
        );
        return acc.replace(
          regex,
          this.replacer(placeHolderIsInsideStringContext)(delimiterTag),
        );
      },
      node,
    );
    try {
      return JSON.parse(output);
    } catch (exc) {
      return output;
    }
  }

  private replacer(placeHolderIsInsideStringContext: boolean) {
    return (delimiterTag: DelimiterTag) =>
      (placeHolder: string): string => {
        const { tag, defaultValue } = this.parseTag(placeHolder, delimiterTag);

        const mapCheckResult = this.checkInEveryMap(tag);
        if (mapCheckResult === undefined) {
          if (defaultValue !== undefined) {
            return defaultValue;
          }
          return placeHolder;
        }
        if (!placeHolderIsInsideStringContext) {
          return mapCheckResult;
        }
        const parsed: any = JSON.parse(mapCheckResult);
        if (typeof parsed === "object") {
          return JSON.stringify(parsed);
        }
        return parsed;
      };
  }

  private parseTag(
    placeHolder: string,
    delimiterTag: DelimiterTag,
  ): { tag: string; defaultValue: string | undefined } {
    const path: string = placeHolder.substring(
      delimiterTag.begin.length,
      placeHolder.length - delimiterTag.begin.length,
    );
    let tag = path;
    let defaultValue: string | undefined;
    const defaultValueSeparatorIndex = path.indexOf(
      this.configuration.defaultValueSeparator,
    );
    if (defaultValueSeparatorIndex > 0) {
      tag = path.substring(0, defaultValueSeparatorIndex);
      defaultValue = path.substring(
        defaultValueSeparatorIndex +
          this.configuration.defaultValueSeparator.length,
      );
    }
    return { tag, defaultValue };
  }

  private checkInEveryMap(path: string): string | undefined {
    let result;
    this.variablesMap.forEach(
      (map) => (result = this.navigateThroughMap(map, path)),
    );
    return result;
  }

  private navigateThroughMap(map: any, path: string): string | undefined {
    if (map === undefined) {
      return;
    }
    const shortCircuit = map[path];
    if (shortCircuit !== undefined) {
      return JSON.stringify(shortCircuit);
    }
    const keys = path.split(".");
    const key: string = keys[0];
    keys.shift();
    return this.navigateThroughMap(map[key], keys.join("."));
  }
}