All files / src/app/common/object-shape-comparer object-shape-comparer.ts

100% Statements 66/66
93.75% Branches 30/32
100% Functions 12/12
100% Lines 59/59
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 1051x     1x   1x 23x     73x 50x 50x   87x 1x     86x     86x 15x 15x       71x 71x 1x       70x 70x 70x 70x 4x       70x   50x     1x 81x 81x 81x 9x   81x 27x   81x     9x 9x 9x 4x       11x 5x 14x 14x 14x 14x 3x 3x   11x   5x     1x 59x   59x         1x 226x     1x 5x 5x     14x 14x 31x 14x   14x     1x  
import { Injectable } from '@angular/core';
 
@Injectable()
export class ObjectShapeComparer {
 
  compare(expected, actual): string[] {
    return this.compareObjectShape(expected, actual);
  }
 
  private compareObjectShape(expected, actual, path = ''): string[] {
    let diffs = [];
    for (const key in expected) {
      // Ignore function properties
      if (!expected.hasOwnProperty(key) || typeof expected[key] === 'function') {
        continue;
      }
 
      const fullPath = path + (path === '' ? '' : '.') + key;
 
      // Property exists?
      if (!actual.hasOwnProperty(key)) {
        diffs.push(`Missing property ${fullPath}`);
        continue; // no need to check further when actual is missing
      }
 
      // Template value = undefined, means no type checking, no nested objects
      const expectedValue = expected[key];
      if (expectedValue === undefined) {
        continue;
      }
 
      // Types match?
      const expectedType = this.getType(expectedValue);
      const actualValue = actual[key];
      const actualType = this.getType(actualValue);
      if (expectedType !== actualType) {
        diffs.push(`Types differ for property ${fullPath} (${expectedType} vs ${actualType})`);
      }
 
      // Recurse nested objects and arrays
      diffs = diffs.concat(this.recurse(expectedValue, actualValue, fullPath));
    }
    return diffs;
  }
 
  private recurse(expectedValue, actualValue, path): string[] {
    let diffs = [];
    const expectedType = this.getType(expectedValue);
    if (expectedType === 'array') {
      diffs = diffs.concat(this.compareArrays(expectedValue, actualValue, path));
    }
    if (expectedType === 'object') {
      diffs = diffs.concat(this.compareObjectShape(expectedValue, actualValue, path));
    }
    return diffs;
  }
 
  private compareArrays(expectedArray, actualArray, path): string[] {
    let diffs = [];
    if (expectedArray.length === 0 || this.arrayIsPrimitive(expectedArray)) {
      return diffs;
    }
 
    // Look for expected element anywhere in the actuals array
    const actualKeys = actualArray.map(element => this.getKeys(element).join(','));
    for (let index = 0; index < expectedArray.length; index++) {
      const fullPath = path + '.' + index;
      const expectedElement = expectedArray[index];
      const actualElement = this.actualMatchingExpected(expectedElement, actualArray);
      if (!actualElement) {
        diffs.push(`Missing array element ${fullPath} (keys: ${this.getKeys(expectedElement).join(',')})`);
        continue;
      }
      diffs = diffs.concat(this.recurse(expectedElement, actualElement, fullPath));
    };
    return diffs;
  }
 
  private getKeys(obj): any[] {
    return typeof obj === 'object' ?
      Object.keys(obj)
        .filter(key => obj.hasOwnProperty(key)) // ignore function properties
        .sort()
      : [];
  }
 
  private getType(el: any): string {
    return Array.isArray(el) ? 'array' : typeof el;
  }
 
  private arrayIsPrimitive(array): boolean {
    const arrayType = this.getType(array[0]);
    return arrayType !== 'object' && arrayType !== 'array';
  }
 
  private actualMatchingExpected(expected, actuals): any {
    const expectedKeys = this.getKeys(expected).join(',');
    const actualKeys = actuals.map(element => this.getKeys(element).join(','));
    const match = actualKeys.indexOf(expectedKeys);
    // tslint:disable-next-line:no-bitwise
    return (~match) ? actuals[match] : null;
  }
 
}