diff --git a/.gitignore b/.gitignore index a22519b..acf24a8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ qpm setupfile coverage src/node_path/node_modules -tmp +*.swp +tmp \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b501675 --- /dev/null +++ b/Makefile @@ -0,0 +1,133 @@ +#export PATH=$PATH:/path/to/Qt/5.7/clang_64/bin +#check for qpm + +.PHONY: test, build, run, clean, example/% + +## +# use bash as shell +# +SHELL:=/bin/bash + +## +# root directory (Makefile location) +# +WORKING_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +## +# path to qt bins +# +QT:=$(HOME)/Qt/5.7/clang_64/bin + +BUILD_DIR:=$(WORKING_DIR)/build +JS_DIR:=$(WORKING_DIR)/src/node_path + +## +# find all relevant sources (sources that end with .js) +# and get their path relative to working dir +# +SOURCES_RELATIVE:= \ + $(shell cd $(JS_DIR) && find src -type f -iname '*.js') + +## +# save the relative sources to a new variable that holds al SOURCES +# with absolute paths +# +SOURCES:= \ + $(foreach x, $(SOURCES_RELATIVE), $(JS_DIR)/$(x)) + +## +# these are our "object"-files - the files that are transpiled from +# es6 to es5 +# +OBJECTS:= \ + $(foreach x, $(SOURCES_RELATIVE), $(BUILD_DIR)/$(x)) + +## +# these are our "object"-files - the files that are transpiled from +# es6 to es5 +# +INSTALLED_OBJECTS:= \ + $(foreach x, $(shell cd $(JS_DIR)/src && find . -type f -iname '*.js'), $(JS_DIR)/lib/$(x)) \ + +export PATH:=$(QT):$(PATH) + +all: build + +build: $(BUILD_DIR)/quark.app + @echo "Build finished" + +run: APP=$(WORKING_DIR)/example/default +run: build + @echo "Running app" + $(WORKING_DIR)/build/quark.app/Contents/MacOS/quark $(APP)/package.json + +force: + +example/%: force + make run APP=$@ + +clean: + rm -rf $(WORKING_DIR)/setupfile + rm -rf $(WORKING_DIR)/src/node_path/node_modules + cd $(BUILD_DIR) && make clean + rm -rf $(BUILD_DIR) + rm -rf $(JS_DIR)/lib + rm -rf $(JS_DIR)/node_modules + +test: $(OBJECTS) $(BUILD_DIR)/node_modules + $(WORKING_DIR)/src/node_path/node_modules/.bin/istanbul cover --root $(BUILD_DIR)/src -x "**/__tests__/**" $(WORKING_DIR)/src/node_path/node_modules/.bin/_mocha $(shell find $(BUILD_DIR)/src -name "*Test.js" -not -path "*node_modules*") -- -R spec --require source-map-support/register + + +setup: $(WORKING_DIR)/setupfile + +## +# builds the qt renderer app +# +$(BUILD_DIR)/quark.app: $(INSTALLED_OBJECTS) test + cd $(BUILD_DIR) && qmake .. + cd $(BUILD_DIR) && make + +## +# adds the node modules to build dir +# for testing purposes +# +# TODO: hier müssen die files einzeln kopiert werden, +# damit der änderungen checkt +# +$(BUILD_DIR)/node_modules: $(WORKING_DIR)/setupfile + cp -r $(JS_DIR)/node_modules $@ + +## +# file to save setup status +# +$(WORKING_DIR)/setupfile: + mkdir -p $(BUILD_DIR) + cd $(WORKING_DIR)/src/node_path && npm install + qpm install + cd $(WORKING_DIR)/tools && make bootstrap + @echo "setup done" > $@ + +## +# this targets are necessary to not always trigger a rebuild of +# transpiled files, even if they exist. if the no-op is removed +# this will trigger a rebuild too +# +$(JS_DIR)/src/%.js: + @echo "" > /dev/null + +## +# every transpiled file requires a matching source file +# to be created. +# +$(BUILD_DIR)/src/%.js: $(JS_DIR)/src/%.js $(WORKING_DIR)/setupfile + mkdir -p $(dir $@) + $(JS_DIR)/node_modules/.bin/eslint $< + $(JS_DIR)/node_modules/.bin/babel $< --out-file $@ --source-maps --presets es2017,es2016,node6 --plugins transform-runtime,transform-class-properties + +## +# every destination file needs a transpiled +# source that is tested +# +$(JS_DIR)/lib/%.js: $(BUILD_DIR)/src/%.js + mkdir -p $(dir $@) + cp $< $@ diff --git a/example/counter/package.json b/example/counter/package.json index c30fae2..e24084f 100644 --- a/example/counter/package.json +++ b/example/counter/package.json @@ -1,5 +1,5 @@ { - "name": "quart-counter", + "name": "quark-counter", "version": "0.1.0", "main": "main.js" } diff --git a/example/login/Menu.js b/example/login/Menu.js new file mode 100644 index 0000000..721dbb0 --- /dev/null +++ b/example/login/Menu.js @@ -0,0 +1,44 @@ +const { Statechart } = require("quark"); + +module.exports = Statechart.of({ + type: { + scrollPosition: Number, + opacity: Number + }, + + OPEN: { + enter() { + return this.set("opacity", 1); + }, + + onClose() { + return this.changeState("CLOSED") + }, + + onLoggedOut: "onClose" + }, + + CLOSED: { + enter() { + return this.set("opacity", 0); + }, + + onOpen() { + return this.changeState("OPEN"); + } + }, + + enter() { + return this + set("scrollPosition", 0) + .changeState("CLOSED"); + }, + + onScroll(e) { + const state = e.scrollPosition.y > 50 && e.scrollPosition.y - this.get("scrollPosition") ? "OPEN" : "CLOSED"; + + return this + .set("scrollPosition", e.scrollPosition.y) + .changeState(state); + } +}); diff --git a/example/login/Todos.js b/example/login/Todos.js new file mode 100644 index 0000000..a38e2ad --- /dev/null +++ b/example/login/Todos.js @@ -0,0 +1,21 @@ +module.exports = Statechart.of({ + enter() { + return this.set([]); + }, + + onAdd(todo) { + return this.push(todo); + }, + + onRemove(todo) { + return this.filter(x => x.id === todo.id); + }, + + onUpdate(todo) { + return this.update(x => x.id === todo.id ? todo : x); + }, + + onSort(sort) { + return this.sort((a, b) => sort === DESCENDING ? a.id - b.id : b.id - a.id); + } +}); diff --git a/example/login/User.js b/example/login/User.js new file mode 100644 index 0000000..d796b55 --- /dev/null +++ b/example/login/User.js @@ -0,0 +1,42 @@ +module.exports = Statechart.of({ + enter() { + return this.changeState(„LOGGED_OUT“); + }, + + LOGGED_IN: { + enter() { + return this.set(„qml“, „loggedIN“); + }, + + logout() { + return this.changeState(„LOGGED_OUT“); + }, + + exit() { + return this.set(„user“, null); + } + }, + + onError(message) { + return this.set(„errorMessage“, message); + }, + + LOGGED_OUT: { + enter() { + return this.set(„qml“, „loggedOut“); + }, + + login({ id, password }, state) { + const success = this.state.find(x => x.id === id).password !== password; + const action = success ? „error“ : „loginSuccess“; + + return success ? this.changeState(„LOGGED_OUT“, user) : this.trigger(„error“, „Error“); + }, + + onLogin: „login“, + + exit(user) { + this.set(„user“, user); + } + } +}); diff --git a/example/login/index.qml b/example/login/index.qml new file mode 100644 index 0000000..3fd4749 --- /dev/null +++ b/example/login/index.qml @@ -0,0 +1,42 @@ +import QtQuick 2.2 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.3 +import Quark 1.0 + +ApplicationWindow { + id: window + visible: true + width: 300 + + Gluon { + /* + This component holds the application state. + The property value holds the current value. + The slot dispatch can be called to emit an action. + */ + id: store + } + + RowLayout { + anchors.fill: parent + + Button { + anchors.left: window.left + text: "-" + + onClicked: store.trigger("sub") + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: JSON.stringify(store.value.count); + } + + Button { + text: "+" + onClicked: store.trigger("add") + } + } +} + diff --git a/example/login/main.js b/example/login/main.js new file mode 100644 index 0000000..2af48ba --- /dev/null +++ b/example/login/main.js @@ -0,0 +1,20 @@ +const Quark = require("quark"); +const path = require("path"); +const Menu = require("./Menu"); +const User = require("./User"); +const Todos = require("./Todos"); + +/* const app = Quark.of({ + qml: path.join(__dirname, "index.qml"), + initialState: { count: 0 }, + intents: { + onSub: state => state.update("count", count => count - 1), + onAdd: state => state.update("count", count => count + 1) + } +});*/ + +const app = Quark.of({ + menu: Menu, + user: User, + todos: Todos +}); diff --git a/example/login/package.json b/example/login/package.json new file mode 100644 index 0000000..b332124 --- /dev/null +++ b/example/login/package.json @@ -0,0 +1,6 @@ +{ + "name": "quark-login", + "version": "0.1.0", + "main": "main.js", + "initialQml": "index.qml" +} diff --git a/example/todo/.main.js.swp b/example/todo/.main.js.swp deleted file mode 100644 index f0c2b5e..0000000 Binary files a/example/todo/.main.js.swp and /dev/null differ diff --git a/src/node_path/.eslintrc.json b/src/node_path/.eslintrc.json new file mode 100644 index 0000000..b4e6bd2 --- /dev/null +++ b/src/node_path/.eslintrc.json @@ -0,0 +1,584 @@ +{ + "parser": "babel-eslint", + "env": { + "browser": true, + "node": true, + "es6": true, + "mocha": true + }, + "rules": { + "strict": [ + 2 + ], + "curly": [ + 2, + "multi" + ], + "default-case": [ + 2 + ], + "comma-dangle": [ + 2 + ], + "no-cond-assign": [ + 2 + ], + "no-constant-condition": [ + 2 + ], + "no-empty-character-class": [ + 2 + ], + "no-empty": [ + 2 + ], + "no-ex-assign": [ + 2 + ], + "no-extra-boolean-cast": [ + 2 + ], + "no-extra-semi": [ + 2 + ], + "no-func-assign": [ + 2 + ], + "no-inner-declarations": [ + 2 + ], + "no-invalid-regexp": [ + 2 + ], + "no-irregular-whitespace": [ + 2 + ], + "valid-typeof": [ + 2 + ], + "no-unexpected-multiline": [ + 2 + ], + "no-negated-in-lhs": [ + 2 + ], + "no-obj-calls": [ + 2 + ], + "no-regex-spaces": [ + 2 + ], + "no-sparse-arrays": [ + 2 + ], + "no-unreachable": [ + 2 + ], + "use-isnan": [ + 2 + ], + "no-control-regex": [ + 2 + ], + "no-debugger": [ + 2 + ], + "no-dupe-keys": [ + 2 + ], + "no-dupe-args": [ + 2 + ], + "no-duplicate-case": [ + 2 + ], + "accessor-pairs": [ + 2 + ], + "block-scoped-var": [ + 2 + ], + "no-multi-spaces": [ + 2, + { + "exceptions": { + "VariableDeclarator": true, + "AssignmentExpression": true, + "IfStatement": true + } + } + ], + "key-spacing": [ + 2, + { + "align": "value" + } + ], + "new-cap": [ + 0, + { + "capIsNewExceptions": [] + } + ], + "valid-jsdoc": [ + 2, + { + "requireReturn": false, + "requireReturnDescription": false + } + ], + "complexity": [ + 2, + 5 + ], + "consistent-return": [ + 2 + ], + "dot-notation": [ + 2 + ], + "dot-location": [ + 2, + "property" + ], + "eqeqeq": [ + 2 + ], + "guard-for-in": [ + 2 + ], + "no-alert": [ + 2 + ], + "no-caller": [ + 2 + ], + "no-div-regex": [ + 2 + ], + "no-else-return": [ + 2 + ], + "no-labels": [ + 2 + ], + "no-eval": [ + 2 + ], + "no-extra-bind": [ + 2 + ], + "no-eq-null": [ + 2 + ], + "no-extend-native": [ + 2 + ], + "no-fallthrough": [ + 2 + ], + "no-floating-decimal": [ + 2 + ], + "no-implicit-coercion": [ + 2 + ], + "no-implied-eval": [ + 2 + ], + "no-invalid-this": [ + 2 + ], + "no-iterator": [ + 2 + ], + "no-lone-blocks": [ + 2 + ], + "no-loop-func": [ + 2 + ], + "no-multi-str": [ + 2 + ], + "no-native-reassign": [ + 2 + ], + "no-new-func": [ + 2 + ], + "no-new-wrappers": [ + 2 + ], + "no-new": [ + 2 + ], + "no-octal": [ + 2 + ], + "no-octal-escape": [ + 2 + ], + "no-param-reassign": [ + 2 + ], + "no-process-env": [ + 2 + ], + "no-proto": [ + 2 + ], + "no-redeclare": [ + 2 + ], + "no-return-assign": [ + 2 + ], + "no-script-url": [ + 2 + ], + "no-self-compare": [ + 2 + ], + "no-sequences": [ + 2 + ], + "no-throw-literal": [ + 2 + ], + "no-unused-expressions": [ + 2 + ], + "no-useless-call": [ + 2 + ], + "no-void": [ + 2 + ], + "no-warning-comments": [ + 0 + ], + "no-with": [ + 2 + ], + "radix": [ + 2 + ], + "vars-on-top": [ + 2 + ], + "wrap-iife": [ + 2 + ], + "yoda": [ + 2 + ], + "no-undef": [ + 2 + ], + "no-undefined": [ + 2 + ], + "init-declarations": [ + 2, + "always" + ], + "no-catch-shadow": [ + 2 + ], + "no-delete-var": [ + 2 + ], + "no-label-var": [ + 2 + ], + "no-shadow-restricted-names": [ + 2 + ], + "no-shadow": [ + 2 + ], + "no-undef-init": [ + 2 + ], + "no-unused-vars": [ + 2 + ], + "no-use-before-define": [ + 2 + ], + "callback-return": [ + 2 + ], + "handle-callback-err": [ + 2 + ], + "no-mixed-requires": [ + 2 + ], + "no-new-require": [ + 2 + ], + "no-path-concat": [ + 2 + ], + "no-process-exit": [ + 2 + ], + "no-sync": [ + 2 + ], + "func-style": [ + 2, + "expression" + ], + "no-inline-comments": [ + 2 + ], + "no-array-constructor": [ + 2 + ], + "no-multiple-empty-lines": [ + 2 + ], + "array-bracket-spacing": [ + 2, + "never" + ], + "block-spacing": [ + 2, + "always" + ], + "brace-style": [ + 2, + "1tbs" + ], + "camelcase": [ + 2 + ], + "comma-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "computed-property-spacing": [ + 2, + "never" + ], + "consistent-this": [ + 2, + "self" + ], + "eol-last": [ + 2 + ], + "id-length": [ + 2, + { + "min": 2, + "max": 20, + "exceptions": [ + "x", + "e", + "T", + "_", + "Q", + "autoAcceptConnections" + ] + } + ], + "indent": [ + 2 + ], + "lines-around-comment": [ + 2, + { + "beforeBlockComment": true, + "beforeLineComment": false + } + ], + "linebreak-style": [ + 2 + ], + "max-nested-callbacks": [ + 2, + 3 + ], + "new-parens": [ + 2 + ], + "newline-after-var": [ + 2 + ], + "no-continue": [ + 2 + ], + "no-mixed-spaces-and-tabs": [ + 2 + ], + "no-nested-ternary": [ + 2 + ], + "no-new-object": [ + 2 + ], + "no-spaced-func": [ + 2 + ], + "no-trailing-spaces": [ + 2 + ], + "no-underscore-dangle": [ + 2 + ], + "no-unneeded-ternary": [ + 2 + ], + "object-curly-spacing": [ + 2, + "always" + ], + "one-var": [ + 2, + "never" + ], + "operator-assignment": [ + 2, + "never" + ], + "operator-linebreak": [ + 2, + "after" + ], + "padded-blocks": [ + 2, + "never" + ], + "quote-props": [ + 2, + "consistent-as-needed" + ], + "quotes": [ + 2, + "double" + ], + "keyword-spacing": [ + 2, + { + "overrides": { + "if": { + "after": false + }, + "for": { + "after": false + }, + "while": { + "after": false + }, + "catch": { + "after": false + } + } + } + ], + "semi-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "semi": [ + 2, + "always" + ], + "space-before-blocks": [ + 2, + "always" + ], + "space-before-function-paren": [ + 2, + "never" + ], + "space-in-parens": [ + 2, + "never" + ], + "space-infix-ops": [ + 2 + ], + "space-unary-ops": [ + 2, + { + "words": true, + "nonwords": false + } + ], + "spaced-comment": [ + 2, + "always" + ], + "arrow-parens": [ + 2, + "as-needed" + ], + "arrow-spacing": [ + 2, + { + "before": true, + "after": true + } + ], + "constructor-super": [ + 2 + ], + "no-class-assign": [ + 2 + ], + "no-const-assign": [ + 2 + ], + "no-dupe-class-members": [ + 2 + ], + "no-this-before-super": [ + 2 + ], + "no-var": [ + 2 + ], + "prefer-const": [ + 2 + ], + "prefer-spread": [ + 2 + ], + "prefer-reflect": [ + 0 + ], + "prefer-template": [ + 2 + ], + "require-yield": [ + 2 + ], + "max-depth": [ + 2, + 5 + ], + "max-statements": [ + 2, + 15 + ] + }, + "ecmaFeatures": { + "blockBindings": true, + "forOf": true, + "jsx": true, + "arrowFunctions": true, + "spread": true, + "modules": true + } + } diff --git a/src/node_path/GCD.js b/src/node_path/GCD.js deleted file mode 100644 index 33b7a5a..0000000 --- a/src/node_path/GCD.js +++ /dev/null @@ -1,51 +0,0 @@ -const stream = require("stream"); -const Transform = stream.Transform; -const assert = require("assert"); -const set = require("lodash.set"); - -module.exports = class GCD extends Transform { - static of(...args) { - return new GCD(...args); - } - - constructor(intents, mappings = {}) { - super({ - objectMode: true - }); - - this.intents = intents; - this.mappings = Object.assign(Object.keys(intents) - .reduce((dest, key) => set(dest, this.toAction(key), key), {}), mappings); - } - - toAction(key) { - return key - .slice(2, 3) - .toLowerCase() - .concat(key.slice(3)); - } - - _transform(data, enc, cb) { - assert(data && typeof data.type === "string", `Your action is in the wrong format. Expected an object with key type, but got '${data}' of type ${typeof data}.`); - - const key = this.mappings[data.type]; - const intent = this.intents[key]; - - assert(typeof intent === "function", `intent for '${key}' not found in intents ${Object.keys(this.intents)}.`); - - let timeout = 0; - - this.push({ - intent: intent.bind({ - after(time) { - timeout = time - return this; - }, - - trigger: (type, payload) => setTimeout(() => this.write({ type, payload }), timeout) - }), - payload: data.payload - }); - cb(); - } -} diff --git a/src/node_path/Selector.js b/src/node_path/Selector.js deleted file mode 100644 index 1bff056..0000000 --- a/src/node_path/Selector.js +++ /dev/null @@ -1,23 +0,0 @@ -const stream = require("stream"); -const Transform = stream.Transform; - -module.exports = class Selector extends Transform { - static of(...args) { - return new Selector(...args); - } - - constructor(path) { - super({ - objectMode: true - }); - - this.keys = path.split("."); - this.selection = null; - } - - _transform(data, enc, cb) { - const selection = this.keys.reduce((slice, key) => slice[key], data); - - return selection !== this.selection ? cb(null, selection) : cb(); - } -} diff --git a/src/node_path/Store.js b/src/node_path/Store.js deleted file mode 100644 index ffb4236..0000000 --- a/src/node_path/Store.js +++ /dev/null @@ -1,43 +0,0 @@ -const stream = require("stream"); -const Transform = stream.Transform; -const assert = require("assert"); -const Immutable = require("immutable"); -const Selector = require("./Selector"); - -module.exports = class Store extends Transform { - static of(...args) { - return new Store(...args); - } - - constructor(state) { - super({ - objectMode: true - }); - - this.state = Immutable.Map(state); - this.push(state); - } - - onResult(cb, state) { - assert(state instanceof Immutable.Map, "unexpected state"); - - if(this.state === state) return cb(); - - // TODO: diff here - this.state = state; - this.push(this.state.toJS()); - cb(); - } - - _transform({ intent, payload }, enc, cb) { - const result = intent(this.state, payload); - - if(result.then) return result.then(this.onResult.bind(this, cb)).catch(cb); - - this.onResult(cb, result); - } - - listen(path) { - return this.pipe(Selector.of(path)); - } -} diff --git a/src/node_path/__tests__/GCDTest.js b/src/node_path/__tests__/GCDTest.js deleted file mode 100644 index 05ccf79..0000000 --- a/src/node_path/__tests__/GCDTest.js +++ /dev/null @@ -1,7 +0,0 @@ -const { expect } = require("chai"); - -describe("GCDTest", function() { - it("tests", function() { - expect(true).to.equal(true); - }); -}); diff --git a/src/node_path/lib/GCD.js b/src/node_path/lib/GCD.js new file mode 100644 index 0000000..e784390 --- /dev/null +++ b/src/node_path/lib/GCD.js @@ -0,0 +1,72 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _keys = require("babel-runtime/core-js/object/keys"); + +var _keys2 = _interopRequireDefault(_keys); + +var _assign = require("babel-runtime/core-js/object/assign"); + +var _assign2 = _interopRequireDefault(_assign); + +var _stream = require("stream"); + +var _assert = require("assert"); + +var _assert2 = _interopRequireDefault(_assert); + +var _lodash = require("lodash.set"); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _Intent = require("./Intent"); + +var _Intent2 = _interopRequireDefault(_Intent); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +class GCD extends _stream.Transform { + static of(...args) { + return new GCD(...args); + } + + constructor(intents = {}, mappings = {}) { + super({ + objectMode: true + }); + + this.intents = intents; + this.mappings = (0, _assign2.default)((0, _keys2.default)(intents).reduce((dest, key) => (0, _lodash2.default)(dest, this.toAction(key), key), {}), mappings); + + this.mappingsString = (0, _stringify2.default)(this.mappings); + } + + toAction(key) { + return key.slice(2, 3).toLowerCase().concat(key.slice(3)); + } + + _transform(data, enc, cb) { + (0, _assert2.default)(data && typeof data.type === "string", `Your action is in the wrong format. Expected an object with key type, but got '${ (0, _stringify2.default)(data) }' of type ${ typeof data }.`); + + const key = this.mappings[data.type]; + + (0, _assert2.default)(typeof key === "string", `There is exists no mapping for '${ data.type }' in ${ this.mappingsString }.`); + + const intent = this.intents[key]; + + (0, _assert2.default)(typeof intent === "function", `No intent found for '${ data.type }' -> '${ key }' in intents [${ (0, _keys2.default)(this.intents) }].`); + + this.push(_Intent2.default.of(intent, data.payload, this)); + cb(); + } +} +exports.default = GCD; + +//# sourceMappingURL=GCD.js.map \ No newline at end of file diff --git a/src/node_path/lib/Gluon.js b/src/node_path/lib/Gluon.js new file mode 100644 index 0000000..53babef --- /dev/null +++ b/src/node_path/lib/Gluon.js @@ -0,0 +1,118 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _stream = require("stream"); + +var _stream2 = _interopRequireDefault(_stream); + +var _jsonstream = require("jsonstream2"); + +var _jsonstream2 = _interopRequireDefault(_jsonstream); + +var _through2Filter = require("through2-filter"); + +var _through2Filter2 = _interopRequireDefault(_through2Filter); + +var _through2Map = require("through2-map"); + +var _through2Map2 = _interopRequireDefault(_through2Map); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// hier das is wg qt seite komisch +const JSONStringifier = () => new _stream.Transform({ + objectMode: true, + + transform(chunk, enc, cb) { + cb(null, `${ (0, _stringify2.default)(chunk) }\n`); + } +}); + +class Gluon extends _stream.Duplex { + + static of(...args) { + return new Gluon(...args); + } + + static Output(type) { + const out = new _stream2.default(Gluon.opts); + + out.pipe((0, _through2Map2.default)(Gluon.opts, payload => ({ type, payload }))).pipe(JSONStringifier()).pipe(global.process.stdout); + + return out; + } + + constructor(qmlPath) { + super({ + objectMode: true + }); + + this.initialLoad = true; + this.qmlPath = qmlPath; + this.valueOut = Gluon.Output("value"); + this.actionOut = Gluon.Output("action"); + this.actions = global.process.stdin.pipe(_jsonstream2.default.parse()).pipe((0, _through2Filter2.default)(Gluon.opts, msg => msg.type === "action")).pipe((0, _through2Map2.default)(Gluon.opts, msg => msg.payload)); + + this.actions.on("data", data => this.push(data)); + } + + // sowohl load als auch start/kill sollten später als io + // über das plugin system gelöst werden iwie ? + load(url) { + this.initialLoad = false; + + this.actionOut.emit("data", { + type: "loadQml", + payload: { + url + } + }); + + return url; + } + + trim(path) { + return path.slice(1).replace(/\//g, "\\"); + } + + start(process) { + this.actionOut.emit("data", { + type: "startProcess", + payload: this.trim(process) + }); + + return process; + } + + kill(process) { + this.actionOut.emit("data", { + type: "killProcess", + payload: this.trim(process) + }); + + return process; + } + + _write(data, enc, next) { + this.valueOut.emit("data", data); + + this.qmlPath = this.initialLoad && this.qmlPath ? this.load(this.qmlPath) : this.qmlPath; + this.qmlPath = !this.initialLoad && data.qml && data.qml !== this.qmlPath ? this.load(data.qml) : this.qmlPath; + next(); + } + + _read() {} +} +exports.default = Gluon; +Gluon.opts = { + objectMode: true +}; + +//# sourceMappingURL=Gluon.js.map \ No newline at end of file diff --git a/src/node_path/lib/Intent.js b/src/node_path/lib/Intent.js new file mode 100644 index 0000000..ec6c56e --- /dev/null +++ b/src/node_path/lib/Intent.js @@ -0,0 +1,32 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +class Intent { + static of(...args) { + return new Intent(...args); + } + + constructor(intent, payload, stream) { + this.timeout = 0; + this.intent = intent.bind(this); + this.payload = payload; + this.stream = stream; + } + + after(timeout) { + this.timeout = this.timeout + timeout; + + return this; + } + + trigger(type, payload) { + setTimeout(() => this.stream.write({ type, payload }), this.timeout); + + return this; + } +} +exports.default = Intent; + +//# sourceMappingURL=Intent.js.map \ No newline at end of file diff --git a/src/node_path/lib/Quark.js b/src/node_path/lib/Quark.js new file mode 100644 index 0000000..a9f4ce4 --- /dev/null +++ b/src/node_path/lib/Quark.js @@ -0,0 +1,92 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _assign = require("babel-runtime/core-js/object/assign"); + +var _assign2 = _interopRequireDefault(_assign); + +var _stream = require("stream"); + +var _GCD = require("./GCD"); + +var _GCD2 = _interopRequireDefault(_GCD); + +var _Store = require("./Store"); + +var _Store2 = _interopRequireDefault(_Store); + +var _Gluon = require("./Gluon"); + +var _Gluon2 = _interopRequireDefault(_Gluon); + +var _Statechart = require("./Statechart"); + +var _Statechart2 = _interopRequireDefault(_Statechart); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +class Quark extends _stream.Duplex { + + static of(...args) { + return new Quark(...args); + } + + constructor({ initialState: state, intents, qml, mappings }) { + super({ + objectMode: true + }); + + // TODO options parsen und hinzufügen + this.store = _Store2.default.of((0, _assign2.default)({ qml, processes: [] }, state || {})); + this.gcd = _GCD2.default.of(intents || {}, mappings); + this.view = _Gluon2.default.of(qml); + this.qml = qml; + this.timeout = 0; + this.buffer = []; + + this.store.on("data", this.buffer.push.bind(this.buffer)); + this.view.pipe(this.gcd).pipe(this.store).pipe(this.view); + } + + after(timeout) { + this.timeout = timeout; + + return this; + } + + trigger(type, payload) { + setTimeout(() => this.write({ type, payload }), this.timeout); + + return this.after(0); + } + + listen(path) { + return this.store.listen(path); + } + + write(...args) { + super.write(...args); + + return this; + } + + _write(data, enc, cb) { + this.gcd.write(data); + cb(); + } + + _read() { + // eslint-disable-line + if (this.buffer.length === 0) return setTimeout(this._read.bind(this), 17); // eslint-disable-line + + this.buffer.forEach(this.push.bind(this)); + this.buffer.length = 0; + } +} +exports.default = Quark; +Quark.Statechart = _Statechart2.default; + +//# sourceMappingURL=Quark.js.map \ No newline at end of file diff --git a/src/node_path/lib/Selector.js b/src/node_path/lib/Selector.js new file mode 100644 index 0000000..1c41b37 --- /dev/null +++ b/src/node_path/lib/Selector.js @@ -0,0 +1,35 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _stream = require("stream"); + +class Selector extends _stream.Transform { + static of(...args) { + return new Selector(...args); + } + + constructor(path) { + super({ + objectMode: true + }); + + this.keys = path.split("/"); + this.selection = null; + } + + _transform(data, enc, cb) { + const selection = this.keys.reduce((slice, key) => slice && slice[key], data); + + if (selection === this.selection) return cb(); + + this.selection = selection; + + return cb(null, selection); + } +} +exports.default = Selector; + +//# sourceMappingURL=Selector.js.map \ No newline at end of file diff --git a/src/node_path/lib/Statechart.js b/src/node_path/lib/Statechart.js new file mode 100644 index 0000000..56841c8 --- /dev/null +++ b/src/node_path/lib/Statechart.js @@ -0,0 +1,19 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +const assign = require("lodash.assign"); + +class Statechart { + static of(...args) { + return new Statechart(...args); + } + + constructor(description) { + return assign(this, description); + } +} +exports.default = Statechart; + +//# sourceMappingURL=Statechart.js.map \ No newline at end of file diff --git a/src/node_path/lib/Store.js b/src/node_path/lib/Store.js new file mode 100644 index 0000000..d91178a --- /dev/null +++ b/src/node_path/lib/Store.js @@ -0,0 +1,82 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _stream = require("stream"); + +var _immutable = require("immutable"); + +var _immutable2 = _interopRequireDefault(_immutable); + +var _Selector = require("./Selector"); + +var _Selector2 = _interopRequireDefault(_Selector); + +var _immutablediff = require("immutablediff"); + +var _immutablediff2 = _interopRequireDefault(_immutablediff); + +var _assert = require("assert"); + +var _assert2 = _interopRequireDefault(_assert); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +class Store extends _stream.Transform { + static of(...args) { + return new Store(...args); + } + + constructor(state) { + super({ + objectMode: true + }); + + // FIXME: das hier macht iwie mucken, is aber jucking, wenn + // auf c++ seite json patch benutzt wird, dadurch sind auch + // die diffs atm nur auf einer ebene sinnvoll, des weiteren + // muss en weg gefunden werden, wie neue records sinnvoll in + // den immutable context gebracht werden + // this.state = Immutable.fromJS(state); + + this.state = _immutable2.default.Map(state); + + this.push(this.state.toJSON()); + } + + onResult(cb, state) { + (0, _assert2.default)(state instanceof _immutable2.default.Map, "unexpected state"); + + const difference = (0, _immutablediff2.default)(this.state, state).toJSON(); + + if (difference.length === 0) return cb(); + + // TODO: diff here + console.error(`diff: ${ (0, _stringify2.default)(difference) }`); + this.state = state; + this.push(this.state.toJSON()); + + return cb(); + } + + _transform({ intent, payload }, enc, cb) { + const result = intent(this.state, payload); + + if (result.then) return result.then(this.onResult.bind(this, cb)).catch(cb); + + return this.onResult(cb, result); + } + + listen(path) { + return this.pipe(_Selector2.default.of(path)); + } +} +exports.default = Store; + +//# sourceMappingURL=Store.js.map \ No newline at end of file diff --git a/src/node_path/lib/__tests__/GCDTest.js b/src/node_path/lib/__tests__/GCDTest.js new file mode 100644 index 0000000..4cb73c1 --- /dev/null +++ b/src/node_path/lib/__tests__/GCDTest.js @@ -0,0 +1,84 @@ +"use strict"; + +var _GCD = require("../GCD"); + +var _GCD2 = _interopRequireDefault(_GCD); + +var _expectStream = require("expect-stream"); + +var _sinon = require("sinon"); + +var _sinon2 = _interopRequireDefault(_sinon); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +describe("GCDTest", function () { + it("lets the gcd dispatch some intents", function (done) { + const intents = { + onData: _sinon2.default.spy(), + blub: _sinon2.default.spy() + }; + + const gcd = _GCD2.default.of(intents, { + test: "blub" + }); + + (0, _expectStream.expect)(gcd).to.produce(result => { + result.intent(); + + if (result.payload && result.payload.key === "test" && intents.onData.callCount === 1 && intents.blub.callCount === 0) return 0; + + if (result.payload && result.payload.key === "test2" && intents.onData.callCount === 1 && intents.blub.callCount === 1) return 1; + + return -1; + }).on({ + type: "data", + payload: { + key: "test" + } + }, { + type: "test", + payload: { + key: "test2" + } + }).notify(done); + }); + + it("produces an error when feeding malformed data", function () { + const gcd = _GCD2.default.of(); + + try { + gcd.write({}); + } catch (e) { + (0, _expectStream.expect)(e.message).to.equal("Your action is in the wrong format. Expected an object with key type, but got \'{}\' of type object."); + } + }); + + it("produces an error for a non existing mapping", function () { + const gcd = _GCD2.default.of(); + + try { + gcd.write({ + type: "test" + }); + } catch (e) { + (0, _expectStream.expect)(e.message).to.equal("There is exists no mapping for \'test\' in {}."); + } + }); + + it("produces an error for an unknown action", function () { + const gcd = _GCD2.default.of({}, { + test: "blub" + }); + + try { + gcd.write({ + type: "test" + }); + } catch (e) { + (0, _expectStream.expect)(e.message).to.equal("No intent found for \'test\' -> \'blub\' in intents []."); + } + }); +}); + +//# sourceMappingURL=GCDTest.js.map \ No newline at end of file diff --git a/src/node_path/lib/__tests__/GluonTest.js b/src/node_path/lib/__tests__/GluonTest.js new file mode 100644 index 0000000..4c57a4b --- /dev/null +++ b/src/node_path/lib/__tests__/GluonTest.js @@ -0,0 +1,99 @@ +"use strict"; + +var _stringify = require("babel-runtime/core-js/json/stringify"); + +var _stringify2 = _interopRequireDefault(_stringify); + +var _sinon = require("sinon"); + +var _sinon2 = _interopRequireDefault(_sinon); + +var _stream = require("stream"); + +var _expectStream = require("expect-stream"); + +var _Gluon = require("../Gluon"); + +var _Gluon2 = _interopRequireDefault(_Gluon); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +describe("GluonTest", function () { + beforeEach(function () { + this.stdin = _sinon2.default.stub(global.process.stdin, "pipe", stream => { + // eslint-disable-line + this.stream = stream; // eslint-disable-line + + return stream; + }); + + const write = global.process.stdout.write; + + this.out = new _stream.Transform({ // eslint-disable-line + transform(data, enc, cb) { + try { + JSON.parse(data); + + return cb(null, data); + } catch (e) { + // because mocha uses stdout too, we need an easy way to filter it's + // messages. Looking for parseability is the easiest one :D + return write.call(process.stdout, data); + } + } + }); + + this.stdout = _sinon2.default.stub(global.process.stdout, "write", this.out.write.bind(this.out)); // eslint-disable-line + }); + + afterEach(function () { + this.stdin.restore(); // eslint-disable-line + this.stdout.restore(); // eslint-disable-line + }); + + it("uses gluon to read", function (done) { + (0, _expectStream.expect)(_Gluon2.default.of("test")).to.produce([undefined, { // eslint-disable-line + test: "test" + }]).notify(done); + + const stream = this.stream; // eslint-disable-line + + stream.write((0, _stringify2.default)({ + type: "value", + payload: { + test: "test" + } + })); + + stream.write((0, _stringify2.default)({ + type: "action" + })); + + stream.write((0, _stringify2.default)({ + type: "action", + payload: { + test: "test" + } + })); + }); + + it("uses gluon to write", function (done) { + (0, _expectStream.expect)(this.out) // eslint-disable-line + .to.exactly.produce(["{\"type\":\"value\",\"payload\":{\"test\":\"test\"}}\n", "{\"type\":\"action\",\"payload\":{\"type\":\"loadQml\",\"payload\":{\"url\":\"path\"}}}\n", "{\"type\":\"value\",\"payload\":{\"qml\":\"test2\"}}\n", "{\"type\":\"action\",\"payload\":{\"type\":\"loadQml\",\"payload\":{\"url\":\"test2\"}}}\n", "{\"type\":\"action\",\"payload\":{\"type\":\"startProcess\",\"payload\":\"blub\\\\prog\\\\prog\"}}\n", "{\"type\":\"action\",\"payload\":{\"type\":\"killProcess\",\"payload\":\"blub\\\\prog\\\\prog\"}}\n"]).notify(done); + + const gluon = _Gluon2.default.of("path"); + + gluon.write({ + test: "test" + }); + + gluon.write({ + qml: "test2" + }); + + gluon.start("/blub/prog/prog"); + gluon.kill("/blub/prog/prog"); + }); +}); + +//# sourceMappingURL=GluonTest.js.map \ No newline at end of file diff --git a/src/node_path/lib/__tests__/IntentTest.js b/src/node_path/lib/__tests__/IntentTest.js new file mode 100644 index 0000000..6865f18 --- /dev/null +++ b/src/node_path/lib/__tests__/IntentTest.js @@ -0,0 +1,53 @@ +"use strict"; + +var _Intent = require("../Intent"); + +var _Intent2 = _interopRequireDefault(_Intent); + +var _sinon = require("sinon"); + +var _sinon2 = _interopRequireDefault(_sinon); + +var _chai = require("chai"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const undef = undefined; // eslint-disable-line + +describe("IntentTest", function () { + it("uses the methods on an intent", function (done) { + const results = []; + const payload = {}; + const action = _sinon2.default.spy(); + const intent = _Intent2.default.of(action, payload, { + write(result) { + try { + if (result.type !== "test4") return results.push(result); + + (0, _chai.expect)(results.concat(result)).to.eql([{ + type: "test", + payload: undef + }, { + type: "test2", + payload: undef + }, { + type: "test3", + payload: { + id: "test" + } + }, { + type: "test4", + payload: undef + }]); + return done(); + } catch (e) { + return done(e); + } + } + }); + + intent.trigger("test").after(10).trigger("test2").trigger("test3", { id: "test" }).after(20).trigger("test4"); + }); +}); + +//# sourceMappingURL=IntentTest.js.map \ No newline at end of file diff --git a/src/node_path/lib/__tests__/QuarkTest.js b/src/node_path/lib/__tests__/QuarkTest.js new file mode 100644 index 0000000..d1169dd --- /dev/null +++ b/src/node_path/lib/__tests__/QuarkTest.js @@ -0,0 +1,110 @@ +"use strict"; + +var _Gluon = require("../Gluon"); + +var _Gluon2 = _interopRequireDefault(_Gluon); + +var _Quark = require("../Quark"); + +var _Quark2 = _interopRequireDefault(_Quark); + +var _sinon = require("sinon"); + +var _sinon2 = _interopRequireDefault(_sinon); + +var _expectStream = require("expect-stream"); + +var _stream = require("stream"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +describe("QuarkTest", function () { + beforeEach(function () { + const stream = new _stream.Duplex({ + objectMode: true, + + read() { + setTimeout(() => [{ + type: "add", + payload: "guckguck" + }, { + type: "sub" + }].forEach(this.push.bind(this)), 30); + }, + + write(data, enc, cb) { + cb(); + } + }); + + this.gluon = _sinon2.default.stub(_Gluon2.default, "of", () => stream); // eslint-disable-line + }); + + afterEach(function () { + this.gluon.restore(); // eslint-disable-line + }); + + it("uses quark", function (done) { + const quark = _Quark2.default.of({ + initialState: { + test: "test" + }, + + intents: { + onAdd: (state, payload) => state.set("test", payload), + onBla: (state, x) => state.set("test3", x), + + blub(state) { + this.after(50).trigger("bla", 2).trigger("add", "last"); + + return state.set("test2", "huhu"); + } + }, + + mappings: { + sub: "blub" + }, + + qml: "test" + }); + + (0, _expectStream.expect)(quark).to.exactly.produce([{ + processes: [], + qml: "test", + test: "test" + }, { + processes: [], + qml: "test", + test: "guckguck" + }, { + processes: [], + qml: "test", + test: "guckguck" + }, { + processes: [], + qml: "test", + test: "guckguck", + test2: "huhu" + }, { + processes: [], + qml: "test", + test: "guckguck", + test2: "huhu", + test3: 2 + }, { + processes: [], + qml: "test", + test: "guckguck", + test2: "huhu", + test3: 2 + }, { + processes: [], + qml: "test", + test: "last", + test2: "huhu", + test3: 2 + }]).notify(done); + }); +}); + +//# sourceMappingURL=QuarkTest.js.map \ No newline at end of file diff --git a/src/node_path/lib/__tests__/SelectorTest.js b/src/node_path/lib/__tests__/SelectorTest.js new file mode 100644 index 0000000..98dca50 --- /dev/null +++ b/src/node_path/lib/__tests__/SelectorTest.js @@ -0,0 +1,38 @@ +"use strict"; + +var _Selector = require("../Selector"); + +var _Selector2 = _interopRequireDefault(_Selector); + +var _expectStream = require("expect-stream"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +describe("SelectorTest", function () { + it("uses a selector stream", function (done) { + (0, _expectStream.expect)(_Selector2.default.of("test/test/test")).to.exactly.produce("test", { test: "test" }, undefined) // eslint-disable-line + .on({ + test: { + test: { + test: "test" + } + } + }, { + test: { + test: { + test: "test" + } + } + }, { + test: { + test: { + test: { + test: "test" + } + } + } + }, {}).notify(done); + }); +}); + +//# sourceMappingURL=SelectorTest.js.map \ No newline at end of file diff --git a/src/node_path/lib/__tests__/StoreTest.js b/src/node_path/lib/__tests__/StoreTest.js new file mode 100644 index 0000000..01c3286 --- /dev/null +++ b/src/node_path/lib/__tests__/StoreTest.js @@ -0,0 +1,70 @@ +"use strict"; + +var _promise = require("babel-runtime/core-js/promise"); + +var _promise2 = _interopRequireDefault(_promise); + +var _Store = require("../Store"); + +var _Store2 = _interopRequireDefault(_Store); + +var _expectStream = require("expect-stream"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +describe("StoreTest", function () { + it("uses the methods on a store", function (done) { + const state1 = { + users: [{ + id: 0, + name: "jupp" + }, { + id: 1, + name: "hansi" + }], + state: "LOGGED_IN" + }; + + const state2 = { + users: [{ + id: 0, + name: "jupp" + }], + state: "LOGGED_OUT", + test: "test" + }; + + (0, _expectStream.expect)(_Store2.default.of(state1)).to.exactly.produce(state1, state2).on({ + intent: (state, x) => state.mergeDeep(x), + payload: state1 + }, { + intent: x => new _promise2.default(resolve => resolve(x)) + }, { + intent: (state, x) => state.mergeDeep(x), + payload: state2 + }).notify(done); + }); + + it("listens to updates", function (done) { + const loggedIn = { + state: { + loggedIn: true + } + }; + + const loggedOut = { + state: { + loggedIn: false + } + }; + const store = _Store2.default.of(loggedIn); + + (0, _expectStream.expect)(store.listen("state/loggedIn")).to.exactly.produce(true, false, true).notify(done); + + store.write({ intent: state => state.mergeDeep(loggedOut) }); + store.write({ intent: state => state.mergeDeep(loggedOut) }); + store.write({ intent: state => state.mergeDeep(loggedIn) }); + }); +}); + +//# sourceMappingURL=StoreTest.js.map \ No newline at end of file diff --git a/src/node_path/package.json b/src/node_path/package.json index 0e31dde..2a8e766 100644 --- a/src/node_path/package.json +++ b/src/node_path/package.json @@ -4,15 +4,27 @@ "main": "quark.js", "dependencies": { "immutable": "^3.8.1", + "immutablediff": "^0.4.4", "jsonstream2": "^1.1.1", + "lodash.assign": "^4.2.0", "lodash.set": "^4.3.2", "through2-filter": "^2.0.0", "through2-map": "^3.0.0" }, "devDependencies": { + "expect-stream": "^0.1.0", + "babel-cli": "^6.18.0", + "babel-eslint": "^7.1.1", + "babel-plugin-transform-class-properties": "^6.19.0", + "babel-plugin-transform-runtime": "^6.15.0", + "babel-preset-es2016": "^6.16.0", + "babel-preset-es2017": "^6.16.0", + "babel-preset-node6": "^11.0.0", "chai": "^3.5.0", + "eslint": "^3.10.2", "istanbul": "^0.4.5", "mocha": "^3.1.2", + "sinon": "^1.17.6", "source-map-support": "^0.4.6" } } diff --git a/src/node_path/quark.js b/src/node_path/quark.js index df58e00..966f301 100644 --- a/src/node_path/quark.js +++ b/src/node_path/quark.js @@ -1,59 +1 @@ -const stream = require("stream"); -const Duplex = stream.Duplex; -const GCD = require("./GCD"); -const Store = require("./Store"); -const View = require("./Gluon"); - -module.exports = class Quark extends Duplex { - static of(...args) { - return new Quark(...args); - } - - constructor({ initialState: state, intents, qml, mappings }) { - super({ - objectMode: true - }); - - // TODO options parsen und hinzufügen - this.store = Store.of(Object.assign({ qml, processes: [] }, state || {})); - this.gcd = GCD.of(intents || {}, mappings); - this.view = View.of(qml); - this.qml = qml; - this.timeout = 0; - - this.store.on("data", this.push.bind(this)); - this.view - .pipe(this.gcd) - .pipe(this.store) - .pipe(this.view); - } - - after(timeout) { - this.timeout = timeout; - - return this; - } - - trigger(type, payload) { - setTimeout(() => this.write({ type, payload }), this.timeout); - - return this.after(0); - } - - listen(path) { - return this.store.listen(path); - } - - write(...args) { - super.write(...args); - - return this; - } - - _write(data, enc, cb) { - this.gcd.write(data); - cb(); - } - - _read() {} -} +module.exports = require("./lib/Quark").default; diff --git a/src/node_path/src/GCD.js b/src/node_path/src/GCD.js new file mode 100644 index 0000000..985ffd7 --- /dev/null +++ b/src/node_path/src/GCD.js @@ -0,0 +1,44 @@ +import { Transform } from "stream"; +import assert from "assert"; +import set from "lodash.set"; +import Intent from "./Intent"; + +export default class GCD extends Transform { + static of(...args) { + return new GCD(...args); + } + + constructor(intents = {}, mappings = {}) { + super({ + objectMode: true + }); + + this.intents = intents; + this.mappings = Object.assign(Object.keys(intents) + .reduce((dest, key) => set(dest, this.toAction(key), key), {}), mappings); + + this.mappingsString = JSON.stringify(this.mappings); + } + + toAction(key) { + return key + .slice(2, 3) + .toLowerCase() + .concat(key.slice(3)); + } + + _transform(data, enc, cb) { + assert(data && typeof data.type === "string", `Your action is in the wrong format. Expected an object with key type, but got '${JSON.stringify(data)}' of type ${typeof data}.`); + + const key = this.mappings[data.type]; + + assert(typeof key === "string", `There is exists no mapping for '${data.type}' in ${this.mappingsString}.`); + + const intent = this.intents[key]; + + assert(typeof intent === "function", `No intent found for '${data.type}' -> '${key}' in intents [${Object.keys(this.intents)}].`); + + this.push(Intent.of(intent, data.payload, this)); + cb(); + } +} diff --git a/src/node_path/Gluon.js b/src/node_path/src/Gluon.js similarity index 68% rename from src/node_path/Gluon.js rename to src/node_path/src/Gluon.js index 5d6c96b..c9d5937 100644 --- a/src/node_path/Gluon.js +++ b/src/node_path/src/Gluon.js @@ -1,47 +1,48 @@ -const util = require("util"); -const Stream = require("stream"); -const StringDecoder = require("string_decoder").StringDecoder; -const Transform = Stream.Transform; -const Duplex = Stream.Duplex; -const JSONStream = require("jsonstream2"); -const Filter = require("through2-filter"); -const Mapper = require("through2-map"); +import { Duplex, Transform } from "stream"; +import Stream from "stream"; +import JSONStream from "jsonstream2"; +import Filter from "through2-filter"; +import Mapper from "through2-map"; // hier das is wg qt seite komisch const JSONStringifier = () => new Transform({ objectMode: true, transform(chunk, enc, cb) { - cb(null, `${JSON.stringify(chunk)}\n`); + cb(null, `${JSON.stringify(chunk)}\n`); } }); -class Gluon extends Duplex { +export default class Gluon extends Duplex { + static opts = { + objectMode: true + }; + static of(...args) { return new Gluon(...args); } static Output(type) { - const out = new Stream(Gluon.opts);; + const out = new Stream(Gluon.opts); out .pipe(Mapper(Gluon.opts, payload => ({ type, payload }))) .pipe(JSONStringifier()) - .pipe(process.stdout); + .pipe(global.process.stdout); return out; } constructor(qmlPath) { super({ - objectMode: true + objectMode: true }); this.initialLoad = true; this.qmlPath = qmlPath; this.valueOut = Gluon.Output("value"); this.actionOut = Gluon.Output("action"); - this.actions = process.stdin + this.actions = global.process.stdin .pipe(JSONStream.parse()) .pipe(Filter(Gluon.opts, msg => msg.type === "action")) .pipe(Mapper(Gluon.opts, msg => msg.payload)); @@ -57,18 +58,23 @@ class Gluon extends Duplex { this.actionOut.emit("data", { type: "loadQml", payload: { - url + url } }); return url; } + trim(path) { + return path + .slice(1) + .replace(/\//g, "\\"); + } + start(process) { this.actionOut.emit("data", { type: "startProcess", - // hier die regexp weg - payload: process.slice(1).replace(new RegExp("/", "g"),"\\") + payload: this.trim(process) }); return process; @@ -77,7 +83,7 @@ class Gluon extends Duplex { kill(process) { this.actionOut.emit("data", { type: "killProcess", - payload: process + payload: this.trim(process) }); return process; @@ -93,9 +99,3 @@ class Gluon extends Duplex { _read() {} } - -Gluon.opts = { - objectMode: true -}; - -module.exports = Gluon; diff --git a/src/node_path/src/Intent.js b/src/node_path/src/Intent.js new file mode 100644 index 0000000..bdd4013 --- /dev/null +++ b/src/node_path/src/Intent.js @@ -0,0 +1,24 @@ +export default class Intent { + static of(...args) { + return new Intent(...args); + } + + constructor(intent, payload, stream) { + this.timeout = 0; + this.intent = intent.bind(this); + this.payload = payload; + this.stream = stream; + } + + after(timeout) { + this.timeout = this.timeout + timeout; + + return this; + } + + trigger(type, payload) { + setTimeout(() => this.stream.write({ type, payload }), this.timeout); + + return this; + } +} diff --git a/src/node_path/src/Quark.js b/src/node_path/src/Quark.js new file mode 100644 index 0000000..70c5274 --- /dev/null +++ b/src/node_path/src/Quark.js @@ -0,0 +1,67 @@ +import { Duplex } from "stream"; +import GCD from "./GCD"; +import Store from "./Store"; +import Gluon from "./Gluon"; +import Statechart from "./Statechart"; + +export default class Quark extends Duplex { + static Statechart = Statechart; + + static of(...args) { + return new Quark(...args); + } + + constructor({ initialState: state, intents, qml, mappings }) { + super({ + objectMode: true + }); + + // TODO options parsen und hinzufügen + this.store = Store.of(Object.assign({ qml, processes: [] }, state || {})); + this.gcd = GCD.of(intents || {}, mappings); + this.view = Gluon.of(qml); + this.qml = qml; + this.timeout = 0; + this.buffer = []; + + this.store.on("data", this.buffer.push.bind(this.buffer)); + this.view + .pipe(this.gcd) + .pipe(this.store) + .pipe(this.view); + } + + after(timeout) { + this.timeout = timeout; + + return this; + } + + trigger(type, payload) { + setTimeout(() => this.write({ type, payload }), this.timeout); + + return this.after(0); + } + + listen(path) { + return this.store.listen(path); + } + + write(...args) { + super.write(...args); + + return this; + } + + _write(data, enc, cb) { + this.gcd.write(data); + cb(); + } + + _read() { // eslint-disable-line + if(this.buffer.length === 0) return setTimeout(this._read.bind(this), 17); // eslint-disable-line + + this.buffer.forEach(this.push.bind(this)); + this.buffer.length = 0; + } +} diff --git a/src/node_path/src/Selector.js b/src/node_path/src/Selector.js new file mode 100644 index 0000000..f2afba6 --- /dev/null +++ b/src/node_path/src/Selector.js @@ -0,0 +1,26 @@ +import { Transform } from "stream"; + +export default class Selector extends Transform { + static of(...args) { + return new Selector(...args); + } + + constructor(path) { + super({ + objectMode: true + }); + + this.keys = path.split("/"); + this.selection = null; + } + + _transform(data, enc, cb) { + const selection = this.keys.reduce((slice, key) => slice && slice[key], data); + + if(selection === this.selection) return cb(); + + this.selection = selection; + + return cb(null, selection); + } +} diff --git a/src/node_path/src/Statechart.js b/src/node_path/src/Statechart.js new file mode 100644 index 0000000..b674c00 --- /dev/null +++ b/src/node_path/src/Statechart.js @@ -0,0 +1,11 @@ +const assign = require("lodash.assign"); + +export default class Statechart { + static of(...args) { + return new Statechart(...args); + } + + constructor(description) { + return assign(this, description); + } +} diff --git a/src/node_path/src/Store.js b/src/node_path/src/Store.js new file mode 100644 index 0000000..322484b --- /dev/null +++ b/src/node_path/src/Store.js @@ -0,0 +1,55 @@ +import { Transform } from "stream"; +import Immutable from "immutable"; +import Selector from "./Selector"; +import diff from "immutablediff"; +import assert from "assert"; + +export default class Store extends Transform { + static of(...args) { + return new Store(...args); + } + + constructor(state) { + super({ + objectMode: true + }); + + // FIXME: das hier macht iwie mucken, is aber jucking, wenn + // auf c++ seite json patch benutzt wird, dadurch sind auch + // die diffs atm nur auf einer ebene sinnvoll, des weiteren + // muss en weg gefunden werden, wie neue records sinnvoll in + // den immutable context gebracht werden + // this.state = Immutable.fromJS(state); + + this.state = Immutable.Map(state); + + this.push(this.state.toJSON()); + } + + onResult(cb, state) { + assert(state instanceof Immutable.Map, "unexpected state"); + + const difference = diff(this.state, state).toJSON(); + + if(difference.length === 0) return cb(); + + // TODO: diff here + console.error(`diff: ${JSON.stringify(difference)}`); + this.state = state; + this.push(this.state.toJSON()); + + return cb(); + } + + _transform({ intent, payload }, enc, cb) { + const result = intent(this.state, payload); + + if(result.then) return result.then(this.onResult.bind(this, cb)).catch(cb); + + return this.onResult(cb, result); + } + + listen(path) { + return this.pipe(Selector.of(path)); + } +} diff --git a/src/node_path/src/__tests__/GCDTest.js b/src/node_path/src/__tests__/GCDTest.js new file mode 100644 index 0000000..682fd8d --- /dev/null +++ b/src/node_path/src/__tests__/GCDTest.js @@ -0,0 +1,85 @@ +import GCD from "../GCD"; +import { expect } from "expect-stream"; +import sinon from "sinon"; + +describe("GCDTest", function() { + it("lets the gcd dispatch some intents", function(done) { + const intents = { + onData: sinon.spy(), + blub: sinon.spy() + }; + + const gcd = GCD.of(intents, { + test: "blub" + }); + + expect(gcd) + .to.produce(result => { + result.intent(); + + if( + result.payload && + result.payload.key === "test" && + intents.onData.callCount === 1 && + intents.blub.callCount === 0 + ) return 0; + + if( + result.payload && + result.payload.key === "test2" && + intents.onData.callCount === 1 && + intents.blub.callCount === 1 + ) return 1; + + return -1; + }) + .on({ + type: "data", + payload: { + key: "test" + } + }, { + type: "test", + payload: { + key: "test2" + } + }) + .notify(done); + }); + + it("produces an error when feeding malformed data", function() { + const gcd = GCD.of(); + + try { + gcd.write({}); + } catch(e) { + expect(e.message).to.equal("Your action is in the wrong format. Expected an object with key type, but got \'{}\' of type object."); + } + }); + + it("produces an error for a non existing mapping", function() { + const gcd = GCD.of(); + + try { + gcd.write({ + type: "test" + }); + } catch(e) { + expect(e.message).to.equal("There is exists no mapping for \'test\' in {}."); + } + }); + + it("produces an error for an unknown action", function() { + const gcd = GCD.of({}, { + test: "blub" + }); + + try { + gcd.write({ + type: "test" + }); + } catch(e) { + expect(e.message).to.equal("No intent found for \'test\' -> \'blub\' in intents []."); + } + }); +}); diff --git a/src/node_path/src/__tests__/GluonTest.js b/src/node_path/src/__tests__/GluonTest.js new file mode 100644 index 0000000..48d70ad --- /dev/null +++ b/src/node_path/src/__tests__/GluonTest.js @@ -0,0 +1,91 @@ +import sinon from "sinon"; +import { Transform } from "stream"; +import { expect } from "expect-stream"; +import Gluon from "../Gluon"; + +describe("GluonTest", function() { + beforeEach(function() { + this.stdin = sinon.stub(global.process.stdin, "pipe", stream => { // eslint-disable-line + this.stream = stream; // eslint-disable-line + + return stream; + }); + + const write = global.process.stdout.write; + + this.out = new Transform({ // eslint-disable-line + transform(data, enc, cb) { + try { + JSON.parse(data); + + return cb(null, data); + } catch(e) { + // because mocha uses stdout too, we need an easy way to filter it's + // messages. Looking for parseability is the easiest one :D + return write.call(process.stdout, data); + } + } + }); + + this.stdout = sinon.stub(global.process.stdout, "write", this.out.write.bind(this.out)); // eslint-disable-line + }); + + afterEach(function() { + this.stdin.restore(); // eslint-disable-line + this.stdout.restore(); // eslint-disable-line + }); + + it("uses gluon to read", function(done) { + expect(Gluon.of("test")) + .to.produce([undefined, { // eslint-disable-line + test: "test" + }]) + .notify(done); + + const stream = this.stream; // eslint-disable-line + + stream.write(JSON.stringify({ + type: "value", + payload: { + test: "test" + } + })); + + stream.write(JSON.stringify({ + type: "action" + })); + + stream.write(JSON.stringify({ + type: "action", + payload: { + test: "test" + } + })); + }); + + it("uses gluon to write", function(done) { + expect(this.out) // eslint-disable-line + .to.exactly.produce([ + "{\"type\":\"value\",\"payload\":{\"test\":\"test\"}}\n", + "{\"type\":\"action\",\"payload\":{\"type\":\"loadQml\",\"payload\":{\"url\":\"path\"}}}\n", + "{\"type\":\"value\",\"payload\":{\"qml\":\"test2\"}}\n", + "{\"type\":\"action\",\"payload\":{\"type\":\"loadQml\",\"payload\":{\"url\":\"test2\"}}}\n", + "{\"type\":\"action\",\"payload\":{\"type\":\"startProcess\",\"payload\":\"blub\\\\prog\\\\prog\"}}\n", + "{\"type\":\"action\",\"payload\":{\"type\":\"killProcess\",\"payload\":\"blub\\\\prog\\\\prog\"}}\n" + ]) + .notify(done); + + const gluon = Gluon.of("path"); + + gluon.write({ + test: "test" + }); + + gluon.write({ + qml: "test2" + }); + + gluon.start("/blub/prog/prog"); + gluon.kill("/blub/prog/prog"); + }); +}); diff --git a/src/node_path/src/__tests__/IntentTest.js b/src/node_path/src/__tests__/IntentTest.js new file mode 100644 index 0000000..1bcacb9 --- /dev/null +++ b/src/node_path/src/__tests__/IntentTest.js @@ -0,0 +1,47 @@ +import Intent from "../Intent"; +import sinon from "sinon"; +import { expect } from "chai"; + +const undef = undefined // eslint-disable-line + +describe("IntentTest", function() { + it("uses the methods on an intent", function(done) { + const results = []; + const payload = {}; + const action = sinon.spy(); + const intent = Intent.of(action, payload, { + write(result) { + try { + if(result.type !== "test4") return results.push(result); + + expect(results.concat(result)).to.eql([{ + type: "test", + payload: undef + }, { + type: "test2", + payload: undef + }, { + type: "test3", + payload: { + id: "test" + } + }, { + type: "test4", + payload: undef + }]); + return done(); + } catch(e) { + return done(e); + } + } + }); + + intent + .trigger("test") + .after(10) + .trigger("test2") + .trigger("test3", { id: "test" }) + .after(20) + .trigger("test4"); + }); +}); diff --git a/src/node_path/src/__tests__/QuarkTest.js b/src/node_path/src/__tests__/QuarkTest.js new file mode 100644 index 0000000..6369b40 --- /dev/null +++ b/src/node_path/src/__tests__/QuarkTest.js @@ -0,0 +1,98 @@ +import Gluon from "../Gluon"; +import Quark from "../Quark"; +import sinon from "sinon"; +import { expect } from "expect-stream"; +import { Duplex } from "stream"; + +describe("QuarkTest", function() { + beforeEach(function() { + const stream = new Duplex({ + objectMode: true, + + read() { + setTimeout(() => [{ + type: "add", + payload: "guckguck" + }, { + type: "sub" + }].forEach(this.push.bind(this)), 30); + }, + + write(data, enc, cb) { + cb(); + } + }); + + this.gluon = sinon.stub(Gluon, "of", () => stream); // eslint-disable-line + }); + + afterEach(function() { + this.gluon.restore(); // eslint-disable-line + }); + + it("uses quark", function(done) { + const quark = Quark.of({ + initialState: { + test: "test" + }, + + intents: { + onAdd: (state, payload) => state.set("test", payload), + onBla: (state, x) => state.set("test3", x), + + blub(state) { + this.after(50) + .trigger("bla", 2) + .trigger("add", "last"); + + return state.set("test2", "huhu"); + } + }, + + mappings: { + sub: "blub" + }, + + qml: "test" + }); + + expect(quark) + .to.exactly.produce([{ + processes: [], + qml: "test", + test: "test" + }, { + processes: [], + qml: "test", + test: "guckguck" + }, { + processes: [], + qml: "test", + test: "guckguck" + }, { + processes: [], + qml: "test", + test: "guckguck", + test2: "huhu" + }, { + processes: [], + qml: "test", + test: "guckguck", + test2: "huhu", + test3: 2 + }, { + processes: [], + qml: "test", + test: "guckguck", + test2: "huhu", + test3: 2 + }, { + processes: [], + qml: "test", + test: "last", + test2: "huhu", + test3: 2 + }]) + .notify(done); + }); +}); diff --git a/src/node_path/src/__tests__/SelectorTest.js b/src/node_path/src/__tests__/SelectorTest.js new file mode 100644 index 0000000..f14ce31 --- /dev/null +++ b/src/node_path/src/__tests__/SelectorTest.js @@ -0,0 +1,31 @@ +import Selector from "../Selector"; +import { expect } from "expect-stream"; + +describe("SelectorTest", function() { + it("uses a selector stream", function(done) { + expect(Selector.of("test/test/test")) + .to.exactly.produce("test", { test: "test" }, undefined) // eslint-disable-line + .on({ + test: { + test: { + test: "test" + } + } + }, { + test: { + test: { + test: "test" + } + } + }, { + test: { + test: { + test: { + test: "test" + } + } + } + }, {}) + .notify(done); + }); +}); diff --git a/src/node_path/src/__tests__/StoreTest.js b/src/node_path/src/__tests__/StoreTest.js new file mode 100644 index 0000000..20b2d2e --- /dev/null +++ b/src/node_path/src/__tests__/StoreTest.js @@ -0,0 +1,62 @@ +import Store from "../Store"; +import { expect } from "expect-stream"; + +describe("StoreTest", function() { + it("uses the methods on a store", function(done) { + const state1 = { + users: [{ + id: 0, + name: "jupp" + }, { + id: 1, + name: "hansi" + }], + state: "LOGGED_IN" + }; + + const state2 = { + users: [{ + id: 0, + name: "jupp" + }], + state: "LOGGED_OUT", + test: "test" + }; + + expect(Store.of(state1)) + .to.exactly.produce(state1, state2) + .on({ + intent: (state, x) => state.mergeDeep(x), + payload: state1 + }, { + intent: x => new Promise(resolve => resolve(x)) + }, { + intent: (state, x) => state.mergeDeep(x), + payload: state2 + }) + .notify(done); + }); + + it("listens to updates", function(done) { + const loggedIn = { + state: { + loggedIn: true + } + }; + + const loggedOut = { + state: { + loggedIn: false + } + }; + const store = Store.of(loggedIn); + + expect(store.listen("state/loggedIn")) + .to.exactly.produce(true, false, true) + .notify(done); + + store.write({ intent: state => state.mergeDeep(loggedOut) }); + store.write({ intent: state => state.mergeDeep(loggedOut) }); + store.write({ intent: state => state.mergeDeep(loggedIn) }); + }); +});