All files / src/app/core/models/category category.mapper.ts

100% Statements 50/50
95.23% Branches 20/21
100% Functions 13/13
100% Lines 50/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 15130x   30x   30x 30x     30x     30x 91x             78x 76x 122x 122x   2x               75x 2x       73x   73x     118x     45x 45x     45x               45x               79x 1x       78x   78x   4x   78x   32x   78x   2x   78x   1x     78x             74x 73x 73x   73x                         1x                 71x     70x 12x 34x 22x   58x       70x 70x     70x     70x     70x   1x        
import { Injectable } from '@angular/core';
 
import { CategoryTreeHelper } from 'ish-core/models/category-tree/category-tree.helper';
import { CategoryTree } from 'ish-core/models/category-tree/category-tree.model';
import { ImageMapper } from 'ish-core/models/image/image.mapper';
import { SeoAttributesMapper } from 'ish-core/models/seo-attributes/seo-attributes.mapper';
 
import { CategoryData, CategoryPathElement } from './category.interface';
import { Category, CategoryHelper } from './category.model';
 
@Injectable({ providedIn: 'root' })
export class CategoryMapper {
  constructor(private imageMapper: ImageMapper) {}
 
  /**
   * Utility Method:
   * Maps the incoming raw category path to a path with unique IDs.
   */
  mapCategoryPath(path: CategoryPathElement[]) {
    if (path?.length) {
      return path
        .map(x => x.id)
        .reduce((acc, _, idx, arr) => [...acc, arr.slice(0, idx + 1).join(CategoryHelper.uniqueIdSeparator)], []);
    }
    throw new Error('input is falsy');
  }
 
  /**
   * Utility Method:
   * Creates Category stubs from the category path (excluding the last element)
   */
  categoriesFromCategoryPath(path: CategoryPathElement[]): CategoryTree {
    if (!path?.length) {
      return CategoryTreeHelper.empty();
    }
 
    let uniqueId: string;
    const newCategoryPath: string[] = [];
 
    return (
      path
        // remove the last
        .filter((_, idx, arr) => idx !== arr.length - 1)
        .map(pathElement => {
          // accumulate and construct uniqueId and categoryPath
          uniqueId = !uniqueId ? pathElement.id : uniqueId + CategoryHelper.uniqueIdSeparator + pathElement.id;
          newCategoryPath.push(uniqueId);
 
          // yield category stub
          return {
            uniqueId,
            name: pathElement.name,
            completenessLevel: 0,
            categoryPath: [...newCategoryPath],
          };
        })
        // construct a tree from it
        .reduce((tree, cat: Category) => CategoryTreeHelper.add(tree, cat), CategoryTreeHelper.empty())
    );
  }
 
  /**
   * Compute completeness level of incoming raw data.
   */
  computeCompleteness(categoryData: CategoryData): number {
    if (!categoryData) {
      return -1;
    }
 
    // adjust CategoryCompletenessLevel.Max accordingly
    let count = 0;
 
    if (categoryData.categoryRef) {
      // category path categories do not contain a categoryRef
      count++;
    }
    if (categoryData.categoryPath && categoryData.categoryPath.length === 1) {
      // root categories have no images but a single-entry categoryPath
      count++;
    }
    if (categoryData.images) {
      // images are supplied for sub categories in the category details call
      count++;
    }
    if (categoryData.seoAttributes) {
      // seo attributes are only supplied with the category details call
      count++;
    }
 
    return count;
  }
 
  /**
   * Maps a raw {@link CategoryData} element to a {@link Category} element ignoring subcategories.
   */
  fromDataSingle(categoryData: CategoryData): Category {
    if (categoryData) {
      const categoryPath = this.mapCategoryPath(categoryData.categoryPath);
      const uniqueId = categoryPath[categoryPath.length - 1];
 
      return {
        uniqueId,
        categoryRef: categoryData.categoryRef,
        categoryPath,
        name: categoryData.name,
        hasOnlineProducts: categoryData.hasOnlineProducts,
        description: categoryData.description,
        images: this.imageMapper.fromImages(categoryData.images),
        attributes: categoryData.attributes,
        completenessLevel: this.computeCompleteness(categoryData),
        seoAttributes: SeoAttributesMapper.fromData(categoryData.seoAttributes),
      };
    } else {
      throw new Error(`'categoryData' is required`);
    }
  }
 
  /**
   * Converts the tree of {@link CategoryData} to the model entity {@link CategoryTree}.
   * Inserts all sub categories accordingly.
   */
  fromData(categoryData: CategoryData): CategoryTree {
    if (categoryData) {
      // recurse into tree
      let subTrees: CategoryTree;
      if (categoryData.subCategories?.length) {
        subTrees = categoryData.subCategories
          .map(c => this.fromData(c) as CategoryTree)
          .reduce((a, b) => CategoryTreeHelper.merge(a, b));
      } else {
        subTrees = CategoryTreeHelper.empty();
      }
 
      // create tree from current category
      const rootCat = this.fromDataSingle(categoryData);
      const tree = CategoryTreeHelper.single(rootCat);
 
      // create tree from categoryPath stubs
      const categoryPathTree = this.categoriesFromCategoryPath(categoryData.categoryPath);
 
      // merge sub categories onto current tree
      const treeWithSubCategories = CategoryTreeHelper.merge(tree, subTrees);
 
      // merge categoryPath stubs onto current tree
      return CategoryTreeHelper.merge(treeWithSubCategories, categoryPathTree);
    } else {
      throw new Error(`'categoryData' is required`);
    }
  }
}