Source: component.js

/**
 * A component is a template for nodes. They represent the structure of the component.
 *
 * A component consists of a `componentId` and is an atomic or a compound node. It must have
 * a valid semver version and a list of ports (each component must have at least one port).
 *
 * ```json
 * {
 *   componentId: 'componentIdentifier',
 *   version: "1.0.0",
 *   atomic: true,
 *   ports: [{port: 'in', kind: 'input', type: 'Number'},{port: 'out', kind: 'output', type: 'Number'}]
 * }
 * ```
 *
 * Accessible via `require('@buggyorg/graphtools').Component`
 * @module Component */

import curry from 'lodash/fp/curry'
import omit from 'lodash/fp/omit'
import zip from 'lodash/fp/zip'
import fromPairs from 'lodash/fp/fromPairs'
import merge from 'lodash/fp/merge'
import _ from 'lodash'
import * as Port from './port'
import {children, isCompound} from './compound'
import {create, id as nodeID, hasChildren} from './node'
import semver from 'semver'
import isEqual from 'lodash/fp/isEqual'

const OUTPUT = 'output'
const INPUT = 'input'

/**
 * Returns the unique identifier of a node
 * @params {Component} node The node
 * @returns {string} The unique identifier of the node
 * @throws {Error} If the node value is invalid.
 */
export function id (component) {
  if (typeof (component) === 'string') {
    return component
  } else if (component == null) {
    throw new Error('Cannot determine id of undefined component.')
  } else if (!component.componentId) {
    throw new Error('Malformed component. The component must either be a string that represents the id. Or it must be an object with an componendId field.\n Component: ' + JSON.stringify(component))
  }
  return component.componentId
}

/**
 * @function
 * @name equal
 * @description Tests whether two components are the same component. This tests only if their component IDs are
 * the same not if both components contain the same information.
 * @param {Component} comp1 One of the components to test.
 * @param {Component} comp2 The other one.
 * @returns {boolean} True if they have the same id, false otherwise.
 */
export const equal = curry((comp1, comp2) => {
  return id(comp1) === id(comp2)
})

/**
 * Gets all ports of the component.
 * @param {Component} comp The component.
 * @returns {Port[]} A list of ports.
 */
export function ports (comp) {
  return comp.ports || []
}

/**
 * Gets all output ports of the comp.
 * @param {Component} comp The node.
 * @returns {Port[]} A possibly empty list of output ports.
 */
export function outputPorts (comp, ignoreCompounds = false) {
  if (!ignoreCompounds && !comp.atomic) {
    return comp.ports
  } else {
    return comp.ports.filter((p) => p.kind === OUTPUT)
  }
}

/**
 * Gets all input ports of the component.
 * @param {Component} comp The component.
 * @returns {Port[]} A possibly empty list of input ports.
 */
export function inputPorts (comp, ignoreCompounds = false) {
  if (!ignoreCompounds && !comp.atomic) {
    return comp.ports
  } else {
    return comp.ports.filter((p) => p.kind === INPUT)
  }
}

/**
 * @function
 * @name port
 * @description Returns the port data for a given port.
 * @param {Component} comp The component which has the port.
 * @param {String} name The name of the port.
 * @returns {Port} The port data.
 * @throws {Error} If no port with the given name exists in this component an error is thrown.
 */
export const port = curry((name, comp) => {
  var port = _.find(comp.ports, (p) => Port.portName(p) === name)
  if (!port) {
    throw new Error('Cannot find port with name ' + name + ' in component ' + JSON.stringify(comp))
  }
  return port
})

/**
 * @function
 * @name hasPort
 * @description Checks whether the component has the specific port.
 * @param {String} name The name of the port.
 * @param {Component} comp The component which has the port.
 * @returns {Port} True if the port has a port with the given name, false otherwise.
 */
export const hasPort = curry((name, comp) => {
  return !!_.find(comp.ports, (p) => Port.portName(p) === name)
})

/**
 * Checks whether a component is in a valid format, i.e. if it has an id field and at least one port.
 * @param {Component} comp The component to test.
 * @returns {boolean} True if the component is valid, false otherwise.
 */
export function isValid (comp) {
  return typeof (comp) === 'object' && typeof (comp.componentId) === 'string' && comp.componentId.length > 0 &&
    ports(comp).length !== 0 && typeof (comp.version) === 'string' && semver.valid(comp.version)
}

export function assertValid (comp) {
  if (typeof (comp) !== 'object') {
    throw new Error('Component is not an object, but it is: ' + comp)
  }
  if (typeof (comp.componentId) !== 'string' || comp.componentId.length === 0) {
    throw new Error('Component must have a valid id (string with at least one character), but it is: ' + comp.componentId)
  }
  if (ports(comp).length === 0) {
    throw new Error('Component "' + id(comp) + '" must have at least one port.')
  }
  if (typeof (comp.version) !== 'string' || !semver.valid(comp.version)) {
    throw new Error('Component "' + id(comp) + '" must have a valid version, but it is: ' + comp.version)
  }
}

const mapEdgeIDs = curry((map, edge) => {
  if (typeof (edge.from) === 'object' && typeof (edge.to) === 'object') {
    return merge(edge, {
      from: {
        node: (map[edge.from.node]) ? map[edge.from.node] : edge.from.node
      },
      to: {
        node: (map[edge.to.node]) ? map[edge.to.node] : edge.to.node
      }
    })
  } else {
    return merge(edge, {
      from: (map[edge.from]) ? map[edge.from] : edge.from,
      to: (map[edge.to]) ? map[edge.to] : edge.to
    })
  }
})

/**
 * Create a node from a component.
 * @param {Reference} reference The reference to the component.
 * @param {Component} comp The component that is the basis for the new node.
 * @returns {Node} A node with the given name representing the component.
 */
export function createNode (reference, comp) {
  if (hasChildren(comp)) {
    const storeID = (n) => { n.__id = n.id; return n }
    const remStoredID = (n) => { delete n.__id; return n }
    const newNodes = children(comp)
      .map(storeID).map(omit('id')).map(create).map((n) => createNode({}, n)).map(remStoredID)
    var idMapping = fromPairs(zip(children(comp).map(nodeID), newNodes.map(nodeID)))
    if (comp.__id) idMapping[comp.__id] = comp.id
    return _.merge({}, reference, comp, {
      nodes: newNodes,
      edges: (comp.edges || []).map(mapEdgeIDs(idMapping))
    })
  }
  return _.merge({}, reference, comp)
}

/**
 * Tests if two components are isomorphic i.e. deep equal.
 * @param {Portgraph} graph1 One of the graphs.
 * @param {Portgraph} graph2 And the other graph to test.
 * @returns {Boolean} True if the components of the two graphs are isomorphic, false otherwise. Components
 * are isomorphic, if they are deep equal.
 */
export function isomorph (graph1, graph2) {
  return isEqual(graph1, graph2)
}