refactor(xod-project): enforce type checks with def-hm

This commit is contained in:
Victor Nakoryakov
2017-02-10 17:21:01 +03:00
parent 3c8b6e83ab
commit ffd76f9156
7 changed files with 102 additions and 44 deletions

View File

@@ -204,7 +204,7 @@ const convertLinksInPatches = R.curry(
const copyImpls = R.compose(
R.map(R.assoc('impls')),
maybeEmpty,
R.prop('impl')
R.propOr({}, 'impl')
);
// :: ProjectOld -> Maybe Function fn

View File

@@ -369,6 +369,7 @@ const getPinType = R.curry((patchTuples, nodes, idGetter, keyGetter, link) =>
const createCastNode = R.curry((patchTuples, nodes, link) => R.compose(
R.chain(type => ({
id: `${Link.getLinkOutputNodeId(link)}-to-${Link.getLinkInputNodeId(link)}`,
position: { x: 0, y: 0 },
type,
})),
// Link -> Maybe String

View File

@@ -8,6 +8,7 @@ import * as Link from './link';
import * as Pin from './pin';
import * as Utils from './utils';
import { sortGraph } from './gmath';
import { def } from './types';
/**
* An object representing single patch in a project
@@ -26,6 +27,8 @@ import { sortGraph } from './gmath';
export const createPatch = () => ({
nodes: {},
links: {},
impls: {},
pins: {},
});
/**
@@ -382,10 +385,9 @@ export const listLinksByPin = R.curry(
* @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)
export const validateLink = def(
'validateLink :: Link -> Patch -> Either Error Link',
(link, patch) => Either.of(link)
.chain(() => Tools.errOnNothing(
CONST.ERROR.LINK_INPUT_NODE_NOT_FOUND,
getNodeById(Link.getLinkInputNodeId(link), patch)

View File

@@ -1,5 +1,6 @@
import R from 'ramda';
import RF from 'ramda-fantasy';
import $ from 'sanctuary-def';
import HMDef from 'hm-def';
@@ -41,6 +42,20 @@ const AliasType = (typeName, type) => NullaryType(
hasType(type)
);
//-----------------------------------------------------------------------------
//
// Fantasy land types
//
//-----------------------------------------------------------------------------
export const $Either = $.BinaryType(
'ramda-fantasy/Either',
'https://github.com/ramda/ramda-fantasy/blob/master/docs/Either.md',
R.is(RF.Either),
either => either.isLeft ? [either.value] : [],
either => either.isRight ? [either.value] : []
);
//-----------------------------------------------------------------------------
//
// Domain types
@@ -49,11 +64,14 @@ const AliasType = (typeName, type) => NullaryType(
const ObjectWithId = NullaryType('ObjectWithId', R.has('id'));
export const Label = AliasType('Label', $.String);
export const Source = AliasType('Source', $.String);
export const ShortId = AliasType('ShortId', $.String);
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 NodePosition = Model('NodePosition', {
x: $.Number,
@@ -77,6 +95,14 @@ export const Link = Model('Link', {
output: PinRef,
});
export const Patch = Model('Patch', {
nodes: $.StrMap(Node),
links: $.StrMap(Link),
impls: $.StrMap(Source),
pins: $.StrMap(Pin),
//label: Label,
});
export const NodeOrId = OneOfType('NodeOrId', [NodeId, ObjectWithId]);
export const LinkOrId = OneOfType('LinkOrId', [LinkId, ObjectWithId]);
@@ -85,15 +111,18 @@ export const LinkOrId = OneOfType('LinkOrId', [LinkId, ObjectWithId]);
// Environment
//
//-----------------------------------------------------------------------------
export const env = $.env.concat([
$Either,
Link,
PinRef,
PinKey,
NodeId,
LinkId,
ShortId,
NodeOrId,
LinkOrId,
NodeId,
NodeOrId,
Patch,
PinKey,
PinRef,
ShortId,
]);
export const def = HMDef.create({ checkTypes: true, env });

View File

@@ -11,7 +11,7 @@ chai.use(dirtyChai);
describe('Flatten', () => {
describe('trivial', () => {
const project = {
const project = Helper.defaultizeProject({
patches: {
'@/main': {
nodes: {
@@ -63,7 +63,7 @@ describe('Flatten', () => {
},
},
},
};
});
it('should return error if implementation not found', () => {
const flatProject = flatten(project, '@/main', ['cpp']);
@@ -118,7 +118,7 @@ describe('Flatten', () => {
});
describe('recursive', () => {
const project = {
const project = Helper.defaultizeProject({
patches: {
'@/main': {
nodes: {
@@ -270,7 +270,7 @@ describe('Flatten', () => {
},
},
},
};
});
it('should ignore not referred patches', () => {
const flatProject = flatten(project, '@/foo', ['js']);
@@ -374,7 +374,7 @@ describe('Flatten', () => {
};
describe('no links to terminal', () => {
const project = {
const project = Helper.defaultizeProject({
patches: {
'@/main': {
nodes: {
@@ -432,7 +432,7 @@ describe('Flatten', () => {
impls: {},
},
},
};
});
it('should return patches without cast patch', () => {
const flatProject = flatten(project, '@/main', ['js']);
@@ -465,7 +465,7 @@ describe('Flatten', () => {
describe('through output terminal', () => {
const createCastOutputTest = (typeIn, typeOut) => {
it(`${typeIn} -> ${getCastPatchPath(typeIn, typeOut)} -> ${typeOut}`, () => {
const project = {
const project = Helper.defaultizeProject({
patches: {
'@/main': {
nodes: {
@@ -581,7 +581,7 @@ describe('Flatten', () => {
impls: {},
},
},
};
});
if (typeOut === typeIn) {
project.patches[`xod/core/${typeOut}`].pins = {
@@ -622,7 +622,7 @@ describe('Flatten', () => {
describe('through input terminal', () => {
const createCastInputTest = (typeIn, typeOut) => {
it(`${typeIn} -> ${getCastPatchPath(typeIn, typeOut)} -> ${typeOut}`, () => {
const project = {
const project = Helper.defaultizeProject({
patches: {
'@/main': {
nodes: {
@@ -738,7 +738,7 @@ describe('Flatten', () => {
impls: {},
},
},
};
});
if (typeOut === typeIn) {
project.patches[`xod/core/${typeOut}`].pins = {
@@ -788,7 +788,7 @@ describe('Flatten', () => {
describe('three different types', () => {});
describe('with same types', () => {
const project = {
const project = Helper.defaultizeProject({
patches: {
'@/main': {
nodes: {
@@ -878,7 +878,7 @@ describe('Flatten', () => {
},
},
},
};
});
it('should return patches without cast patch', () => {
const flatProject = flatten(project, '@/main', ['js']);
@@ -933,7 +933,7 @@ describe('Flatten', () => {
});
describe('needed, but missing in the project', () => {
const project = {
const project = Helper.defaultizeProject({
patches: {
'@/main': {
nodes: {
@@ -996,7 +996,7 @@ describe('Flatten', () => {
},
},
},
};
});
it(`should return Either.Left with error "${CONST.ERROR.CAST_PATCH_NOT_FOUND}"`, () => {
const flatProject = flatten(project, '@/main', ['js']);
@@ -1008,7 +1008,7 @@ describe('Flatten', () => {
});
describe('injected pins', () => {
const project = {
const project = Helper.defaultizeProject({
patches: {
'@/main': {
nodes: {
@@ -1241,7 +1241,7 @@ describe('Flatten', () => {
},
},
},
};
});
it('should return node b~b with injected pin a2', () => {
const flatProject = flatten(project, '@/main', ['js']);
@@ -1264,7 +1264,7 @@ describe('Flatten', () => {
});
describe('implementations', () => {
const project = {
const project = Helper.defaultizeProject({
patches: {
'@/main': {
nodes: {
@@ -1296,7 +1296,7 @@ describe('Flatten', () => {
},
},
},
};
});
describe('single', () => {
it('defined implementation exists', () => {

View File

@@ -84,3 +84,43 @@ export const expectOptionalNumberSetter = R.curry((expect, method, propName) =>
.to.have.property(propName).to.be.equal(0);
});
});
//-----------------------------------------------------------------------------
// Defaultizers
//-----------------------------------------------------------------------------
export const defaultizeLink = R.merge({
id: '$$defaultLinkId',
input: { nodeId: '$$defaultInputNodeId', pinKey: '$$defaultInputPin' },
output: { nodeId: '$$defaultOutputNodeId', pinKey: '$$defaultOutputPin' },
});
export const defaultizeNode = R.merge({
id: '$$defaultNodeId',
position: { x: 0, y: 0 },
type: '@/defaultType',
});
export const defaultizePatch = R.compose(
R.evolve({
nodes: R.map(defaultizeNode),
links: R.map(defaultizeLink),
impls: R.identity,
pins: R.identity,
}),
R.merge({
nodes: {},
links: {},
impls: {},
pins: {},
})
);
export const defaultizeProject = R.compose(
R.evolve({
patches: R.map(defaultizePatch),
}),
R.merge({
patches: {},
})
);

View File

@@ -737,12 +737,12 @@ describe('Patch', () => {
});
});
describe('validateLink', () => {
const patch = {
const patch = Helper.defaultizePatch({
nodes: {
out: { id: 'out' },
in: { id: 'in' },
},
};
});
const linkId = '1';
const validInput = {
nodeId: 'in',
@@ -753,26 +753,12 @@ describe('Patch', () => {
pinKey: 'out',
};
it('should return Either.Left for link without id', () => {
const err = Patch.validateLink({}, {});
expect(err.isLeft).to.be.true();
});
it('should return Either.Left if input property is not exist or invalid', () => {
const link = { id: linkId };
const err = Patch.validateLink(link, patch);
expect(err.isLeft).to.be.true();
});
it('should return Either.Left for non-existent input node in the patch', () => {
const link = { id: linkId, input: { nodeId: 'non-existent', pinKey: 'a' }, output: validOutput };
const err = Patch.validateLink(link, patch);
expect(err.isLeft).to.be.true();
Helper.expectErrorMessage(expect, err, CONST.ERROR.LINK_INPUT_NODE_NOT_FOUND);
});
it('should return Either.Left if output property is not exist or invalid', () => {
const link = { id: linkId, input: validInput };
const err = Patch.validateLink(link, patch);
expect(err.isLeft).to.be.true();
});
it('should return Either.Left for non-existent output node in the patch', () => {
const link = { id: linkId, input: validInput, output: { nodeId: 'non-existent', pinKey: 'a' } };
const err = Patch.validateLink(link, patch);