Files
xod/packages/xod-project/src/patch.js
2017-02-02 11:40:43 +03:00

479 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import R from 'ramda';
import { Either } from 'ramda-fantasy';
import * as CONST from './constants';
import * as Tools from './func-tools';
import * as Node from './node';
import * as Link from './link';
import * as Pin from './pin';
/**
* An object representing single patch in a project
* @typedef {Object} Patch
*/
/**
* A {@link Patch} object or just its full path as a {@link string}
* @typedef {(Patch|string)} PatchOrPath
*/
/**
* @function createPatch
* @returns {Patch} a validatePath error or newly created patch
*/
export const createPatch = () => ({
nodes: {},
links: {},
});
/**
* @function duplicatePatch
* @param {Patch} patch
* @returns {Patch} deeply cloned patch
*/
export const duplicatePatch = R.clone;
/**
* @function getPatchLabel
* @param {Patch} patch
* @returns {string}
*/
export const getPatchLabel = R.propOr('', 'label');
/**
* @function setPatchLabel
* @param {string} label
* @param {Patch} patch
* @returns {Patch} a copy of the `patch` with new label
*/
export const setPatchLabel = R.useWith(
R.assoc('label'),
[String, R.identity]
);
/**
* Returns a list of implementations for which a `patch` has native implementation
*
* For example, `['js', 'arduino', 'espruino', 'nodejs']`.
*
* @function listImpls
* @param {Patch} patch
* @returns {string[]}
*/
export const listImpls = R.compose(
R.keys,
R.propOr({}, 'impls')
);
/**
* Returns true if patch has any of specified implementations.
*
* @function hasImpl
* @param {string[]} impls
* @param {Patch} patch
* @type {Boolean}
*/
export const hasImpl = R.curry((impls, patch) => R.compose(
R.complement(R.isEmpty),
R.intersection(impls),
listImpls
)(patch));
/**
* @function validatePatch
* @param {Patch} patch
* @returns {Either<Error|Patch>}
*/
export const validatePatch = Tools.errOnFalse(
CONST.ERROR.PATCH_INVALID,
R.allPass([
R.has('nodes'),
R.has('links'),
])
);
// =============================================================================
//
// Nodes
//
// =============================================================================
/**
* Checks that node id to be equal specified value
*
* @function nodeIdEquals
* @param {string} id
* @param {NodeOrId} node
* @returns {boolean}
*/
export const nodeIdEquals = R.curry(
(id, node) =>
R.compose(
R.equals(id),
Node.getNodeId
)(node)
);
/**
* @function listNodes
* @param {Patch} patch - a patch to get nodes from
* @returns {Node[]} list of all nodes not sorted in any arbitrary order
*/
export const listNodes = R.compose(
R.values,
R.propOr([], 'nodes')
);
/**
* @function getNodeById
* @param {string} id - node ID to find
* @param {Patch} patch - a patch where node should be searched
* @returns {Maybe<Node>} a node with given ID or `undefined` if it wasnt not found
*/
export const getNodeById = R.curry(
(id, patch) => R.compose(
Tools.find(nodeIdEquals(id)),
listNodes
)(patch)
);
// =============================================================================
//
// Pins
//
// =============================================================================
/**
* Returns new patch with new pin.
*
* @function assocPin
* @param {Pin} pin
* @param {Patch} patch
* @returns {Either<Error|Patch>}
*/
export const assocPin = R.curry(
(pin, patch) => Pin.validatePin(pin).map(
(validPin) => {
const key = Pin.getPinKey(validPin);
return R.assocPath(['pins', key], validPin, patch);
}
)
);
/**
* Returns new patch without pin.
*
* @function dissocPin
* @param {PinOrKey} key
* @param {Patch} patch
* @returns {Patch}
*/
export const dissocPin = R.curry(
(pinOrKey, patch) => R.dissocPath(['pins', Pin.getPinKey(pinOrKey)], patch)
);
/**
* @private
* @function getPins
* @param {Patch}
* @returns {Pins}
*/
const getPins = R.propOr({}, 'pins');
/**
* Returns pin object by key
*
* @function getPinByKey
* @param {string} key
* @param {Patch} patch
* @returns {Maybe<Pin>}
*/
export const getPinByKey = R.curry(
(key, patch) => R.compose(
Tools.prop(key),
getPins
)(patch)
);
/**
* @function listPins
* @param {Patch} patch
* @returns {Pin[]}
*/
export const listPins = R.compose(
R.values,
getPins
);
/**
* @function listInputPins
* @param {Patch} patch
* @returns {Pin[]}
*/
export const listInputPins = R.compose(
R.filter(Pin.isInputPin),
listPins
);
/**
* @function listOutputPins
* @param {Patch} patch
* @returns {Pin[]}
*/
export const listOutputPins = R.compose(
R.filter(Pin.isOutputPin),
listPins
);
// =============================================================================
//
// Links
//
// =============================================================================
/**
* @function listLinks
* @param {Patch} patch - a patch to operate on
* @returns {Link[]} list of all links not sorted in any arbitrary order
*/
export const listLinks = R.compose(
R.values,
R.propOr({}, 'links')
);
/**
* Checks that link id to be equal specified value
* @private
* @function linkIdEquals
* @param {string} id [description]
* @param {LinkOrId} link [description]
* @returns {boolean}
*/
export const linkIdEquals = R.curry(
(id, link) =>
R.compose(
R.equals(id),
Link.getLinkId
)(link)
);
/**
* @function getLinkById
* @param {string} id - a link ID to find
* @param {Patch} patch - a patch to operate on
* @returns {Maybe<Link>} a link with given `id` or Null if not found
*/
export const getLinkById = R.curry(
(id, patch) => R.compose(
Tools.find(linkIdEquals(id)),
listLinks
)(patch)
);
/**
* Returns list of all links are connected to specified node.
*
* @function listLinksByNode
* @param {NodeOrId} nodeOrId
* @param {Patch} patch
* @returns {Link[]}
*/
export const listLinksByNode = R.curry(
(nodeOrId, patch) => {
const id = Node.getNodeId(nodeOrId);
const list = listLinks(patch);
// :: Link -> boolean
const filterByNodeId = R.either(
Link.isLinkInputNodeIdEquals(id),
Link.isLinkOutputNodeIdEquals(id)
);
return R.filter(filterByNodeId, list);
}
);
/**
* Returns list of all links are connected to specified pin.
*
* @function listLinksByPin
* @param {string} pinKey
* @param {NodeOrId} nodeOrId
* @param {Patch} patch
* @returns {Link[]}
*/
export const listLinksByPin = R.curry(
(pinKey, nodeOrId, patch) => {
const id = Node.getNodeId(nodeOrId);
const list = listLinks(patch);
// :: Link -> boolean
const filterByNodeIdAndPinKey = R.either(
R.both(
Link.isLinkInputNodeIdEquals(id),
Link.isLinkInputPinKeyEquals(pinKey)
),
R.both(
Link.isLinkOutputNodeIdEquals(id),
Link.isLinkOutputPinKeyEquals(pinKey)
)
);
return R.filter(filterByNodeIdAndPinKey, list);
}
);
/**
* Checks if the `link` would be valid on the `patch`. I.e. it could be
* [associated](@link assocLink) with it.
*
* The link is considered invalid in eiter case:
* - the link is put on two inputs or on two outputs;
* - a pin does not exists;
* - the input already has another incoming link;
*
* @function validateLink
* @param {Link} link - a link to validate
* @param {Patch} patch - a patch to operate on
* @returns {Either<Error|Link>} validation errors or valid {@link Link}
*/
export const validateLink = R.curry(
(link, patch) => Link.validateLinkId(link)
.chain(Link.validateLinkInput)
.chain(Link.validateLinkOutput)
.chain(() => Tools.errOnNothing(
CONST.ERROR.LINK_INPUT_NODE_NOT_FOUND,
getNodeById(Link.getLinkInputNodeId(link), patch)
))
.chain(() => Tools.errOnNothing(
CONST.ERROR.LINK_OUTPUT_NODE_NOT_FOUND,
getNodeById(Link.getLinkOutputNodeId(link), patch)
))
.map(R.always(link))
);
/**
* Replaces an existing `link` or inserts new one in the `patch`.
*
* Matching is done by links ID.
*
* @function assocLink
* @param {Link} link - new link
* @param {Patch} patch - a patch to operate on
* @returns {Either<Error|Patch>} error or a copy of the `patch` with changes applied
* @see {@link validateLink}
*/
export const assocLink = R.curry(
(link, patch) => validateLink(link, patch).map(
(validLink) => {
const id = Link.getLinkId(validLink);
return R.assocPath(['links', id], validLink, patch);
}
)
);
/**
* Removes the `link` from the `patch`.
*
* Does nothing if the `link` is not found in the patch.
*
* @function dissocLink
* @param {LinkOrId} link - a link to remove
* @param {Patch} patch - a patch to operate on
* @returns {Patch} a copy of the `patch` with changes applied
*/
export const dissocLink = R.curry(
(linkOrId, patch) => R.dissocPath(['links', Link.getLinkId(linkOrId)], patch)
);
// =============================================================================
//
// Nodes assoc/dissoc
//
// =============================================================================
/**
* Replaces a node with new one or inserts new one if it doesnt exist yet.
*
* The node is searched by ID and its state
* subtree is completely replaced with one given as argument.
*
* @function assocNode
* @param {Node} node - new node
* @param {Patch} patch - a patch with the `node`
* @returns {Patch} a copy of the `patch` with the node replaced
*/
// TODO: Refactoring needed
export const assocNode = R.curry(
(node, patch) => {
const id = Node.getNodeId(node);
const addPin = R.curry(
(_node, _patch) => R.ifElse(
Node.isPinNode,
(pinNode) => {
const newPatch = Node.getPinNodeDataType(pinNode).chain(
type => Node.getPinNodeDirection(pinNode).chain(
direction => Pin.createPin(id, type, direction).chain(
// TODO: Add optional data (label, description, order) from node to pin
newPin => assocPin(newPin, _patch)
)
)
);
// TODO: Think is it okay or we should return Either<Error|Patch> for invalid pinNodes?
return Either.either(
() => _patch,
valid => valid,
newPatch
);
},
R.always(_patch)
)(_node)
);
const addNode = R.assocPath(['nodes', id]);
return R.compose(
addNode(node),
addPin(node)
)(patch);
}
);
/**
* Removes the `node` from the `patch`.
*
* Does nothing if the `node` is not in the `patch`.
*
* Also removes all links associated with the `node`.
*
* @function dissocNode
* @param {NodeOrId} node - node to delete
* @param {Patch} patch - a patch where the node should be deleted
* @returns {Patch} a copy of the `patch` with the node deleted
*/
// TODO: Move child function into top-level
export const dissocNode = R.curry(
(nodeOrId, patch) => {
const id = Node.getNodeId(nodeOrId);
const links = listLinksByNode(id, patch);
const removeLinks = R.reduce(
R.flip(dissocLink)
);
const removePin = R.ifElse(
R.compose(
R.chain(Node.isPinNode),
getNodeById(id)
),
dissocPin(id),
R.identity
);
const removeNode = R.dissocPath(['nodes', id]);
return R.compose(
removeNode,
removePin,
removeLinks
)(patch, links);
}
);