Home Reference Source

src/refs.js

import Immutable, {List} from 'immutable';

import {valueRenderers} from '~/renderers';
import RenderData from '~/renderers/renderData.js';
import * as Types from '~/types';

export function parseRef(field) {
  if (field instanceof Ref) {
    return field;
  }

  if (field === null || typeof field == 'undefined') {
    return null;
  }

  if (typeof field == 'string') {
    // Quick hack. Remove soon.
    if (field[1] == ':') {
      const refTypeCode = field[0];
      const refValue = field.slice(2);
      switch (refTypeCode) {
        case 'r':
          return new ImmutableRef(refValue);

        case 'q':
          return new ImmutableListFindRef(createTruthyViewType(refValue));

        case 'f':
          return new ImmutableListFilterRef(createTruthyViewType(refValue));

        case 'm':
          return new ImmutableListMapRef(createMappyViewType(refValue));

        case 'v':
          return new ImmutableViewRef(refValue);

        default:
          throw new Error(`Invalid hacky ref type: "${field}"`);
      }
    }

    return new ImmutableRef(field);
  }

  switch (field.get('type')) {
    case 'value':
      return new ImmutableRef(field.get('value'));

    case 'view':
      return new ImmutableViewRef(Types.parseField(
        Types.VIEW,
        field.get('view')
      ));

    case 'list':
      const refValue = field.get('value');
      switch (refValue.get('type')) {
        case 'find': 
          return new ImmutableListFindRef(
            Types.parseField(
              Types.VIEW,
              refValue.get('finder')
            )
          );

        case 'filter':
          return new ImmutableListFilterRef(
            Types.parseField(
              Types.VIEW,
              refValue.get('filter')
            )
          );

        case 'map':
          return new ImmutableListMapRef(
            Types.parseField(
              Types.VIEW,
              refValue.get('mapper')
            )
          );

        default:
          throw new Error(`Unknown list ref type "${refValue.get('type')}"`);
      }

    default:
      throw new Error(`Unknown ref type "${field.get('type')}"`);
  }
}

function createMappyViewType(refValue) {
  return new Types.view.data({
    label: refValue,
    ref: parseRef(refValue)
  });
}

function createTruthyViewType(refValue) {
  const [ref, value] = refValue.split('=');

  return new Types.view.condition({
    label: `${ref}=${value}`,
    op: '=',
    args: [
      createMappyViewType(ref),
      new Types.view.value({value})
    ],
    trueType: new Types.view.value({value: true}),
    falseType: new Types.view.value({value: false})
  });
}

export class Ref {
  getValue() {
    throw new Error('Abstract function');
  }

  getDisplay() {
    throw new Error('go away');
  }

  toString() {
    return this.getDisplay();
  }

  equals() {
    return false;
  }

  hashCode() {
    return 0;
  }
}

export class ImmutableRef extends Ref {
  constructor(ref) {
    super();
    this.ref = ref;
  }

  isListRef() {
    return false;
  }

  isSingleRef() {
    return true;
  }

  isMultiRef() {
    return !this.isSingleRef();
  }

  getValue(dataType, dataValue, renderOptions) {
    if (!dataValue) {
      return null;
    }

    if (!this.ref) {
      return dataValue;
    }

    return dataValue.get(this.ref);
  }

  setValue(dataType, dataValue, childValue, renderOptions) {
    if (!dataValue) {
      throw new Error(`Cannot set value for on a null object: (with ${this.getDisplay})`);
    }

    if (!this.ref) {
      return dataValue;
    }

    return dataValue.set(this.ref, childValue);
  }

  getDisplay() {
    return this.ref;
  }

  equals(other) {
    return this.ref == other.ref;
  }

  hashCode() {
    return Immutable.hash(this.ref);
  }
}

export class ImmutableViewRef extends ImmutableRef {
  constructor(view) {
    super();
    this.view = view;
  }

  getValue(dataType, dataValue, renderOptions) {
    if (!dataValue) {
      return null;
    }

    if (!this.view) {
      return dataValue;
    }

    const renderData = new RenderData(dataType, dataValue, renderOptions);
    return valueRenderers.getValue(this.view, renderData)
  }

  setValue() {
    throw new Error('Not yet implemented');
  }

  getDisplay() {
    return this.view.getLabel();
  }

  equals(other) {
    return this.view.uniqueId == other.view.uniqueId;
  }

  hashCode() {
    return Immutable.hash(this.view.uniqueId);
  }
}

export class ImmutableListRef extends ImmutableRef {
  constructor(view) {
    super();
    this.view = view;
  }

  isListRef() {
    return true;
  }

  isFinder() {
    return false;
  }

  isFilterer() {
    return false;
  }

  isMapper() {
    return false;
  }

  checkValidData(dataType, dataValue) {
    if (!dataType instanceof Types.data.list) {
      throw new Error(`Cannot reference a list with a non-list based data type "${dataType}"`);
    }

    if (!dataValue || !Immutable.isImmutable(dataValue)) {
      throw new Error(`Cannot reference a non-list with a list ref of ${this.view}`);
    }
  }

  getDisplay() {
    return this.view.getLabel();
  }

  applyView(itemType, item, renderOptions) {
    const renderData = new RenderData(itemType, item, renderOptions);
    return valueRenderers.getValue(this.view, renderData);
  }

  getValue(listType, list, renderOptions) {
    this.checkValidData(listType, list);
    
    if (!this.constructor.method) {
      return list;
    }

    const itemType = listType.getItemType();
    return list[this.constructor.method](
      (item, index) => this.applyView(itemType, item, renderOptions)
    );
  }

  setValue(listType, list, newItem, renderOptions) {
    if (List.isList(newItem)) {
      console.warn('Setting each list item to be a list. This may be due to using multiple list refs which is currently unimplemented');
    }

    return list.map(() => newItem);
  }

  equals(other) {
    return this.view.uniqueId == other.view.uniqueId;
  }

  hashCode() {
    return Immutable.hash(this.view.uniqueId);
  }
}

export class ImmutableListFindRef extends ImmutableListRef {
  static method = 'find';

  isFinder() {
    return true;
  }

  setValue(listType, list, newItem, renderOptions) {
    const itemType = listType.getItemType();
    const index = list
      .findIndex(item => {
        const renderData = new RenderData(itemType, item, renderOptions);
        return valueRenderers.getValue(this.view, renderData);
      });

    if (index >= 0) {
      return list.set(index, newItem);
    } else {
      return list.push(newItem);
    }
  }
}

export class ImmutableListFilterRef extends ImmutableListRef {
  static method = 'filter'

  isSingleRef() {
    return false;
  }

  isFilterer() {
    return true;
  }

  setValue(listType, list, newItem, renderOptions) {
    if (List.isList(newItem)) {
      console.warn('Setting each list item to be a list. This may be due to using multiple list refs which is currently unimplemented');
    }

    const itemType = listType.getItemType();

    const indexes = list
      .map((item, index) => [
        index,
        item
      ])
      .filter(([index, item]) => {
        const renderData = new RenderData(itemType, item, renderOptions);
        return valueRenderers.getValue(this.view, renderData);
      })
      .map(([index, item]) => index);

    return list
      .map((item, index) => indexes.includes(index) ?
        newItem :
        index
      );
  }
}

export class ImmutableListMapRef extends ImmutableListRef {
  static method = 'map'

  isSingleRef() {
    return false;
  }

  isMapper() {
    return true;
  }

  setValue(listType, list, newItem) {
    throw new Error('Not yet implemented');
  }
}