refactor(xod-project): introduce $Pin type

This commit is contained in:
Victor Nakoryakov
2017-02-16 13:55:04 +03:00
parent bfd1f1e4a4
commit 30baf5380a
8 changed files with 99 additions and 179 deletions

View File

@@ -13,8 +13,8 @@ import { err, errOnNothing } from './func-tools';
const terminalRegExp = /^xod\/core\/(input|output)/;
// :: String -> Pin[]
const getTerminalPins = type => ([
{ key: '__in__', type, direction: CONST.PIN_DIRECTION.INPUT },
{ key: '__out__', type, direction: CONST.PIN_DIRECTION.OUTPUT },
explode(Pin.createPin('__in__', type, CONST.PIN_DIRECTION.INPUT)),
explode(Pin.createPin('__out__', type, CONST.PIN_DIRECTION.OUTPUT)),
]);
// :: String -> String
const getTerminalType = type => `terminal${type}`;

View File

@@ -209,7 +209,8 @@ export const getNodeById = def(
* @param {Patch} patch
* @returns {Either<Error|Patch>}
*/
export const assocPin = R.curry(
export const assocPin = def(
'assocPin :: Pin -> Patch -> Either Error Patch',
(pin, patch) => Pin.validatePin(pin).map(
(validPin) => {
const key = Pin.getPinKey(validPin);

View File

@@ -1,7 +1,9 @@
import R from 'ramda';
import { Either } from 'ramda-fantasy';
import * as Tools from './func-tools';
import * as Utils from './utils';
import * as CONST from './constants';
import { def } from './types';
/**
* An object representing patch pin
@@ -114,54 +116,12 @@ export const isTerminalPin = R.compose(
getPinKey
);
/**
* Validates for correct pin type
*
* @function validatePinType
* @param {PIN_TYPE} type
* @returns {Either<Error|PIN_TYPE>}
*/
export const validatePinType = Tools.errOnFalse(
CONST.ERROR.PIN_TYPE_INVALID,
R.flip(Tools.hasPropEq)(CONST.PIN_TYPE)
// TODO: remove me
export const validatePin = def(
'validatePin :: Pin -> Either Error Pin',
Either.of
);
/**
* Validates for correct pin direction
*
* @function validatePinDirection
* @param {PIN_DIRECTION} type
* @returns {Either<Error|PIN_DIRECTION>}
*/
export const validatePinDirection = Tools.errOnFalse(
CONST.ERROR.PIN_DIRECTION_INVALID,
R.flip(Tools.hasPropEq)(CONST.PIN_DIRECTION)
);
/**
* Validates for correct pin key
*
* @function validatePinKey
* @param {string} pinKey
* @returns {Either<Error|string>}
*/
export const validatePinKey = Tools.errOnFalse(
CONST.ERROR.PIN_KEY_INVALID,
Utils.validateId
);
/**
* Validates pin correctness
*
* @function validatePin
* @param {Pin} pin
* @returns {Either<Error|Pin>}
*/
export const validatePin = pin => validatePinType(getPinType(pin))
.chain(() => validatePinDirection(getPinDirection(pin)))
.chain(() => validatePinKey(getPinKey(pin)))
.map(R.always(pin));
/**
* @function createPin
* @param {string} key
@@ -169,6 +129,15 @@ export const validatePin = pin => validatePinType(getPinType(pin))
* @param {PIN_DIRECTION} direction
* @returns {Either<Error|Pin>}
*/
export const createPin = R.curry(
(key, type, direction) => validatePin({ key, type, direction })
export const createPin = def(
'createPin :: PinKey -> DataType -> PinDirection -> Either Error Pin',
(key, type, direction) => Either.of({
key,
type,
direction,
label: key,
description: '',
order: 0,
value: Utils.defaultValueOfType(type),
})
);

View File

@@ -3,6 +3,7 @@ import R from 'ramda';
import RF from 'ramda-fantasy';
import $ from 'sanctuary-def';
import HMDef from 'hm-def';
import * as C from './constants';
/* Types are by convention starts with a capital leter, so: */
/* eslint-disable new-cap */
@@ -13,12 +14,26 @@ import HMDef from 'hm-def';
//
//-----------------------------------------------------------------------------
// :: String -> String
const qualifiedTypeName = typeName => `xod-project/${typeName}`;
// :: String -> String
const typeUrl = typeName => `http://xod.io/docs/dev/xod-project/#${typeName}`;
// :: (String, Any -> Boolean) -> Type
const NullaryType = (typeName, predicate) => $.NullaryType(
`xod-project/${typeName}`,
`http://xod.io/docs/dev/xod-project/#${typeName}`,
qualifiedTypeName(typeName),
typeUrl(typeName),
predicate
);
// :: (String, [Any]) -> Type
const EnumType = (typeName, values) => $.EnumType(
qualifiedTypeName(typeName),
typeUrl(typeName),
values
);
// :: Type -> Any -> Boolean
const hasType = type => x => type._test(x);
@@ -78,7 +93,19 @@ export const LinkId = AliasType('LinkId', ShortId);
export const NodeId = AliasType('NodeId', ShortId);
export const PinKey = AliasType('PinKey', $.String);
export const PatchPath = AliasType('PatchPath', $.String);
export const Pin = AliasType('Pin', $.Object); // TODO: enforce model
export const PinDirection = EnumType('PinDirection', R.values(C.PIN_DIRECTION));
export const DataType = EnumType('DataType', R.values(C.PIN_TYPE));
export const DataValue = NullaryType('DataValue', R.complement(R.isNil));
export const Pin = Model('Pin', {
key: PinKey,
direction: PinDirection,
label: Label,
type: DataType,
value: DataValue,
order: $.Number,
description: $.String,
});
export const NodePosition = Model('NodePosition', {
x: $.Number,
@@ -128,8 +155,12 @@ export const env = $.env.concat([
NodeId,
NodeOrId,
Patch,
Pin,
PinKey,
PinRef,
PinDirection,
DataType,
DataValue,
ShortId,
Label,
Source,

View File

@@ -5,6 +5,7 @@ import shortid from 'shortid';
import * as Node from './node';
import * as Tools from './func-tools';
import * as CONST from './constants';
import { def } from './types';
/**
* Contains resulting value or error
@@ -138,6 +139,23 @@ export const validateId = R.test(/^[a-zA-Z0-9\-_]+$/);
*/
export const getCastPatchPath = (typeIn, typeOut) => `xod/core/cast-${typeIn}-to-${typeOut}`;
/**
* Returns a default (empty) value for a given data type.
*
* @function defaultValueOfType
* @param {PIN_TYPE} t
* @returns {*}
*/
export const defaultValueOfType = def(
'defaultValueOfType :: DataType -> DataValue',
R.cond([
[ R.equals(CONST.PIN_TYPE.STRING), R.always('') ],
[ R.equals(CONST.PIN_TYPE.NUMBER), R.always(0) ],
[ R.equals(CONST.PIN_TYPE.BOOLEAN), R.always(false) ],
[ R.equals(CONST.PIN_TYPE.PULSE), R.always(false) ],
])
);
// =============================================================================
//
// Transforming node ids in the patch

View File

@@ -101,12 +101,22 @@ export const defaultizeNode = R.merge({
type: '@/defaultType',
});
export const defaultizePin = R.merge({
key: '$$defaultPinKey',
direction: '$$defaultDirection',
label: '$$defaultLabel',
type: 'number',
value: 0,
order: 0,
description: '$$defaultDesription',
});
export const defaultizePatch = R.compose(
R.evolve({
nodes: R.map(defaultizeNode),
links: R.map(defaultizeLink),
impls: R.identity,
pins: R.identity,
pins: R.map(defaultizePin),
}),
R.merge({
nodes: {},

View File

@@ -431,17 +431,13 @@ describe('Patch', () => {
// entity setters
describe('assocPin', () => {
it('should return Either.Left for invalid pin', () => {
const newPatch = Patch.assocPin({}, {});
expect(newPatch.isLeft).to.be.true();
});
it('should return Either.Right with new patch with new pin', () => {
const pin = {
const pin = Helper.defaultizePin({
key: 'A',
type: CONST.PIN_TYPE.STRING,
direction: CONST.PIN_DIRECTION.OUTPUT,
};
const newPatch = Patch.assocPin(pin, {});
});
const newPatch = Patch.assocPin(pin, emptyPatch);
Helper.expectEither(
(validPatch) => {
@@ -455,17 +451,17 @@ describe('Patch', () => {
);
});
it('should not affect on other pins', () => {
const patchWithPins = {
const patchWithPins = Helper.defaultizePatch({
pins: {
A: { key: 'A', type: CONST.PIN_TYPE.NUMBER, direction: CONST.PIN_DIRECTION.INPUT },
C: { key: 'C', type: CONST.PIN_TYPE.STRING, direction: CONST.PIN_DIRECTION.OUTPUT },
},
};
const pin = {
});
const pin = Helper.defaultizePin({
key: 'B',
type: CONST.PIN_TYPE.BOOLEAN,
direction: CONST.PIN_DIRECTION.INPUT,
};
});
const newPatch = Patch.assocPin(pin, patchWithPins);
const expectedPatch = R.assocPath(['pins', pin.key], pin, patchWithPins);
@@ -562,7 +558,7 @@ describe('Patch', () => {
expect(newPatch)
.to.have.property('pins')
.that.have.property('1')
.that.have.keys(['key', 'type', 'direction']);
.that.include.keys('key', 'type', 'direction');
});
it('should update pin by associating pinNode', () => {
const patch = Helper.defaultizePatch({
@@ -581,12 +577,13 @@ describe('Patch', () => {
const newPatch = Patch.assocNode(node, patch);
expect(newPatch)
.to.have.property('pins')
.that.have.property('1')
.that.deep.equal({
key: '1',
type: 'number',
direction: 'input',
});
.that.have.property('1');
const newPin = newPatch.pins['1'];
expect(newPin).to.have.property('key', '1');
expect(newPin).to.have.property('type', 'number');
expect(newPin).to.have.property('direction', 'input');
});
});
// TODO: Add test for deleting pinNode

View File

@@ -9,112 +9,6 @@ import * as Helper from './helpers';
chai.use(dirtyChai);
describe('Pin', () => {
// Validations
describe('validatePinType', () => {
it('should return Either.Left for invalid type', () => {
const invalid = Pin.validatePinType('a');
expect(invalid.isLeft).to.be.true();
Helper.expectErrorMessage(expect, invalid, CONST.ERROR.PIN_TYPE_INVALID);
});
it('should return Either.Right for valid type', () => {
const type = CONST.PIN_TYPE.NUMBER;
const valid = Pin.validatePinType(type);
expect(valid.isRight).to.be.true();
Helper.expectEither(
(val) => { expect(val).to.be.equal(type); },
valid
);
});
});
describe('validatePinDirection', () => {
it('should return Either.Left for invalid direction', () => {
const invalid = Pin.validatePinDirection('a');
expect(invalid.isLeft).to.be.true();
Helper.expectErrorMessage(expect, invalid, CONST.ERROR.PIN_DIRECTION_INVALID);
});
it('should return Either.Right for valid direction', () => {
const direction = CONST.PIN_DIRECTION.INPUT;
const valid = Pin.validatePinDirection(direction);
expect(valid.isRight).to.be.true();
Helper.expectEither(
(val) => { expect(val).to.be.equal(direction); },
valid
);
});
});
describe('validatePinKey', () => {
it('should return Either.Left for invalid key', () => {
const pinKey = 'i have $paceZ и немного кириллицы';
const result = Pin.validatePinKey(pinKey);
expect(result.isLeft).to.be.true();
Helper.expectErrorMessage(expect, result, CONST.ERROR.PIN_KEY_INVALID);
});
it('should return Either.Right for valid key', () => {
const pinKey = '123aBc';
const result = Pin.validatePinKey(pinKey);
expect(result.isRight).to.be.true();
Helper.expectEither(
val => expect(val).to.be.equal(pinKey),
result
);
});
});
describe('validatePin', () => {
it('should return Either.Left for empty object', () => {
const invalid = Pin.validatePin({});
expect(invalid.isLeft).to.be.true();
});
it('should return Either.Left for wrong type', () => {
const invalid = Pin.validatePin({ type: 'a' });
expect(invalid.isLeft).to.be.true();
});
it('should return Either.Left for wrong direction', () => {
const invalid = Pin.validatePin({ type: CONST.PIN_TYPE.NUMBER, direction: 'z' });
expect(invalid.isLeft).to.be.true();
});
it('should return Either.Left for wrong key', () => {
const invalid = Pin.validatePin({ type: CONST.PIN_TYPE.NUMBER, direction: CONST.PIN_DIRECTION.INPUT, key: 'неправильный ключ для пина' });
expect(invalid.isLeft).to.be.true();
});
it('should return Either.Right for valid pin', () => {
const valid = Pin.validatePin({
key: 'a',
type: CONST.PIN_TYPE.NUMBER,
direction: CONST.PIN_DIRECTION.INPUT,
});
expect(valid.isRight).to.be.true();
});
});
// constructors
describe('createPin', () => {
it('should return Either.Left for wrong pin type', () => {
const newPin = Pin.createPin('test', 'a', 'i');
expect(newPin.isLeft).to.be.true();
});
it('should return Either.Left for wrong pin direction', () => {
const newPin = Pin.createPin('test', CONST.PIN_TYPE.NUMBER, 'i');
expect(newPin.isLeft).to.be.true();
});
it('should return Either.Right with new Pin', () => {
const key = 'test';
const type = CONST.PIN_TYPE.NUMBER;
const direction = CONST.PIN_DIRECTION.INPUT;
const newPin = Pin.createPin(key, type, direction);
expect(newPin.isRight).to.be.true();
Helper.expectEither(
(pinObj) => {
expect(pinObj)
.to.be.an('object')
.that.deep.equal({
key,
type,
direction,
});
},
newPin
);
});
});
// props required
describe('getPinType', () => {
it('should return pin type', () => {