javiercejudo/modelico

View on GitHub
src/types/Base.js

Summary

Maintainability
A
1 hr
Test Coverage
import {
  always,
  defaultTo,
  isFunction,
  isPlainObject,
  isSomething,
  emptyObject,
  haveDifferentTypes,
  equals
} from '../U'

import {typeSymbol, fieldsSymbol} from '../symbols'
import getInnerTypes from '../getInnerTypes'

const getInReducer = (result, part) => result.get(part)

class Base {
  constructor(arg1, arg2 = emptyObject, thisArg) {
    let fields = arg2
    let Type = this.constructor

    if (isPlainObject(arg1)) {
      fields = arg1
    } else if (arg1 !== undefined) {
      Type = arg1
    }

    if (!isPlainObject(fields)) {
      throw TypeError(
        `expected an object with fields for ${Type.displayName ||
          Type.name} but got ${fields}`
      )
    }

    // This slows down the benchmarks by a lot, but it isn't clear whether
    // real usage would benefit from removing it.
    // See: https://github.com/javiercejudo/modelico-benchmarks
    Object.freeze(fields)

    const defaults = {}
    const innerTypes = getInnerTypes([], Type)

    thisArg = defaultTo(this)(thisArg)
    thisArg[typeSymbol] = always(Type)

    Object.keys(innerTypes).forEach(key => {
      const valueCandidate = fields[key]
      const defaultCandidate = innerTypes[key].default
      let value

      if (isSomething(valueCandidate)) {
        value = valueCandidate
      } else if (defaultCandidate !== undefined) {
        value = defaultCandidate
        defaults[key] = value
      } else {
        throw TypeError(
          `no value for key "${key}" of ${Type.displayName || Type.name}`
        )
      }

      thisArg[key] = always(value)
    })

    thisArg[fieldsSymbol] = always(
      Object.freeze(Object.assign(defaults, fields))
    )
  }

  get [Symbol.toStringTag]() {
    return 'ModelicoModel'
  }

  get(field) {
    return this[fieldsSymbol]()[field]
  }

  getIn(path) {
    return path.reduce(getInReducer, this)
  }

  copy(fields) {
    const newFields = Object.assign({}, this[fieldsSymbol](), fields)

    return new (this[typeSymbol]())(newFields)
  }

  set(field, value) {
    if (isFunction(this[field]) && this[field]() === value) {
      return this
    }

    return this.copy({[field]: value})
  }

  setIn(path, value) {
    if (path.length === 0) {
      return this.copy(value)
    }

    const [key, ...restPath] = path
    const item = this[key]()

    if (!item.setIn) {
      return this.set(key, value)
    }

    return this.set(key, item.setIn(restPath, value))
  }

  equals(other) {
    if (this === other) {
      return true
    }

    if (haveDifferentTypes(this, other)) {
      return false
    }

    const thisFields = this[fieldsSymbol]()
    const otherFields = other[fieldsSymbol]()

    const thisKeys = Object.keys(thisFields)
    const otherKeys = Object.keys(otherFields)

    if (thisKeys.length !== otherKeys.length) {
      return false
    }

    return thisKeys.every(key => equals(thisFields[key], otherFields[key]))
  }

  toJSON() {
    return this[fieldsSymbol]()
  }

  toJS() {
    return JSON.parse(JSON.stringify(this))
  }

  stringify(n) {
    return JSON.stringify(this, null, n)
  }

  static innerTypes() {
    return emptyObject
  }

  static factory(...args) {
    return new Base(...args)
  }
}

export default Object.freeze(Base)