All files / src/app/services/data-service data.service.ts

100% Statements 98/98
71.43% Branches 10/14
100% Functions 31/31
100% Lines 85/85
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 1451x   1x   1x 1x   1x       1x 1x 1x 1x 1x   1x     1x   1x 1x                 30x 30x 30x 30x 30x 30x         3x 3x     3x       7x 7x 7x 7x 7x 7x 7x 4x 1x       8x 8x   18x     8x 8x 6x       3x 3x 3x 3x   2x 1x       14x 14x 12x 39x       12x   3x     27x 27x 27x   20x 20x 20x 20x 20x   7x     6x 22x 6x 14x     1x 6x 22x 22x 22x 22x   6x 14x     1x   22x 22x 22x 22x 22x 22x 22x     1x 7x 7x 7x 7x   1x  
import '../../rxjs-extensions';
 
import { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { forkJoin } from 'rxjs/observable/forkJoin';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { NgRedux, select } from '@angular-redux/store';
 
import { IAppState } from '../../store/state/AppState';
import { IFileInfo } from '../../model/fileInfo.model';
import { FormatService } from './format.service';
import { NameParsingService } from './name-parsing.service';
import { ListFormatterService } from '../list-formatter.service/list-formatter.service';
import { FileService } from '../file-service/file.service';
import { MigrationWorkBenchCommonModule, ToastrService, Logger, maskedTrim } from '../../common/mw.common.module';
import { IMeasureUpdate } from '../../model/measure.model';
import { PageActions } from '../../linqpad-review-pages/common/page.actions';
 
@Injectable()
export abstract class DataService {
 
  @select(['config', 'baseDataUrl']) baseDataUrl$: Observable<string>;
  @select(['file', 'fileList', 'files']) fileNames$: Observable<string[]>;
  public abstract config$: Observable<any>;
  public abstract files$: Observable<IFileInfo[]>;
 
  protected abstract PAGE;
  protected baseUrl;
  protected filePrefixes;
 
  constructor (
    protected formatService: FormatService,
    protected nameParsingService: NameParsingService,
    protected listFormatterService: ListFormatterService,
    protected fileService: FileService,
    protected logger: Logger,
    protected pageActions: PageActions,
  ) {}
 
  public abstract getMeasure(): Observable<IMeasureUpdate>;
 
  protected getBaseDataUrl$() {
    return this.baseDataUrl$
      .waitFor$()
      .do(baseUrl => {
        this.baseUrl = baseUrl;
      });
  }
 
  public initializeList(numToInitialize, numToDisplay) {
    this.pageActions.initializeListRequest();
    this.fileService.getFileList(this.baseUrl);
    const parsed$ = this.parsed$(this.filesOfType$);
    const ordered$ = this.listFormatterService.process(parsed$);
    const withContent$ = this.withContent$(ordered$, numToInitialize);
    withContent$.subscribe(
      (files) => { this.pageActions.initializeListSuccess(files, numToDisplay); },
      (error) => { this.pageActions.initializeListFailed(error); }
    );
  }
 
  get filesOfType$(): Observable<string[]> {
    return this.fileNames$
      .waitFor$()  // ensures can move between Observable<fileInfo> and Observable<fileIno[]> with .toArray()
      .map(files => files.filter(file => this.filePrefixes.some(value => file.startsWith(value))));
  }
 
  protected parsed$(files$: Observable<string[]>): Observable<IFileInfo[]> {
    return files$.map(files =>
      this.nameParsingService.parseFiles(files, this.filePrefixes)
    );
  }
 
  public updateList(numToDisplay: number) {
    this.pageActions.updateListRequest();
    const files$ = this.files$.take(1);  // read once - otherwise, endlessly loops due to self update
    this.withContent$(files$, numToDisplay)
      .subscribe(
        (files) => { this.pageActions.updateListSuccess(files, numToDisplay); },
        (error) => { this.pageActions.updateListFailed(error); }
      );
  }
 
  private withContent$(files$: Observable<IFileInfo[]>, numToInitialize: number): Observable<IFileInfo[]> {
    return files$.concatMap(files => {
      const withContent$ = files.map((file, index) => {
        return file.content || index >= numToInitialize
          ? Observable.of(file)
          : this.getContent(file);
      });
      return forkJoin(...withContent$);
    })
    .catch(error => this.handleError(error, 'withContent$'));
  }
 
  public getContent(fileInfo: IFileInfo): Observable<IFileInfo> {
    const url = this.baseUrl + fileInfo.name + '.html';
    return this.fileService.getFile(url)
      .map((res: Response) => {
        const newFileInfo = {...fileInfo};
        const date = res.headers.get('date');
        newFileInfo.lastModified = date ? new Date(date) : null;
        const content = res.text() || '';
        return this.formatService.processContent(content, newFileInfo);
      })
      .catch(error => this.handleError(error, 'getContent'));
  }
 
  protected filesByDate(files) {
    const keyFn = (file) => this.formatDate(file.effectiveDate);
    return this.groupBy(files, keyFn)
      .map(group => ({date: group.key, files: group.group }));
  }
 
  private groupBy(arr, keyFn) {
    const grouped = arr.reduce( (grouping, item) => {
      const key = keyFn(item);
      grouping[key] = grouping[key] || [];
      grouping[key].push(item);
      return grouping;
    }, {} );
    return Object.keys(grouped)
      .map(key => ({key: key, group: grouped[key]}));
  }
 
  private formatDate(date) {
    // tslint:disable:prefer-const
    let d = new Date(date),
        month = '' + (d.getMonth() + 1),
        day = '' + d.getDate(),
        year = d.getFullYear();
    month = month.length < 2 ? '0' + month : month;
    day = day.length < 2 ? '0' + day : day;
    return [year, month, day].join('-');
  }
 
  private handleError(error: any, method: string) {
    const caller = `${this.constructor.name}.${method}`;
    this.logger.error(caller + ': ' + error);
    error.caller = caller;
    return Observable.throw(error);
  }
}