AkashBabu/supervised-emitter

View on GitHub
src/pattern.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { InvalidPatternError } from './errors';

const SEPERATOR = '/';

/**
 * Predicate that checks if the given
 * part is a wildcard part or not
 *
 * @param part Part of the event to check
 *
 * @returns {Boolean}
 */
function isWildcard(part: string) {
  return part === '*' || part === '**';
}

/**
 * Returns the parts of the event.
 * i.e, it essentially splits the event string
 * based on a seperator '/' and removes
 * the empty parts
 *
 * @param event Event
 *
 * @returns list of event parts
 */
const getEventParts = (event: string): string[] => sanitizeEvent(event, true) as string[];

/**
 * Predicate that checks if any part of the
 * event is a wildcard or not
 *
 * @param event Event to check for wildcard
 *
 * @returns true if the given event contains
 *    wildcards
 */
export function isPatternEvent(event: string): boolean {
  return !!getEventParts(event).find(isWildcard);
}

/**
 * Checks if the given event is valid
 *
 * @param event event to check
 *
 * @returns {Boolean}
 */
export function isValidEvent(event: string) {
  const eventParts = getEventParts(event);

  // let prevPart = eventParts[0];
  let prevPart = '';

  return !eventParts.some((eventPart) => {
    if (isWildcard(prevPart) && isWildcard(eventPart)) {
      // This essentially checks for */** or **/* or **/**
      if ((eventPart + prevPart).length > 2) return true;
    }

    prevPart = eventPart;
  });
}

/**
 * Closure that returns the next patterPart in the array
 *
 */
function arrayIterator(arr: any[]) {
  let index = -1;

  return (i?: number): [any, number] => {
    if (i) index = i + 1;
    else index++;

    return [arr[index], index];
  };
}

/**
 * Checks if the pubEvent matches the pattern event
 *
 * @param pubEvent Event to check for match
 * @param subEvent pattern subEvent
 *
 * @returns true if it matches
 *
 * @throws Invalid Pattern if it violates wildcard
 *     policies
 */
export function doesPatternMatch(pubEvent: string, subEvent: string): boolean {
  if (!isValidEvent(subEvent)) throw new InvalidPatternError();

  // Publish event parts
  const pubParts = getEventParts(pubEvent);
  const getNextPubPart = arrayIterator(pubParts);

  // Subscribe event parts
  const subParts = getEventParts(subEvent);
  const getNextSubPart = arrayIterator(subParts);

  /**
   * Checks if the parts matches post a '**'
   * if not then, checks for match from
   * the next eventPart index
   *
   * @param eventPart Event part in the form of
   *       [eventPart, eventIndex]
   * @param nextPatternIndex Next Pattern index
   *
   * @returns true if parts match after '**'
   */
  function checkMulti([eventPart, eventIndex]: [string, number], nextPatternIndex: number): boolean {
    if (!eventPart) return false;

    const isMatch = match([eventPart, eventIndex], getNextSubPart(nextPatternIndex - 1));

    return !isMatch ? checkMulti(getNextPubPart(eventIndex), nextPatternIndex) : true;
  }

  function match(
    [eventPart, eventIndex]: [string, number] = getNextPubPart(),
    [patternPart]: [string, number] = getNextSubPart(),
  ): boolean { // tslint:disable cyclomatic-complexity
    // if eventPart is present but not patternPart
    // or vice-versa then it's not a match
    if ((!eventPart && patternPart && patternPart !== '**') || (!patternPart && eventPart)) return false;

    // if both parts are not present then
    // it's a match
    if (!eventPart && !patternPart) return true;

    // if it's not a pattern (*, **) then perform
    // a direct string match
    if (!isWildcard(patternPart)) {
      return patternPart === eventPart ? match() : false;
    }

    // Since * would match any string, goto next match;
    if (patternPart === '*') return match();

    // Else patternPart == '**'
    const [nextPatternPart, nextPatternIndex] = getNextSubPart();
    return nextPatternPart ? checkMulti([eventPart, eventIndex], nextPatternIndex) : true;
  }

  return match();
}

/**
 * Sanitizes the given event by removing all
 * empty parts. If `getParts` is true, then it
 * returns all the non-empty parts as a list else
 * string
 *
 * @param event Event
 * @param getParts If true, then returns non-empty parts list
 *
 * @returns sanitized-event
 */
export function sanitizeEvent(event: string, getParts?: boolean): string | string[] {
  const sanitizedEvent: string[] = [];
  let part = '';

  for (const char of event) {
    if (char === SEPERATOR) {
      if (part) {
        sanitizedEvent.push(part);
        part = '';
      }
    } else {
      part += char;
    }
  }

  // Check if there are any remaining characters
  // in `part`, if so then add them up
  if (part) {
    sanitizedEvent.push(part);
  }

  return getParts ? sanitizedEvent : sanitizedEvent.join(SEPERATOR);
}