diff --git a/README.md b/README.md index 1bebcac..cab1d95 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Convert a [xstate](https://github.com/davidkpiano/xstate) or [react-automata](https://github.com/MicheleBertoli/react-automata) statechart to a [plantuml](http://plantuml.com/state-diagram) state diagram ## Installation + ``` npm install xstate-plantuml ``` @@ -17,11 +18,6 @@ const config = { initial: 'green', states: { green: { - on: { - TIMER: 'yellow' - } - }, - yellow: { on: { TIMER: 'red' } @@ -34,24 +30,21 @@ const config = { } }; -console.log(convert(config)); +convert(config, options); ``` -## Supports - -- [x] Single machine -- [x] Parallel machine -- [x] Hierarchical machine -- [ ] History machine -- [x] Initial event -- [x] Internal string events `{on: {x: '.y'}}` -- [x] Internal obj events `{on {x: {target: 'y'}}}` -- [x] String event `{on: {x: 'y'}}` -- [x] Object event `{on: {x: {y: {}}}}` -- [x] Array event `{on: {x: [{target: 'y'}]}}` -- [x] String action `fetch` -- [ ] Object action `{ type: 'fetch'}` -- [ ] Function action -- [x] `onEntry` actions -- [x] `onExit` actions -- [ ] `transition` actions +## Examples + +### Hierarchical machine + +- [json](./examples/alarm.json) +- [puml](./examples/alarm.puml) + +![alarm](./examples/alarm.png) + +### Parallel machine + +- [json](./examples/parallel.json) +- [puml](./examples/parallel.puml) + +![parallel](./examples/parallel.png) diff --git a/examples/alarm.json b/examples/alarm.json new file mode 100644 index 0000000..85295a2 --- /dev/null +++ b/examples/alarm.json @@ -0,0 +1,17 @@ +{ + "key": "alarm", + "initial": "inactive", + "states": { + "inactive": { "on": { "ENABLE": "active.waiting" } }, + "active": { + "states": { + "snoozing": { "on": { "TIMER": "beeping" } }, + "waiting": { "on": { "TIMER": "beeping" } }, + "beeping": { "on": { "SNOOZE": "snoozing" } } + }, + "on": { + "DISABLE": "inactive" + } + } + } +} diff --git a/examples/alarm.png b/examples/alarm.png new file mode 100644 index 0000000..803b81e Binary files /dev/null and b/examples/alarm.png differ diff --git a/examples/alarm.puml b/examples/alarm.puml new file mode 100644 index 0000000..01ac089 --- /dev/null +++ b/examples/alarm.puml @@ -0,0 +1,25 @@ +@startuml +left to right direction +state "alarm" as alarm { + [*] --> alarm.inactive + + state "inactive" as alarm.inactive { + alarm.inactive --> alarm.active.waiting : ENABLE + } + + state "active" as alarm.active { + alarm.active --> alarm.inactive : DISABLE + state "snoozing" as alarm.active.snoozing { + alarm.active.snoozing --> alarm.active.beeping : TIMER + } + + state "waiting" as alarm.active.waiting { + alarm.active.waiting --> alarm.active.beeping : TIMER + } + + state "beeping" as alarm.active.beeping { + alarm.active.beeping --> alarm.active.snoozing : SNOOZE + } + } +} +@enduml diff --git a/examples/hierarchical.js b/examples/hierarchical.js deleted file mode 100644 index 8315b1f..0000000 --- a/examples/hierarchical.js +++ /dev/null @@ -1,44 +0,0 @@ -const fs = require('fs'); -const convert = require('../src/index').default; - -const pedestrianStates = { - initial: 'walk', - states: { - walk: { - on: { - PED_TIMER: 'wait' - } - }, - wait: { - on: { - PED_TIMER: 'stop' - } - }, - stop: {} - } -}; - -const lightMachine = { - key: 'light', - initial: 'green', - states: { - green: { - on: { - TIMER: 'yellow' - } - }, - yellow: { - on: { - TIMER: 'red' - } - }, - red: { - on: { - TIMER: 'green' - }, - ...pedestrianStates - } - } -}; - -fs.writeFileSync('examples/hierarchical.puml', convert(lightMachine)); diff --git a/examples/hierarchical.png b/examples/hierarchical.png deleted file mode 100644 index 18d93dd..0000000 Binary files a/examples/hierarchical.png and /dev/null differ diff --git a/examples/hierarchical.puml b/examples/hierarchical.puml deleted file mode 100644 index f205ee8..0000000 --- a/examples/hierarchical.puml +++ /dev/null @@ -1,18 +0,0 @@ -@startuml - state "light" as light { - state "green" as light_green - state "yellow" as light_yellow - [*] --> light_green - light_green --> light_yellow : TIMER - light_yellow --> light_red : TIMER - light_red --> light_green : TIMER - state "red" as light_red { - state "walk" as red_walk - state "wait" as red_wait - state "stop" as red_stop - [*] --> red_walk - red_walk --> red_wait : PED_TIMER - red_wait --> red_stop : PED_TIMER - } - } -@enduml diff --git a/examples/parallel.js b/examples/parallel.js deleted file mode 100644 index ffc70f5..0000000 --- a/examples/parallel.js +++ /dev/null @@ -1,57 +0,0 @@ -const fs = require('fs'); -const convert = require('../src/index').default; - -const wordMachine = { - parallel: true, - states: { - bold: { - initial: 'off', - states: { - on: { - on: { TOGGLE_BOLD: 'off' } - }, - off: { - on: { TOGGLE_BOLD: 'on' } - } - } - }, - underline: { - initial: 'off', - states: { - on: { - on: { TOGGLE_UNDERLINE: 'off' } - }, - off: { - on: { TOGGLE_UNDERLINE: 'on' } - } - } - }, - italics: { - initial: 'off', - states: { - on: { - on: { TOGGLE_ITALICS: 'off' } - }, - off: { - on: { TOGGLE_ITALICS: 'on' } - } - } - }, - list: { - initial: 'none', - states: { - none: { - on: { BULLETS: 'bullets', NUMBERS: 'numbers' } - }, - bullets: { - on: { NONE: 'none', NUMBERS: 'numbers' } - }, - numbers: { - on: { BULLETS: 'bullets', NONE: 'none' } - } - } - } - } -}; - -fs.writeFileSync('examples/parallel.puml', convert(wordMachine)); diff --git a/examples/parallel.json b/examples/parallel.json new file mode 100644 index 0000000..e9dfcce --- /dev/null +++ b/examples/parallel.json @@ -0,0 +1,37 @@ +{ + "key": "wordmachine", + "parallel": true, + "states": { + "bold": { + "initial": "off", + "states": { + "on": { "on": { "TOGGLE_BOLD": "off" } }, + "off": { "on": { "TOGGLE_BOLD": "on" } } + } + }, + "underline": { + "initial": "off", + "states": { + "on": { "on": { "TOGGLE_UNDERLINE": "off" } }, + "off": { "on": { "TOGGLE_UNDERLINE": "on" } } + } + }, + "italics": { + "initial": "off", + "states": { + "on": { "on": { "TOGGLE_ITALICS": "off" } }, + "off": { "on": { "TOGGLE_ITALICS": "on" } } + } + }, + "list": { + "initial": "none", + "states": { + "none": { + "on": { "BULLETS": "bullets", "NUMBERS": "numbers" } + }, + "bullets": { "on": { "NONE": "none", "NUMBERS": "numbers" } }, + "numbers": { "on": { "BULLETS": "bullets", "NONE": "none" } } + } + } + } +} diff --git a/examples/parallel.png b/examples/parallel.png index ff529db..234502f 100644 Binary files a/examples/parallel.png and b/examples/parallel.png differ diff --git a/examples/parallel.puml b/examples/parallel.puml index d3a870e..35ab413 100644 --- a/examples/parallel.puml +++ b/examples/parallel.puml @@ -1,35 +1,59 @@ @startuml - state "bold" as bold { - state "on" as bold_on - state "off" as bold_off - [*] --> bold_off - bold_on --> bold_off : TOGGLE_BOLD - bold_off --> bold_on : TOGGLE_BOLD - } - state "underline" as underline { - state "on" as underline_on - state "off" as underline_off - [*] --> underline_off - underline_on --> underline_off : TOGGLE_UNDERLINE - underline_off --> underline_on : TOGGLE_UNDERLINE - } - state "italics" as italics { - state "on" as italics_on - state "off" as italics_off - [*] --> italics_off - italics_on --> italics_off : TOGGLE_ITALICS - italics_off --> italics_on : TOGGLE_ITALICS - } - state "list" as list { - state "none" as list_none - state "bullets" as list_bullets - state "numbers" as list_numbers - [*] --> list_none - list_none --> list_bullets : BULLETS - list_none --> list_numbers : NUMBERS - list_bullets --> list_none : NONE - list_bullets --> list_numbers : NUMBERS - list_numbers --> list_bullets : BULLETS - list_numbers --> list_none : NONE +left to right direction +state "wordmachine" as wordmachine { + state "bold" as wordmachine.bold { + [*] --> wordmachine.bold.off + + state "on" as wordmachine.bold.on { + wordmachine.bold.on --> wordmachine.bold.off : TOGGLE_BOLD } + + state "off" as wordmachine.bold.off { + wordmachine.bold.off --> wordmachine.bold.on : TOGGLE_BOLD + } + } + + state "underline" as wordmachine.underline { + [*] --> wordmachine.underline.off + + state "on" as wordmachine.underline.on { + wordmachine.underline.on --> wordmachine.underline.off : TOGGLE_UNDERLINE + } + + state "off" as wordmachine.underline.off { + wordmachine.underline.off --> wordmachine.underline.on : TOGGLE_UNDERLINE + } + } + + state "italics" as wordmachine.italics { + [*] --> wordmachine.italics.off + + state "on" as wordmachine.italics.on { + wordmachine.italics.on --> wordmachine.italics.off : TOGGLE_ITALICS + } + + state "off" as wordmachine.italics.off { + wordmachine.italics.off --> wordmachine.italics.on : TOGGLE_ITALICS + } + } + + state "list" as wordmachine.list { + [*] --> wordmachine.list.none + + state "none" as wordmachine.list.none { + wordmachine.list.none --> wordmachine.list.bullets : BULLETS + wordmachine.list.none --> wordmachine.list.numbers : NUMBERS + } + + state "bullets" as wordmachine.list.bullets { + wordmachine.list.bullets --> wordmachine.list.none : NONE + wordmachine.list.bullets --> wordmachine.list.numbers : NUMBERS + } + + state "numbers" as wordmachine.list.numbers { + wordmachine.list.numbers --> wordmachine.list.bullets : BULLETS + wordmachine.list.numbers --> wordmachine.list.none : NONE + } + } +} @enduml diff --git a/examples/simple.js b/examples/simple.js deleted file mode 100644 index 19c85c8..0000000 --- a/examples/simple.js +++ /dev/null @@ -1,26 +0,0 @@ -const fs = require('fs'); -const convert = require('../src/index').default; - -const lightMachine = { - key: 'light', - initial: 'green', - states: { - green: { - on: { - TIMER: 'yellow' - } - }, - yellow: { - on: { - TIMER: 'red' - } - }, - red: { - on: { - TIMER: 'green' - } - } - } -}; - -fs.writeFileSync('examples/simple.puml', convert(lightMachine)); diff --git a/examples/simple.png b/examples/simple.png deleted file mode 100644 index 3ccc044..0000000 Binary files a/examples/simple.png and /dev/null differ diff --git a/examples/simple.puml b/examples/simple.puml deleted file mode 100644 index e0dae63..0000000 --- a/examples/simple.puml +++ /dev/null @@ -1,11 +0,0 @@ -@startuml - state "light" as light { - state "green" as light_green - state "yellow" as light_yellow - state "red" as light_red - [*] --> light_green - light_green --> light_yellow : TIMER - light_yellow --> light_red : TIMER - light_red --> light_green : TIMER - } -@enduml diff --git a/package.json b/package.json index b53b81f..04137c6 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,13 @@ { "name": "xstate-plantuml", - "version": "0.1.1", - "description": - "Convert a xstate or react-automata statechart to a plantuml state diagram", - "keywords": ["xstate", "react-automata", "plantuml", "statecharts"], + "version": "0.2.0", + "description": "Convert a xstate or react-automata statechart to a plantuml state diagram", + "keywords": [ + "xstate", + "react-automata", + "plantuml", + "statecharts" + ], "main": "src/index.js", "license": "MIT", "repository": { @@ -13,10 +17,11 @@ "scripts": { "test": "yarn jest" }, - "dependencies": { - "lodash": "^4.17.10" - }, "devDependencies": { - "jest": "^23.5.0" + "jest": "^23.5.0", + "xstate": "^3.3.3" + }, + "peerDependencies": { + "xstate": "^3.3.3" } } diff --git a/src/__tests__/__snapshots__/formatter.js.snap b/src/__tests__/__snapshots__/formatter.js.snap deleted file mode 100644 index 67403ac..0000000 --- a/src/__tests__/__snapshots__/formatter.js.snap +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`formatter nested machine 1`] = ` -"@startuml - state \\"name\\" as key { - machine : onEntry/a1 - state \\"on\\" as users-crud_on - state \\"off\\" as users-crud_off - [*] --> users-crud_on - users-crud_on --> users-crud_off : TOGGLE - users-crud_off --> users-crud_on : TOGGLE - state \\"name\\" as key { - machine : onEntry/a1 - state \\"on\\" as users-crud_on - state \\"off\\" as users-crud_off - [*] --> users-crud_on - users-crud_on --> users-crud_off : TOGGLE - users-crud_off --> users-crud_on : TOGGLE - } - } -@enduml -" -`; - -exports[`formatter simple machine 1`] = ` -"@startuml - state \\"name\\" as key { - machine : onEntry/a1 - state \\"on\\" as users-crud_on - state \\"off\\" as users-crud_off - [*] --> users-crud_on - users-crud_on --> users-crud_off : TOGGLE - users-crud_off --> users-crud_on : TOGGLE - } -@enduml -" -`; diff --git a/src/__tests__/__snapshots__/parser.js.snap b/src/__tests__/__snapshots__/parser.js.snap deleted file mode 100644 index 760826a..0000000 --- a/src/__tests__/__snapshots__/parser.js.snap +++ /dev/null @@ -1,168 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`root hierachical machine 1`] = ` -Array [ - "root", - Object {}, - Array [ - "machine", - Object { - "key": "light", - "name": "light", - }, - Array [ - "state", - Array [ - "green", - "light_green", - ], - ], - Array [ - "event", - Array [ - "[*]", - "light_red", - ], - ], - Array [ - "event", - Array [ - "light_red", - "light_green", - "TOGGLE", - ], - ], - Array [ - "event", - Array [ - "light_green", - "light_red", - "TOGGLE", - ], - ], - Array [ - "machine", - Object { - "key": "light_red", - "name": "red", - }, - Array [ - "state", - Array [ - "nested", - "red_nested", - ], - ], - Array [ - "event", - Array [ - "[*]", - "red_nested", - ], - ], - ], - ], -] -`; - -exports[`root parallel machine 1`] = ` -Array [ - "root", - Object {}, - Array [ - "machine", - Object { - "key": "light", - "name": "light", - }, - Array [ - "state", - Array [ - "on", - "light_on", - ], - ], - Array [ - "state", - Array [ - "off", - "light_off", - ], - ], - Array [ - "event", - Array [ - "[*]", - "light_on", - ], - ], - Array [ - "event", - Array [ - "light_on", - "light_off", - "TOGGLE", - ], - ], - Array [ - "event", - Array [ - "light_off", - "light_on", - "TOGGLE", - ], - ], - ], -] -`; - -exports[`root simple machine 1`] = ` -Array [ - "root", - Object {}, - Array [ - "machine", - Object { - "key": "users-crud", - "name": "users-crud", - }, - Array [ - "state", - Array [ - "on", - "users-crud_on", - ], - ], - Array [ - "state", - Array [ - "off", - "users-crud_off", - ], - ], - Array [ - "event", - Array [ - "[*]", - "users-crud_on", - ], - ], - Array [ - "event", - Array [ - "users-crud_on", - "users-crud_off", - "TOGGLE", - ], - ], - Array [ - "event", - Array [ - "users-crud_off", - "users-crud_on", - "TOGGLE", - ], - ], - ], -] -`; diff --git a/src/__tests__/core.js b/src/__tests__/core.js new file mode 100644 index 0000000..97ab916 --- /dev/null +++ b/src/__tests__/core.js @@ -0,0 +1,19 @@ +const fs = require('fs'); +const path = require('path'); + +const convert = require('../core'); + +const example = (name, opts) => { + const root = `${__dirname}/../../examples`; + const json = require(`${root}/${name}.json`); + const puml = fs.readFileSync(`${root}/${name}.puml`, 'utf8'); + + test(name, () => { + expect(convert(json, opts)).toEqual(puml); + }); +}; + +describe('xstate-plantuml core examples', () => { + example('alarm'); + example('parallel'); +}); diff --git a/src/__tests__/formatter.js b/src/__tests__/formatter.js deleted file mode 100644 index f83ecef..0000000 --- a/src/__tests__/formatter.js +++ /dev/null @@ -1,22 +0,0 @@ -const format = require('../formatter').default; - -const machine = [ - 'machine', - { key: 'key', name: 'name' }, - ['action', ['machine', 'onEntry', 'a1']], - ['state', ['on', 'users-crud_on']], - ['state', ['off', 'users-crud_off']], - ['event', ['[*]', 'users-crud_on']], - ['event', ['users-crud_on', 'users-crud_off', 'TOGGLE']], - ['event', ['users-crud_off', 'users-crud_on', 'TOGGLE']] -]; - -describe('formatter', () => { - test('simple machine', () => { - expect(format(['root', {}, machine])).toMatchSnapshot(); - }); - - test('nested machine', () => { - expect(format(['root', {}, [...machine, machine]])).toMatchSnapshot(); - }); -}); diff --git a/src/__tests__/parser.js b/src/__tests__/parser.js deleted file mode 100644 index 13a422c..0000000 --- a/src/__tests__/parser.js +++ /dev/null @@ -1,196 +0,0 @@ -const parser = require('../parser'); - -describe('parse state', () => { - test('none', () => { - const config = { states: {} }; - expect(parser.state('pre', config)).toEqual([]); - }); - - test('single', () => { - const config = { states: { x: {} } }; - expect(parser.state('pre', config)).toEqual([['state', ['x', 'pre_x']]]); - }); - - test('multiple', () => { - const config = { states: { x: {}, y: {} } }; - expect(parser.state('pre', config)).toEqual([ - ['state', ['x', 'pre_x']], - ['state', ['y', 'pre_y']] - ]); - }); -}); - -describe('parse events', () => { - test('none', () => { - const config = { states: {} }; - expect(parser.events('pre', config)).toEqual([]); - }); - - test('string', () => { - const config = { states: { x: { on: { EV: 'y' } } } }; - expect(parser.events('pre', config)).toEqual([ - ['event', ['pre_x', 'pre_y', 'EV']] - ]); - }); - - test('object', () => { - const config = { states: { x: { on: { EV: { y: {} } } } } }; - expect(parser.events('pre', config)).toEqual([ - ['event', ['pre_x', 'pre_y', 'EV']] - ]); - }); - - test('array', () => { - const config = { states: { x: { on: { EV: [{ target: 'y' }] } } } }; - expect(parser.events('pre', config)).toEqual([ - ['event', ['pre_x', 'pre_y', 'EV']] - ]); - }); - - test('initial', () => { - const config = { initial: 'x', states: { x: {} } }; - expect(parser.events('pre', config)).toEqual([['event', ['[*]', 'pre_x']]]); - }); - - test('internal', () => { - const config = { - states: { x: {}, y: {} }, - on: { EV1: '.x', EV2: { target: 'y', internal: true } } - }; - - expect(parser.events('pre', config)).toEqual([ - ['event', ['pre_x', 'pre_x', 'EV1']], - ['event', ['pre_x', 'pre_y', 'EV2']], - ['event', ['pre_y', 'pre_x', 'EV1']], - ['event', ['pre_y', 'pre_y', 'EV2']] - ]); - }); - - test('complex', () => { - const config = { - initial: 'x', - states: { x: { on: { EV3: 'y' } }, y: {} }, - on: { EV1: '.x', EV2: '.y' } - }; - - expect(parser.events('pre', config)).toEqual([ - ['event', ['[*]', 'pre_x']], - ['event', ['pre_x', 'pre_y', 'EV3']], - ['event', ['pre_x', 'pre_x', 'EV1']], - ['event', ['pre_x', 'pre_y', 'EV2']], - ['event', ['pre_y', 'pre_x', 'EV1']], - ['event', ['pre_y', 'pre_y', 'EV2']] - ]); - }); -}); - -describe('actions', () => { - test('none', () => { - const config = { key: 'machine', states: {} }; - expect(parser.actions('pre', config)).toEqual([]); - }); - - test('onEntry', () => { - const config = { key: 'machine', states: { x: { onEntry: 'action' } } }; - expect(parser.actions('pre', config)).toEqual([ - ['action', ['machine', 'onEntry', 'action']] - ]); - }); - - test('onExit', () => { - const config = { key: 'machine', states: { x: { onExit: 'action' } } }; - expect(parser.actions('pre', config)).toEqual([ - ['action', ['machine', 'onExit', 'action']] - ]); - }); - - test('multiple', () => { - const config = { - key: 'machine', - states: { x: { onExit: 'action', onEntry: 'action' } } - }; - expect(parser.actions('pre', config)).toEqual([ - ['action', ['machine', 'onEntry', 'action']], - ['action', ['machine', 'onExit', 'action']] - ]); - }); -}); - -describe('root', () => { - test('simple machine', () => { - const config = { - key: 'users-crud', - initial: 'on', - states: { - on: { - on: { - TOGGLE: 'off' - } - }, - off: { - on: { - TOGGLE: 'on' - } - } - } - }; - - expect(parser.root(config)).toMatchSnapshot(); - }); - - test('parallel machine', () => { - const config = { - key: 'users-crud', - parallel: true, - states: { - light: { - initial: 'on', - states: { - on: { - on: { - TOGGLE: 'off' - } - }, - off: { - on: { - TOGGLE: 'on' - } - } - } - } - } - }; - - expect(parser.root(config)).toMatchSnapshot(); - }); - - test('hierachical machine', () => { - const config = { - key: 'users-crud', - parallel: true, - states: { - light: { - initial: 'red', - states: { - red: { - on: { - TOGGLE: 'green' - }, - initial: 'nested', - states: { - nested: {} - } - }, - green: { - on: { - TOGGLE: 'red' - } - } - } - } - } - }; - - expect(parser.root(config)).toMatchSnapshot(); - }); -}); diff --git a/src/buffer.js b/src/buffer.js new file mode 100644 index 0000000..169117a --- /dev/null +++ b/src/buffer.js @@ -0,0 +1,39 @@ +class Buffer { + constructor() { + this.value = ''; + this.indentation = 0; + } + + indent() { + this.indentation++; + } + + outdent() { + this.indentation--; + } + + whitespace() { + return new Array(this.indentation + 1).join(' '); + } + + newline() { + this.value += '\n'; + } + + append(value) { + this.value += this.whitespace() + value + '\n'; + } + + appendf(strings, ...values) { + const fn = (str, value) => + str + value.replace(/[\(\)]/g, '').replace(/-/g, '_'); + + const value = strings + .map((str, i) => fn(str, values[i] || '')) + .join(''); + + this.append(value); + } +} + +module.exports = Buffer; diff --git a/src/core.js b/src/core.js new file mode 100644 index 0000000..4f6a0a8 --- /dev/null +++ b/src/core.js @@ -0,0 +1,105 @@ +const xstate = require('xstate'); +const Buffer = require('./buffer'); + +const isStateNode = machine => + machine.constructor.name === 'StateNode'; + +const getNode = (stateNode, path) => + path.split('.').reduce((acc, k) => acc.states[k], stateNode); + +const resolvePath = (stateNode, path) => + path[0] === '#' ? path.substr(1) : getNode(stateNode, path).id; + +/** + * Write `stateNode` events to `buffer`. + * + * An event is a constant string that initiates a transition + * between states. An `event` originates from state `from` and + * causes the machine to transition to state `to`. + * + * In plantuml, this is represented as `from --> to : event`. + * the initial event is represented as `[*] --> to`. + */ +const events = (stateNode, buffer) => { + if (stateNode.initial) { + const to = resolvePath(stateNode, stateNode.initial); + buffer.appendf`[*] --> ${to}`; + buffer.newline(); + } + + for (const [ev, targets] of Object.entries(stateNode.on)) { + for (const { target } of targets) { + const to = resolvePath(stateNode.parent, target[0]); + buffer.appendf`${stateNode.id} --> ${to} : ${ev}`; + } + } +}; + +/** + * Write `stateNode` actions to `buffer`. + */ +const actions = (stateNode, buffer) => { + for (const action of stateNode.onEntry) { + buffer.appendf`${stateNode.id} : onEntry/${action}`; + } + + for (const action of stateNode.onExit) { + buffer.appendf`${stateNode.id} : onExit/${action}`; + } +}; + +/** + * Write `stateNode` states to `buffer`. + */ +const states = (stateNode, buffer) => { + const values = Object.values(stateNode.states); + + for (const [index, child] of values.entries()) { + state(child, buffer); + + if (index < values.length - 1) { + buffer.newline(); + } + } +}; + +/** + * Write `stateNode` to buffer. + */ +const state = (stateNode, buffer) => { + buffer.appendf`state "${stateNode.key}" as ${stateNode.id} {`; + buffer.indent(); + + actions(stateNode, buffer); + events(stateNode, buffer); + states(stateNode, buffer); + + buffer.outdent(); + buffer.append(`}`); +}; + +const options = (opts, buffer) => { + if (opts.leftToRight) { + buffer.append('left to right direction'); + } +}; + +const defaultOpts = { + leftToRight: true +}; + +const convert = (machine, opts = defaultOpts) => { + const buffer = new Buffer(); + const stateNode = isStateNode(machine) + ? machine + : xstate.Machine(machine); + + buffer.append('@startuml'); + options(opts, buffer); + state(stateNode, buffer); + buffer.append('@enduml'); + + return buffer.value; +}; + +module.exports = convert; diff --git a/src/formatter.js b/src/formatter.js deleted file mode 100644 index a402ae5..0000000 --- a/src/formatter.js +++ /dev/null @@ -1,75 +0,0 @@ -const _ = require('lodash/fp'); - -/** - * Increase buffer's indentation by one level. - */ -const indent = buffer => _.update('indent', v => v + 1, buffer); - -/** - * Decrease buffer's indentation by one level. - */ -const outdent = buffer => _.update('indent', v => (v > 0 ? v - 1 : v), buffer); - -/** - * Return a string of whitespace equal to the buffer's indentation. - */ -const tabs = buffer => _.repeat(buffer.indent, _.repeat(4, ' ')); - -/** - * Append a line to a buffer - */ -const append = (value, buffer) => - _.update('value', v => v + tabs(buffer) + value + '\n', buffer); - -const root = () => [ - buffer => append('@startuml', buffer), - buffer => append('@enduml', buffer) -]; - -const machine = ({ name, key }) => [ - buffer => append(`state "${name}" as ${key} {`, buffer), - buffer => append('}', buffer) -]; - -const state = ([name, key]) => [ - buffer => append(`state "${name}" as ${key}`, buffer) -]; - -const event = ([from, to, ev]) => [ - buffer => append(`${from} --> ${to} ${ev ? `: ${ev}` : ''}`, buffer) -]; - -const action = ([machine, on, label]) => [ - buffer => append(`${machine} : ${on}/${label}`, buffer) -]; - -const ops = { - machine, - state, - event, - action, - root -}; - -const format = (ast, buffer) => { - const [op, args, ...children] = ast; - const [pre, post] = ops[op](args); - - if (pre) { - buffer = pre(buffer); - } - - for (const child of children) { - buffer = indent(buffer); - buffer = format(child, buffer); - buffer = outdent(buffer); - } - - if (post) { - buffer = post(buffer); - } - - return buffer; -}; - -module.exports.default = ast => format(ast, { value: '', indent: 0 }).value; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 3e9c8ce..0000000 --- a/src/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const parser = require('./parser'); -const formatter = require('./formatter'); - -module.exports.default = (config, options = {}) => - formatter.default(parser.root(config)); diff --git a/src/parser.js b/src/parser.js deleted file mode 100644 index f8dae40..0000000 --- a/src/parser.js +++ /dev/null @@ -1,125 +0,0 @@ -const _ = require('lodash/fp'); - -const combinations = (collA, collB) => - _.flatMap(a => _.map(b => [a, b], collB), collA); - -const iterateEvents = (fn, config) => { - let ast = _.flatMap( - ([from, v]) => - _.map(([ev, target]) => fn(from, target, ev), _.toPairs(v.on)), - _.toPairs(config.states) - ); - - if (config.on) { - ast = _.concat( - ast, - _.compact( - _.map(([[from], [ev, target]]) => { - if (_.isObject(target) && target.internal) { - return fn(from, target.target, ev); - } - - if (_.isString(target) && target[0] === '.') { - return fn(from, target.substr(1), ev); - } - }, combinations(_.toPairs(config.states), _.toPairs(config.on))) - ) - ); - } - - return ast; -}; - -const state = (prefix, { states }) => - _.reduce( - (acc, [k, v]) => { - if (v.states) { - return acc; - } - - return [...acc, ['state', [k, `${prefix}_${k}`]]]; - }, - [], - _.toPairs(states) - ); - -const actions = (prefix, config) => { - ast = _.flatMap( - ([k, v]) => - _.map(key => (v[key] ? ['action', [config.key, key, v[key]]] : null), [ - 'onEntry', - 'onExit' - ]), - _.toPairs(config.states) - ); - - return _.compact(ast); -}; - -const event = prefix => (from, target, ev) => { - let targetName; - - if (_.isString(target)) { - targetName = target; - } else if (_.isArray(target)) { - targetName = _.get('0.target', target); - } else if (_.isObject(target) && !target.internal) { - targetName = _.keys(target)[0]; - } else if (_.isObject(target) && !!target.internal) { - targetName = target.target; - } - return ['event', [`${prefix}_${from}`, `${prefix}_${targetName}`, ev]]; -}; - -const events = (prefix, config) => { - let ast = config.initial - ? [['event', ['[*]', `${prefix}_${config.initial}`]]] - : []; - - ast = _.concat(ast, iterateEvents(event(prefix), config)); - return ast; -}; - -const submachine = (prefix, config) => { - return _.compact( - _.map(([name, target]) => { - if (_.isObject(target) && target.states) { - return machine(`${prefix}_${name}`, name, target); - } - }, _.toPairs(config.states)) - ); -}; - -const machine = (key, name, xstate) => [ - 'machine', - { key, name }, - ...actions(name, xstate), - ...state(name, xstate), - ...events(name, xstate), - ...submachine(name, xstate) -]; - -const root = xstate => { - if (!xstate.parallel) { - return ['root', {}, machine(xstate.key, xstate.key, xstate)]; - } - - const machines = _.map( - ([key, config]) => ({ ...config, key }), - _.toPairs(xstate.states) - ); - - return [ - 'root', - {}, - ..._.map(config => machine(config.key, config.key, config), machines) - ]; -}; - -module.exports = { - state, - events, - actions, - machine, - root -}; diff --git a/yarn.lock b/yarn.lock index 84bcee0..aac7da0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3128,6 +3128,10 @@ xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" +xstate@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-3.3.3.tgz#64177cd4473d4c2424b3df7d2434d835404b09a9" + y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"