diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f500ad --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.xml] +indent_style = space +indent_size = 4 + +[*.json] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore index df3b8ef..96e2e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,12 @@ .*.swp /node_modules +.idea/ +yarn.lock + +object.d.ts +object.js +object.js.map + +async.d.ts +async.js +async.js.map diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..7deaa21 --- /dev/null +++ b/.npmignore @@ -0,0 +1,7 @@ +.*.swp +/node_modules +.idea/ +yarn.lock + +object.ts +async.ts diff --git a/api.d.ts b/api.d.ts new file mode 100644 index 0000000..55da799 --- /dev/null +++ b/api.d.ts @@ -0,0 +1,95 @@ +type Buffer = Uint8Array | ArrayBuffer; +type ByteArray = any[]; + +declare class Encoder { + encode(term): ByteArray; + + undefined(x): ByteArray; + + null(x): ByteArray; + + number(x): ByteArray; + + int(x): ByteArray; + + array(x): ByteArray; + + object(x): ByteArray; + + atom(x): ByteArray; + + tuple(x): ByteArray; + + buffer(x): ByteArray; + + string(x): ByteArray; + + boolean(x): ByteArray; +} + +interface Encode { + (term: any): Buffer; + + Encoder: Encoder; + + optlist_to_term(opts: any[]): any[]; + + optlist_to_binary(opts: any[]): Buffer; +} + +declare class Decoder { + constructor(bin: ArrayBuffer) + + decode(): any; + + SMALL_INTEGER(): any; + + INTEGER(): any; + + STRING(): any; + + ATOM(): any; + + LIST(): any; + + LARGE_TUPLE(): any; + + SMALL_TUPLE(): any; + + BINARY(): any; +} + +interface Decode { + (term: Buffer): any; + + Decoder: Decoder +} + +interface IOList { + to_buffer(list): Buffer + + size(list): number; +} + +declare let encode: Encode; +declare let decode: Decode; +declare let iolist: IOList; + +export function term_to_binary(term: any): Buffer; + +export function optlist_to_term(opts: any[]): any[]; + +export function optlist_to_binary(opts: any[]): Buffer; + + +export function binary_to_term(term: Buffer): any; + + +export function iolist_to_binary(list): Buffer; + +export function iolist_to_buffer(list): Buffer; + +export function iolist_size(list): number; + + +export function blob_to_term(blob: Blob): Promise; diff --git a/api.js b/api.js index e5a18da..a7a249a 100644 --- a/api.js +++ b/api.js @@ -13,6 +13,12 @@ var encode = require('./encode.js') var decode = require('./decode.js') var iolist = require('./iolist.js') +var async = require('./async.js') + +function blob_to_term(blob){ + return async.blob_to_buffer(blob) + .then(decode) +} module.exports = { term_to_binary : encode @@ -24,4 +30,6 @@ module.exports = , iolist_to_binary : iolist.to_buffer , iolist_to_buffer : iolist.to_buffer , iolist_size : iolist.size + + , blob_to_term : blob_to_term } diff --git a/async.ts b/async.ts new file mode 100644 index 0000000..54645e8 --- /dev/null +++ b/async.ts @@ -0,0 +1,14 @@ +import * as arraybufferToBuffer from "arraybuffer-to-buffer"; + +export function blob_to_buffer(blob: Blob) { + return new Promise(((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const buf = reader.result as ArrayBuffer; + const bs = arraybufferToBuffer(buf); + resolve(bs); + }; + reader.onerror = reject; + reader.readAsArrayBuffer(blob); + })); +} diff --git a/decode.js b/decode.js index d7ea4cf..44eafed 100644 --- a/decode.js +++ b/decode.js @@ -16,10 +16,10 @@ module.exports.Decoder = Decoder //module.exports.term_to_optlist = term_to_optlist //module.exports.binary_to_optlist = binary_to_optlist -var util = require('util') var debug = require('debug')('erlang:decode') var lib = require('./lib.js') +var object = require('./object.js') function binary_to_term(term) { if (!Buffer.isBuffer(term)) @@ -95,6 +95,8 @@ Decoder.prototype.ATOM = function() { term = true else if (term == 'nil') term = null + else if (term == 'undefined') + term = undefined else term = {a:term} @@ -140,6 +142,9 @@ Decoder.prototype.SMALL_TUPLE = function() { this.bin = body.bin debug('Small tuple %j', this.bin) + if (term[0] === object.map_optlist_tag) { + return object.optlist_to_object(term); + } return {t:term} } diff --git a/encode.js b/encode.js index c3f83a0..04c7f03 100644 --- a/encode.js +++ b/encode.js @@ -18,6 +18,7 @@ module.exports.optlist_to_binary = optlist_to_binary var util = require('util') var lib = require('./lib.js') +var object = require('./object.js') var typeOf = lib.typeOf function Encoder () { @@ -33,6 +34,10 @@ Encoder.prototype.encode = function(term) { return encoder.apply(this, [term]) } +Encoder.prototype.undefined = function(x) { + return this.atom('undefined') +} + Encoder.prototype.null = function(x) { return this.atom('nil') } @@ -74,8 +79,11 @@ Encoder.prototype.array = function(x) { Encoder.prototype.object = function(x) { var keys = Object.keys(x) - if(keys.length !== 1) - throw new Error("Don't know how to process: " + util.inspect(x)) + if(keys.length !== 1){ + // throw new Error("Don't know how to process: " + util.inspect(x)) + var res = object.object_to_optlist(x); + return this.encode(res); + } var tag = keys[0] var val = x[tag] diff --git a/object.ts b/object.ts new file mode 100644 index 0000000..86e9c6f --- /dev/null +++ b/object.ts @@ -0,0 +1,114 @@ +export const map_optlist_tag = "map_optlist"; +export const map_list_tag = "map_list"; +import * as util from "util"; + +export type format = "tagged_optlist" | "optlist" | "list" ; +const default_format: format = "tagged_optlist"; + +const isTuple = t => !!t && (Array.isArray(t.t) || Array.isArray(t.tuple)); + +export function getTupleArray(term): any[] { + if (!term) { + throw new TypeError("argument is not tuple: " + util.inspect(term)); + } + const xs = term.t || term.tuple; + if (Array.isArray(xs)) { + return xs; + } + throw new TypeError("argument is not tuple: " + util.inspect(term)); +} + +/** + * @param x: Javascript term + * @param format: type format + * */ +const map_object = (x, format) => { + return typeof x === "object" && x !== null && !Array.isArray(x) + ? object_to_optlist(x) + : Array.isArray(x) + ? x.map(x => map_object(x, format)) + : x; +}; + +/** + * @param x: Erlang term + * @param format: type format + * */ +const map_list = (x, format) => { + if (isTuple(x)) { + const t = x.t || x.tuple; + if (t.length === 2 && t[0] === map_list_tag) { + return optlist_to_object(t[1], format); + } + return {t: map_list(t, format)}; + } + if (Array.isArray(x)) { + return x.map(x => map_list(x, format)); + } + return x; +}; + +/** + * object -> optlist + * */ +export function object_to_optlist(o: any, format: format = default_format): any { + switch (format) { + case "tagged_optlist": + case "optlist": { + const vs = Object.keys(o) + .map(x => ({t: [x, map_object(o[x], format)]})); + return format === "tagged_optlist" + ? {t: [map_optlist_tag, vs]} + : vs; + } + case "list": { + const vs = []; + Object.keys(o) + .forEach(x => vs.push(x, map_object(o[x], format))); + return {t: [map_list_tag, vs]}; + } + default: + throw new TypeError("unsupported format: " + util.inspect(format)); + } +} + +/** + * optlist -> object + * */ +export function optlist_to_object(term: [any, any], format: format = default_format): any { + if (!term) { + throw new TypeError("unsupported term: " + util.inspect(term)); + } + switch (format) { + case "tagged_optlist": + case "optlist": { + if (format === "tagged_optlist") { + if (term.length !== 2) { + throw new Error("term should be tuple of 2 elements: " + util.inspect(term)); + } + term = term[1]; + } + const o = {}; + term.forEach(tuple => { + const xs = getTupleArray(tuple); + if (xs.length !== 2) { + throw new Error("tuple should be size of 2: " + util.inspect(tuple)); + } + o[xs[0]] = map_list(xs[1], format); + }); + return o; + } + case "list": { + const o = {}; + if (term.length % 2 !== 0) { + throw new Error("invalid list, should be even number of element: " + util.inspect(term)); + } + for (let i = 0; i < term.length; i += 2) { + o[term[i]] = map_list(term[i + 1], format); + } + return o; + } + default: + throw new TypeError("unsupported format: " + util.inspect(format)); + } +} diff --git a/package.json b/package.json index e6a32ba..4187fbb 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,51 @@ -{ "name": "erlang" -, "version": "1.0.1" -, "author": { "name": "Jason Smith", "email": "jason.h.smith@gmail.com" } -, "description": "Erlang interoperability with Javascript" -, "homepage": "http://github.com/iriscouch/erlang.js" -, "repository": { "type": "git" - , "url": "git://github.com/iriscouch/erlang.js" } -, "engines": [ "node" ] -, "main": "./api.js" -, "scripts": { "test" : "tap test/" - } - -, "dependencies": { "debug": "~1.0.4" - } - -, "devDependencies": { "tap": "~0.4.8" - , "async": "~0.2.10" - , "traceback": "~0.3.1" - } +{ + "name": "@beenotung/erlang", + "version": "1.4.3", + "author": "Jason Smith ", + "contributors": [ + "Beeno Tung " + ], + "description": "Erlang interoperability with Javascript", + "homepage": "http://github.com/beenotung/erlang.js", + "repository": { + "type": "git", + "url": "git://github.com/beenotung/erlang.js.git" + }, + "bugs": { + "url": "https://github.com/beenotung/erlang.js/issues" + }, + "main": "./api.js", + "types": "./api.d.ts", + "scripts": { + "lint": "tslint -p .", + "build": "tsc && run-p buildType:*", + "buildType:async": "tsc -d async.ts || true", + "buildType:object": "tsc -d object.ts || true", + "pretest": "npm run build", + "prepublishOnly": "npm test", + "test": "npm run lint && tap test/" + }, + "dependencies": { + "arraybuffer-to-buffer": "^0.0.4", + "debug": "~1.0.4", + "util": "^0.10.3" + }, + "devDependencies": { + "@angular/compiler": ">=4.0.0-beta", + "@angular/core": ">=4.0.0-beta", + "async": "~0.2.10", + "codelyzer": "^3.2.2", + "npm-run-all": "^4.1.1", + "rxjs": "^5.0.1", + "tap": "~0.4.8", + "traceback": "~0.3.1", + "tslint": "^5.8.0", + "tslint-eslint-rules": "^4.1.1", + "typescript": "^2.5.3", + "zone.js": "^0.8.4" + }, + "directories": { + "test": "test" + }, + "license": "ISC" } diff --git a/test/object.js b/test/object.js new file mode 100644 index 0000000..65d3e50 --- /dev/null +++ b/test/object.js @@ -0,0 +1,51 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +var tap = require('tap') +var test = tap.test +var util = require('util') + +var package = require('../package.json') +var api = require('../' + package.main) +var lib = require('../lib.js') + +test('Simple object codec', function(t) { + var o = {username:'user',password:'123'}; + + var bin = api.term_to_binary(o) + t.equal(bin[0], lib.VERSION_MAGIC, 'Encoded tuple begins with the magic number') + t.equal(bin[1], lib.tags.SMALL_TUPLE, 'Object is encoded as a SMALL_TUPLE') + + var dec = api.binary_to_term(bin) + t.equal(JSON.stringify(o), JSON.stringify(dec), 'Decoded is same as original data') + + t.end() +}) + +var o = {username:'user',password:'123', logs:[{time:123}, {time:234}]}; + +function test_deep_object(format){ + test(`Deep object codec (${format})`, function(t){ + var bin = api.term_to_binary(o, format) + t.equal(bin[0], lib.VERSION_MAGIC, 'Encoded tuple begins with the magic number') + t.equal(bin[1], lib.tags.SMALL_TUPLE, 'Object is encoded as a SMALL_TUPLE') + + var dec = api.binary_to_term(bin, format) + t.equal(JSON.stringify(o), JSON.stringify(dec), 'Decoded is same as original data') + + t.end() + }) +} + +test_deep_object("map_optlist"); +test_deep_object("map_list"); +test_deep_object("util"); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d06afe0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noImplicitThis": true, + "skipLibCheck": true, + "lib": [ + "dom", + "es2015" + ], + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true + }, + "include": [ + "api.d.ts", + "object.ts", + "async.ts" + ], + "exclude": [ + "node_modules" + ], + "compileOnSave": false, + "atom": { + "rewriteTsconfig": false + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..cedb669 --- /dev/null +++ b/tslint.json @@ -0,0 +1,136 @@ +{ + "rules": { + "space-before-function-paren": false, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": false, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": "instance-sandwich" + }, + "static-before-instance", + "variables-before-functions" + ], + "no-arg": true, + "no-bitwise": false, + "no-console": [ + false, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-unused-variable": [ + true + ], + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": false, + "no-shadowed-variable": false, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "double", + "avoid-escape" + ], + "radix": true, + "semicolon": [ + true, + "always", + "ignore-interfaces" + ], + "triple-equals": false, + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "typeof-compare": true, + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "directive-selector": [ + true, + "attribute", + "", + "camelCase" + ], + "component-selector": [ + true, + "element", + "", + "kebab-case" + ], + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": false, + "directive-class-suffix": true, + "no-access-missing-member": true, + "templates-use-public": true, + "invoke-injectable": true + }, + "rulesDirectory": [ + "node_modules/codelyzer", + "node_modules/tslint-eslint-rules/dist/rules" + ] +}