Source: port.js

/**
 * Accessible via `require('@buggyorg/graphtools').Port`
 * 
 * Methods for handling ports. A port is attached to a node and connects data sources with sinks.
 * The port format is a unique identifier for the port and its node unlike the port name.
 * @module Port */

import curry from 'lodash/fp/curry'
import merge from 'lodash/fp/merge'
import _ from 'lodash'

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

/**
 * Checks whether a string represents a port notation. A port notation is a string that
 * contains the information about the node and the port separated by an `@`, e.g. 'nodeA@portB'
 * @params {string} port The string to check
 * @returns {boolean} True if the string represents a port notation, false otherwise.
 */
export function isPortNotation (port) {
  return typeof (port) === 'string' && port.indexOf('@') !== -1
}

function parsePortNotation (port) {
  var split = port.split('@')
  var res = {}
  res.node = split[0]
  if (split[1] === '') {
    throw new Error('Invalid port notation. Port notation does not contain a port. Parsed port: ' + port)
  } else {
    res.port = split[1]
  }
  return res
}

/**
 * Returns a normalized port object in the form `{node: <node-id>, port: <port-name>}`
 * @param port The port object in any digestable form.
 * @returns {Port} A port object.
 */
export function normalize (port) {
  if (isPortNotation(port)) {
    return assureType(parsePortNotation(port))
  } else {
    if (isPortNotation(port.port)) {
      return assureType(merge(port, {port: parsePortNotation(port.port).port}))
    }
    return assureType(port)
  }
}

function assureType (port) {
  if (!port.type) {
    return _.merge({type: 'generic'}, port)
  }
  return port
}

/**
 * Returns the node stored in the port
 * @param {Port} port The port in any form.
 * @returns {NodeID} The node of the port.
 */
export function node (port) {
  return normalize(port).node
}

/**
 * Returns the port name stored for the port.
 * @param {Port} port The port in any form.
 * @returns {string} The name of the port.
 */
export function portName (port) {
  return normalize(port).port
}

/**
 * Returns the data type of the port.
 * @param {Port} port The port
 * @returns The type of the port.
 */
export function type (port) {
  return normalize(port).type
}

/**
 * Sets the type of a port and returns a new port with this updated port.
 * @param type The type to set.
 * @param {Port} port The port
 * @returns The updated port.
 */
export function setType (type, port) {
  return merge(port, {type})
}

/**
 * Returns the kind of the port. Either output or input.
 * @param {Port} port The port
 * @returns The kind of the port. It is either 'input' or 'output'.
 */
export function kind (port) {
  return normalize(port).kind
}

/**
 * Returns whether the given port is an output port or not
 * @param {Port} port The port to check
 * @returns {boolean} True if the port is an output port, false otherwise.
 */
export function isOutputPort (port) {
  return port.kind === OUTPUT
}

/**
 * Returns whether the given port is an input port or not
 * @param {Port} port The port to check
 * @returns {boolean} True if the port is an input port, false otherwise.
 */
export function isInputPort (port) {
  return port.kind === INPUT
}

/**
 * Returns whether the given object is a port object or not.
 * @params any Any value.
 * @returns {boolean} True if the value is a port, false otherwise.
 */
export function isPort (any) {
  return (typeof (any) === 'object' && typeof (any.node) === 'string' && typeof (any.port) === 'string') ||
    typeof (any) === 'string' && isPortNotation(any)
}

function isLetter (c) {
  return /[a-zA-Z]/.test(c)
}

/**
 * Returns whether the given port is valid or not.
 * @param port The object to test.
 * @returns {boolean} True if the value is a valid port, false otherwise.
 */
export function isValid (port) {
  return typeof (port) === 'object' && port.port && (port.kind === INPUT || port.kind === OUTPUT) && port.type &&
    isLetter(port.port[0])
}

/**
 * Asserts that the given port is valid.
 * @param port The object to test.
 * @throws {Error} Error if the object is not a valid port.
 */
export function assertValid (port) {
  if (typeof (port) !== 'object') {
    throw new Error('Port is not an object, but is ' + port)
  }
  if (!port.port) {
    throw new Error('Port does not have a `port` prop')
  }
  if (!port.type) {
    throw new Error('Port does not have a `type` prop')
  }
  if (port.kind !== INPUT && port.kind !== OUTPUT) {
    throw new Error('Port `kind` prop should be "input" or "output", but was ' + port.kind)
  }
}

/**
 * Returns a string representation of the port
 * @params {Port} port The port
 * @returns {string} A string representation of the port.
 */
export function toString (port) {
  return node(port) + '@' + portName(port)
}

/**
 * @function
 * @name equal
 * @description Determines if two ports are equal
 * @param {Port} port1 One of the ports.
 * @param {Port} port2 The other port..
 * @returns {boolean} True if the ports are equal, false otherwise.
 */
export const equal = curry((port1, port2) => {
  return node(port1) === node(port2) && isomorph(port1, port2)
})

export const isomorph = curry((port1, port2) => {
  return portName(port1) === portName(port2) && (type(port1) === type(port2))
})

/**
 * @function
 * @name create
 * @description Create a new port object.
 * @param {Node|String} node A node object or the id of a valid node.
 * @param {String} port The name of the port
 * @param {String} kind 'input' or 'output'
 * @returns {Port} A valid port object.
 */
export const create = curry((node, port, kind, type = 'generic') => {
  if (typeof (node) === 'object') {
    return normalize({node: node.id, port, kind, type})
  } else {
    return normalize({node, port, kind, type})
  }
})

/**
 * @function
 * @name port
 * @description Create a proto port object that can be used as a location. It is no
 * valid port.
 * @param {Node|String} node A node object or the id of a valid node.
 * @param {String} port The name of the port
 * @returns {Port} A valid port object.
 */
export const port = curry((node, port) => {
  if (typeof (node) === 'object') {
    return normalize({node: node.id, port})
  } else {
    return normalize({node, port})
  }
})