diff --git a/packages/xod-project/src/constants.js b/packages/xod-project/src/constants.js index 00665282..b1a68d58 100644 --- a/packages/xod-project/src/constants.js +++ b/packages/xod-project/src/constants.js @@ -31,6 +31,7 @@ export const ERROR = { PIN_KEY_INVALID: 'Pin key should be generated with shortid', // other DATATYPE_INVALID: 'Invalid data type', + IMPLEMENTATION_NOT_FOUND: 'Implementation not found', }; /** diff --git a/packages/xod-project/src/flatten.js b/packages/xod-project/src/flatten.js new file mode 100644 index 00000000..b487f6bf --- /dev/null +++ b/packages/xod-project/src/flatten.js @@ -0,0 +1,77 @@ +import R from 'ramda'; +import { Maybe } from 'ramda-fantasy'; + +import * as Project from './project'; +import * as Patch from './patch'; +import * as Node from './node'; + +// :: Monad a -> Monad a +const reduceChainOver = R.reduce(R.flip(R.chain)); + +// :: String[] -> Project -> Path -> [Path, Patch, ...] +const extractImplPatches = R.curry((impls, project, path, patch) => R.ifElse( + Patch.hasImpl(impls), + patchWithImpl => [path, patchWithImpl], + R.compose( + R.chain(R.compose( + type => Project.getPatchByPath(type, project) + .chain(extractImplPatches(impls, project, type)), + Node.getNodeType + )), + Patch.listNodes + ) +)(patch)); + +// :: Node -> Boolean +const isNodeToImplPatch = implPatchPaths => R.compose( + R.contains(R.__, implPatchPaths), + Node.getNodeType +); + +// :: Project -> String[] -> Patch -> Node[] +const extractNodes = (project, implPatchPaths) => R.compose( + R.chain(R.ifElse( + isNodeToImplPatch(implPatchPaths), + R.identity, + R.compose( + R.chain(patch => extractNodes(project, implPatchPaths)(patch)), + type => Project.getPatchByPath(type, project), + Node.getNodeType + ) + ) + ), + Patch.listNodes +); + +export default R.curry((project, path, impls) => { + const patch = Project.getPatchByPath(path, project); + + const implPatches = patch + .map(extractImplPatches(impls, project, path)) + .chain(R.splitEvery(2)); + const assocImplPatches = implPatches.map(R.apply(Project.assocPatch)); + + let assocPatch = R.identity; + + if (!patch.chain(Patch.hasImpl(impls))) { + const implPatchPaths = R.pluck(0, implPatches); + const nodes = patch.chain(extractNodes(project, implPatchPaths)); + const assocNodes = nodes.map(node => Patch.assocNode(node)); + + const newPatch = R.reduce( + (p, fn) => fn(p), + Patch.createPatch(), + assocNodes + ); + assocPatch = R.compose( + R.chain, + Project.assocPatch(path) + )(newPatch); + } + + return R.compose( + assocPatch, + reduceChainOver(R.__, assocImplPatches), + Maybe.of + )(Project.createProject()); +}); diff --git a/packages/xod-project/src/patch.js b/packages/xod-project/src/patch.js index c32e3d22..cfbc390d 100644 --- a/packages/xod-project/src/patch.js +++ b/packages/xod-project/src/patch.js @@ -51,19 +51,33 @@ export const setPatchLabel = R.useWith( ); /** - * Returns a list of platforms for which a `patch` has native implementation + * Returns a list of implementations for which a `patch` has native implementation * * For example, `['js', 'arduino', 'espruino', 'nodejs']`. * - * @function listPatchPlatforms + * @function listImpls * @param {Patch} patch * @returns {string[]} */ -export const listPatchPlatforms = R.compose( +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 diff --git a/packages/xod-project/src/project.js b/packages/xod-project/src/project.js index 3b9a8a5c..f7fee890 100644 --- a/packages/xod-project/src/project.js +++ b/packages/xod-project/src/project.js @@ -141,6 +141,16 @@ export const listPatches = R.compose( getPatches ); +/** + * @function listPatchPaths + * @param {Project} project - project bundle + * @returns {String[]} list of all patche paths not sorted in any arbitrary order + */ +export const listPatchPaths = R.compose( + R.keys, + getPatches +); + /** * Return a list of local patches (excluding external libraries) * diff --git a/packages/xod-project/test/fixtures/blinkFlat.js b/packages/xod-project/test/fixtures/blinkFlat.js new file mode 100644 index 00000000..86baa938 --- /dev/null +++ b/packages/xod-project/test/fixtures/blinkFlat.js @@ -0,0 +1,134 @@ +const projectB = { + patches: { + '@/main': { + nodes: { + // blink + 'a/a': { + id: 'a/a', + type: 'xod/core/clock', + pins: { + interval: { + injected: true, + value: 0.2, + }, + }, + }, + 'a/b': { + id: 'a/b', + type: 'xod/core/latch', + }, + 'a/generatedNodeId__out': { + id: 'a/generatedNodeId__out', + type: 'xod/core/outputNumber', + }, + // led + 'b/a': { + id: 'b/a', + type: 'xod/core/multiply', + }, + 'b/b': { + id: 'b/b', + type: 'xod/core/pwm-output', + pins: { + 'hardware-pin': { + injected: true, + value: 13, + }, + }, + }, + }, + links: { + // blink + 'a/b': { + id: 'a/b', + output: { + nodeId: 'a/a', + pinKey: 'tick', + }, + input: { + nodeId: 'a/b', + pinKey: 'toggle', + }, + }, + // blink to led + 'a-b/c': { + id: 'a-b/c', + output: { + nodeId: 'a/b', + pinKey: 'out', + }, + input: { + nodeId: 'b/a', + pinKey: 'in1', + }, + }, + 'a-b/d': { + id: 'a-b/d', + output: { + nodeId: 'a/b', + pinKey: 'out', + }, + input: { + nodeId: 'b/a', + pinKey: 'in2', + }, + }, + // led + 'b/c': { + id: 'b/c', + output: { + nodeId: 'b/a', + pinKey: 'out', + }, + input: { + nodeId: 'b/b', + pinKey: 'duty', + }, + }, + }, + }, + 'xod/core/multiply': { + nodes: {}, + links: {}, + pins: { + in1: { + key: 'in1', + type: 'number', + direction: 'input', + }, + in2: { + key: 'in2', + type: 'number', + direction: 'input', + }, + out: { + key: 'out', + type: 'number', + direction: 'output', + }, + }, + impls: { + js: '//ok', + }, + }, + 'xod/core/pwm-output': { + nodes: {}, + links: {}, + pins: { + duty: { + key: 'duty', + type: 'number', + direction: 'input', + }, + 'hardware-pin': { + key: 'hardware-pin', + type: 'number', + direction: 'input', + }, + }, + impls: { + js: '//ok', + }, + }, + }, +}; diff --git a/packages/xod-project/test/fixtures/blinkOriginal.js b/packages/xod-project/test/fixtures/blinkOriginal.js new file mode 100644 index 00000000..0be923b9 --- /dev/null +++ b/packages/xod-project/test/fixtures/blinkOriginal.js @@ -0,0 +1,244 @@ +const projectA = { + patches: { + '@/main': { + nodes: { + a: { + id: 'a', + type: '@/blink', + pins: { + generatedNodeId__interval: { + injected: true, + value: 0.2, + }, + }, + }, + b: { + id: 'b', + type: 'xod/core/led', + pins: { + generatedNodeId__hardwarePin: { + injected: true, + value: 13, + }, + }, + }, + }, + links: { + a: { + id: 'a', + output: { + nodeId: 'a', + pinKey: 'generatedNodeId__out', + }, + input: { + nodeId: 'b', + pinKey: 'generatedNodeId__interval', + }, + }, + }, + }, + '@/blink': { + nodes: { + generatedNodeId__interval: { + id: 'generatedNodeId__interval', + type: 'xod/core/inputNumber', + }, + a: { + id: 'a', + type: 'xod/core/clock', + }, + b: { + id: 'b', + type: 'xod/core/latch', + }, + generatedNodeId__out: { + id: 'generatedNodeId__out', + type: 'xod/core/outputNumber', + }, + }, + links: { + a: { + id: 'a', + output: { + nodeId: 'generatedNodeId__interval', + pinKey: 'out', + }, + input: { + nodeId: 'a', + pinKey: 'interval', + }, + }, + b: { + id: 'b', + output: { + nodeId: 'a', + pinKey: 'tick', + }, + input: { + nodeId: 'b', + pinKey: 'toggle', + }, + }, + c: { + id: 'c', + output: { + nodeId: 'b', + pinKey: 'out', + }, + input: { + nodeId: 'generatedNodeId__out', + pinKey: 'in', + }, + }, + }, + pins: { + generatedNodeId__interval: { + key: 'generatedNodeId__interval', + type: 'number', + direction: 'input', + }, + generatedNodeId__out: { + key: 'generatedNodeId__out', + type: 'number', + direction: 'output', + }, + }, + }, + 'xod/core/led': { + nodes: { + generatedNodeId__brightness: { + id: 'generatedNodeId__brightness', + type: 'xod/core/inputNumber', + }, + generatedNodeId__hardwarePin: { + id: 'generatedNodeId__hardwarePin', + type: 'xod/core/inputNumber', + }, + a: { + id: 'a', + type: 'xod/core/multiply', + }, + b: { + id: 'b', + type: 'xod/core/pwm-output', + }, + }, + links: { + a: { + id: 'a', + output: { + nodeId: 'generatedNodeId__brightness', + pinKey: 'out', + }, + input: { + nodeId: 'a', + pinKey: 'in1', + }, + }, + b: { + id: 'b', + output: { + nodeId: 'generatedNodeId__brightness', + pinKey: 'out', + }, + input: { + nodeId: 'a', + pinKey: 'in2', + }, + }, + c: { + id: 'c', + output: { + nodeId: 'a', + pinKey: 'out', + }, + input: { + nodeId: 'b', + pinKey: 'duty', + }, + }, + d: { + id: 'd', + output: { + nodeId: 'generatedNodeId__hardwarePin', + pinKey: 'out', + }, + input: { + nodeId: 'b', + pinKey: 'hardware-pin', + }, + }, + }, + pins: { + generatedNodeId__brightness: { + key: 'generatedNodeId__brightness', + type: 'number', + direction: 'input', + }, + generatedNodeId__hardwarePin: { + key: 'generatedNodeId__hardwarePin', + type: 'string', + direction: 'input', + }, + }, + }, + 'xod/core/inputNumber': { + nodes: {}, + links: {}, + pins: { + out: { + key: 'out', + type: 'number', + direction: 'output', + }, + }, + }, + 'xod/core/multiply': { + nodes: {}, + links: {}, + pins: { + in1: { + key: 'in1', + type: 'number', + direction: 'input', + }, + in2: { + key: 'in2', + type: 'number', + direction: 'input', + }, + out: { + key: 'out', + type: 'number', + direction: 'output', + }, + }, + }, + 'xod/core/pwm-output': { + nodes: {}, + links: {}, + pins: { + duty: { + key: 'duty', + type: 'number', + direction: 'input', + }, + 'hardware-pin': { + key: 'hardware-pin', + type: 'number', + direction: 'input', + }, + }, + impls: { + js: '//ok', + }, + }, + 'xod/core/or': { + nodes: {}, + links: {}, + impls: { + js: '//ok', + }, + }, + }, +}; diff --git a/packages/xod-project/test/fixtures/bundle.v2.json b/packages/xod-project/test/fixtures/bundle.v2.json index 4d7dd003..f261b32d 100644 --- a/packages/xod-project/test/fixtures/bundle.v2.json +++ b/packages/xod-project/test/fixtures/bundle.v2.json @@ -7,7 +7,7 @@ "patches": { "xod/core/inputNumber": { "nodes": {}, - "links": [], + "links": {}, "label": "", "pins": { "PIN": { @@ -19,7 +19,7 @@ }, "xod/core/pot": { "nodes": {}, - "links": [], + "links": {}, "pins": { "sample": { "key": "sample", @@ -38,7 +38,7 @@ }, "xod/core/and": { "nodes": {}, - "links": [], + "links": {}, "label": "and", "pins": { "a": { @@ -63,7 +63,7 @@ }, "xod/core/led": { "nodes": {}, - "links": [], + "links": {}, "label": "LED", "pins": { "brightness": { diff --git a/packages/xod-project/test/flatten.spec.js b/packages/xod-project/test/flatten.spec.js new file mode 100644 index 00000000..bb4bcd3b --- /dev/null +++ b/packages/xod-project/test/flatten.spec.js @@ -0,0 +1,138 @@ +import R from 'ramda'; +import chai, { expect } from 'chai'; +import dirtyChai from 'dirty-chai'; + +import * as Helper from './helpers'; +import * as CONST from '../src/constants'; +import flatten from '../src/flatten'; + +chai.use(dirtyChai); + +describe('Flatten', () => { + describe('trivial', () => { + const project = { + patches: { + '@/main': { + nodes: { + a: { + id: 'a', + type: 'xod/core/or', + }, + }, + links: {}, + }, + 'xod/core/or': { + nodes: {}, + links: {}, + impls: { + js: '//ok', + }, + }, + }, + }; + + it('should ignore not referred patches', () => { + const flatProject = flatten(project, 'xod/core/or', ['js']); + + expect(flatProject.isRight).to.be.true(); + Helper.expectEither( + (newProject) => { + expect(R.keys(newProject.patches)) + .to.be.deep.equal(['xod/core/or']); + expect(newProject.patches['xod/core/or']) + .to.be.deep.equal(project.patches['xod/core/or']); + }, + flatProject + ); + }); + + it('should return patch with dependency', () => { + const flatProject = flatten(project, '@/main', ['js']); + + expect(flatProject.isRight).to.be.true(); + Helper.expectEither( + (newProject) => { + expect(R.keys(newProject.patches)) + .to.be.deep.equal(['xod/core/or', '@/main']); + expect(newProject.patches) + .to.be.deep.equal(project.patches); + }, + flatProject + ); + }); + + // its('should return error if implementation not found', () => { + // const flatProject = flatten(project, '@/main', ['cpp']); + // + // expect(flatProject.isLeft).to.be.true(); + // Helper.expectErrorMessage(expect, flatProject, CONST.ERROR.IMPLEMENTATION_NOT_FOUND); + // }); + }); + + describe('recursive', () => { + const project = { + patches: { + '@/main': { + nodes: { + a: { + id: 'a', + type: '@/foo', + }, + }, + links: {}, + }, + '@/foo': { + nodes: { + a: { + id: 'a', + type: 'xod/core/or', + }, + }, + links: {}, + }, + 'xod/core/or': { + nodes: {}, + links: {}, + impls: { + js: '//ok', + }, + }, + }, + }; + + it('should ignore not referred patches', () => { + const flatProject = flatten(project, '@/foo', ['js']); + + expect(flatProject.isRight).to.be.true(); + Helper.expectEither( + (newProject) => { + expect(R.keys(newProject.patches)) + .to.be.deep.equal(['xod/core/or', '@/foo']); + expect(newProject.patches['xod/core/or']) + .to.be.deep.equal(project.patches['xod/core/or']); + expect(newProject.patches['@/foo']) + .to.be.deep.equal(project.patches['@/foo']); + }, + flatProject + ); + }); + + it('should return patch with dependency', () => { + const flatProject = flatten(project, '@/main', ['js']); + + expect(flatProject.isRight).to.be.true(); + Helper.expectEither( + (newProject) => { + expect(R.keys(newProject.patches)) + .to.be.deep.equal(['xod/core/or', '@/main']); + expect(newProject.patches['xod/core/or']) + .to.be.deep.equal(project.patches['xod/core/or']); + expect(R.values(newProject.patches['@/main'].nodes)[0]) + .to.have.property('type') + .that.equals('xod/core/or'); + }, + flatProject + ); + }); + }); +}); diff --git a/packages/xod-project/test/patch.spec.js b/packages/xod-project/test/patch.spec.js index bce61de9..5d7403d8 100644 --- a/packages/xod-project/test/patch.spec.js +++ b/packages/xod-project/test/patch.spec.js @@ -89,9 +89,9 @@ describe('Patch', () => { .that.equals('[object Object]'); }); }); - describe('listPatchPlatforms', () => { + describe('listImpls', () => { it('should return empty array for empty patch', () => { - expect(Patch.listPatchPlatforms({})) + expect(Patch.listImpls({})) .to.be.instanceof(Array) .to.be.empty(); }); @@ -102,11 +102,41 @@ describe('Patch', () => { espruino: '', }, }; - expect(Patch.listPatchPlatforms(patch)) + expect(Patch.listImpls(patch)) .to.be.an('array') .to.have.members(['js', 'espruino']); }); }); + describe('hasImpl', () => { + it('should return false for empty', () => { + expect(Patch.hasImpl(['js'], {})).to.be.false(); + }); + it('should return false if impl not found', () => { + const patch = { + impls: { + js: '//ok', + }, + }; + expect(Patch.hasImpl(['cpp'], patch)).to.be.false(); + }); + it('should return true for the only correct impl', () => { + const patch = { + impls: { + js: '//ok', + }, + }; + expect(Patch.hasImpl(['js'], patch)).to.be.true(); + }); + it('should return true for a few existent impls', () => { + const patch = { + impls: { + js: '//ok', + nodejs: '//ok', + }, + }; + expect(Patch.hasImpl(['js', 'nodejs'], patch)).to.be.true(); + }); + }); // entity getters describe('listNodes', () => { diff --git a/packages/xod-project/test/project.spec.js b/packages/xod-project/test/project.spec.js index e84559df..b5a0ca4a 100644 --- a/packages/xod-project/test/project.spec.js +++ b/packages/xod-project/test/project.spec.js @@ -446,6 +446,17 @@ describe('Project', () => { .and.have.lengthOf(2); }); }); + describe('listPatchPaths', () => { + it('should return empty array for empty project', () => { + expect(Project.listPatchPaths({})) + .to.be.instanceof(Array) + .and.to.be.empty(); + }); + it('should return array with two keys', () => { + expect(Project.listPatchPaths(project)) + .to.be.deep.equal(['@/test', 'external/patch']); + }); + }); describe('listLocalPatches', () => { it('should return empty array for empty project', () => { expect(Project.listLocalPatches({}))