From ad5abe7bac7885ba4f68df7eeb800d2e3b81750b Mon Sep 17 00:00:00 2001 From: dcodeIO Date: Fri, 2 Dec 2016 00:36:24 +0100 Subject: [PATCH] Static pbjs target progress, now generates usable CommonJS code, see #512 --- cli/targets/static.js | 226 ++++++++++++++++++--- runtime.js | 71 +++++++ src/index.js | 1 + src/type.js | 13 +- src/verifier.js | 79 ++------ tests/data/package.js | 420 ++++++++++++++++++++++++++++++++++++++++ tests/package-static.js | 37 ++++ 7 files changed, 758 insertions(+), 89 deletions(-) create mode 100644 runtime.js create mode 100644 tests/data/package.js create mode 100644 tests/package-static.js diff --git a/cli/targets/static.js b/cli/targets/static.js index b400d50a7..80b9bacd7 100644 --- a/cli/targets/static.js +++ b/cli/targets/static.js @@ -2,14 +2,12 @@ module.exports = static_target; static_target.private = true; -// This file contains the beginnings of static code generation. -// It doesn't generate anything useful, yet, but can be used as a starting point. +// Currently, this file contains initial static code for CommonJS modules. // TBD: // - Generate a single file or scaffold an entire project directory? Both? // - Targets: ES5, ES6, TypeScript? CommonJS? AMD? -// - Is there a need for a minimal runtime composed only of Reader/Writer/minimal util? -// - What about generating comments and typescript definitions for non-ts targets? +// - What about generating typescript definitions for non-ts targets? var protobuf = require("../.."); @@ -17,16 +15,18 @@ var Type = protobuf.Type, Service = protobuf.Service, Enum = protobuf.Enum, Namespace = protobuf.Namespace, - codegen = protobuf.util.codegen; + encoder = protobuf.encoder, + decoder = protobuf.decoder, + verifier = protobuf.verifier, + util = protobuf.util; var out = []; +var indent = 0; function static_target(root, options, callback) { tree = {}; try { - out.push("var protobuf = require(\"protobufjs\");"); - out.push("var root = exports;"); - buildNamespace("root", root); + buildNamespace("module.exports", root); callback(null, out.join('\n')); } catch (err) { callback(err); @@ -35,33 +35,215 @@ function static_target(root, options, callback) { } } +function push(line) { + if (line === "") + return out.push(""); + var ind = ""; + for (var i = 0; i < indent; ++i) + ind += " "; + out.push(ind + line); +} + +function pushComment(lines) { + push("/**"); + lines.forEach(function(line, i) { + push(" * " + line); + }); + push(" */"); +} + +function name(name) { + if (!name) + return "$root"; + return name; +} + function buildNamespace(ref, ns) { if (!ns) return; + if (ns.name === "") { // root + push(name(ref) + " = (function() {"); + ++indent; + push('"use strict";'); + push(""); + push("// Minimal static codegen runtime"); + push("var $runtime = require(\"protobufjs/runtime\");") + push(""); + push("// Lazily resolved type references"); + push("var $lazyTypes = [];"); + } else { + push(""); + push("/** @alias " + ns.fullName.substring(1) + " */"); + push(name(ref) + "." + name(ns.name) + " = (function() {"); + ++indent; + } + + if (ns instanceof Type) { + buildType(undefined, ns); + } else if (ns instanceof Service) + buildService(undefined, ns); + else { + push(""); + push("/** @alias " + (ns.name && ns.fullName.substring(1) || "exports") + " */"); + push("var " + name(ns.name) + " = {};"); + } + ns.nestedArray.forEach(function(nested) { - if (nested instanceof Type) - buildType(ref, nested); - else if (nested instanceof Service) - buildService(ref, nested); - else if (nested instanceof Enum) - buildEnum(ref, nested); + if (nested instanceof Enum) + buildEnum(ns.name, nested); else if (nested instanceof Namespace) - buildNamespace(ref, nested); + buildNamespace(ns.name, nested); + }); + push(""); + if (ns.name === "") // root + push("return $runtime.resolve($root, $lazyTypes);"); + else + push("return " + name(ns.name) + ";"); + --indent; + push("})();"); +} + +function buildFunction(type, functionName, gen, scope) { + var lines = gen.str(functionName) + .replace("(this.getCtor())", " $root" + type.fullName) + .split(/\n/g); + push(name(type.name) + "." + functionName + " = (function() {"); + ++indent; + push("/* eslint-disable */"); + Object.keys(scope).forEach(function(key) { + push("var " + key + " = " + scope[key] + ";"); + }); + push("var types; $lazyTypes.push(types = [" + type.fieldsArray.map(function(field) { + return field.resolve().resolvedType + ? JSON.stringify(field.resolvedType.fullName.substring(1)) + : "null"; + }).join(',') + "]);"); + push("return " + lines[0]); + lines.slice(1).forEach(function(line) { + if (line === '\t"use strict"') + return; + var prev = indent; + var i = 0; + while (line.charAt(i++) === "\t") + ++indent; + push(line.trim()); + indent = prev; }); + push("/* eslint-enable */"); + --indent; + push("})();"); } function buildType(ref, type) { - out.push(""); - out.push(ref + "." + type.name + " = function " + type.name + "() {};"); // currently just an empty function - buildNamespace(ref + "." + type.name, type); + var fullName = type.fullName.substring(1); + + push(""); + pushComment([ + "Constructs a new " + type.name + ".", + "@exports " + fullName, + "@constructor", + "@param {Object} [properties] Properties to set" + ]); + push("function " + name(type.name) + "(properties) {"); + ++indent; + push("if (properties) {"); + ++indent; + push("var keys = Object.keys(properties);"); + push("for (var i = 0; i < keys.length; ++i)"); + ++indent; + push("this[keys[i]] = properties[keys[i]];"); + --indent; + --indent; + push("}"); + --indent; + push("}"); + push(""); + type.fieldsArray.forEach(function(field) { + field.resolve(); + if (typeof field.defaultValue === 'object' && field.defaultValue) + return; + push(name(type.name) + ".prototype." + name(field.name) + " = " +JSON.stringify(field.defaultValue) + ";"); + }); + + // #encode + push(""); + pushComment([ + "Encodes the specified " + type.name + ".", + "@function", + "@param {" + fullName + "|Object} message " + type.name + " or plain object to encode", + "@param {Writer} [writer] Writer to encode to", + "@returns {Writer} Writer" + ]); + buildFunction(type, "encode", encoder.generate(type), { + Writer : "$runtime.Writer", + util : "$runtime.util" + }); + + // #encodeDelimited + push(""); + pushComment([ + "Encodes the specified " + type.name + ", length delimited.", + "@param {" + fullName + "|Object} message " + type.name + " or plain object to encode", + "@param {Writer} [writer] Writer to encode to", + "@returns {Writer} Writer" + ]); + push(name(type.name) + ".encodeDelimited = function encodeDelimited(message, writer) {"); + ++indent; + push("return this.encode(message, writer).ldelim();"); + --indent; + push("};"); + + // #decode + push(""); + pushComment([ + "Decodes a " + type.name + " from the specified reader or buffer.", + "@function", + "@param {Reader|Uint8Array} readerOrBuffer Reader or buffer to decode from", + "@param {number} [length] Message length if known beforehand", + "@returns {" + fullName + "} " + type.name + ]); + buildFunction(type, "decode", decoder.generate(type), { + Reader : "$runtime.Reader", + util : "$runtime.util" + }); + + // #decodeDelimited + push(""); + pushComment([ + "Decodes a " + type.name + " from the specified reader or buffer, length delimited.", + "@param {Reader|Uint8Array} readerOrBuffer Reader or buffer to decode from", + "@returns {" + fullName + "} " + type.name + ]); + push(name(type.name) + ".decodeDelimited = function decodeDelimited(readerOrBuffer) {"); + ++indent; + push("readerOrBuffer = readerOrBuffer instanceof Reader ? readerOrBuffer : Reader(readerOrBuffer);"); + push("return this.decode(readerOrBuffer, readerOrBuffer.uint32());"); + --indent; + push("};"); + + // #verify + push(""); + pushComment([ + "Verifies a " + type.name + ".", + "@param {" + fullName + "|Object} message " + type.name + " or plain object to verify", + "@returns {?string} `null` if valid, otherwise the reason why it is not" + ]); + buildFunction(type, "verify", verifier.generate(type), {}); } function buildService(ref, service) { - out.push(""); - out.push(ref + "." + service.name + " = {};"); // currently just an empty object + push(""); + push(name(ref) + "." + name(service.name) + " = {};"); // currently just an empty object } function buildEnum(ref, enm) { - out.push(""); - out.push(ref + "." + enm.name + " = " + JSON.stringify(enm.values, null, "\t") + ";"); + push(""); + push(ref + "." + enm.name + " = {"); + ++indent; + push(""); + Object.keys(enm.values).forEach(function(key) { + push(name(key) + ": " + enm.values[key].toString(10) + ","); + }); + --indent; + push("};"); } diff --git a/runtime.js b/runtime.js new file mode 100644 index 000000000..d691ff83b --- /dev/null +++ b/runtime.js @@ -0,0 +1,71 @@ +"use strict"; + +/** + * Minimal static code generator runtime. + * @namespace + */ +var runtime = exports; + +/** + * @alias Reader + */ +runtime.Reader = require("./src/reader"); + +/** + * @alias Writer + */ +runtime.Writer = require("./src/writer"); + +/** + * Runtime utility. + * @memberof runtime + */ +var util = runtime.util = {}; + +/** + * Converts a number or long to an 8 characters long hash string. + * @param {Long|number} value Value to convert + * @returns {string} Hash + */ +util.longToHash = function longToHash(value) { + return value + ? LongBits.from(value).toHash() + : '\0\0\0\0\0\0\0\0'; +}; + +/** + * Tests if two possibly long values are not equal. + * @param {number|Long} a First value + * @param {number|Long} b Second value + * @returns {boolean} `true` if not equal + */ +util.longNeq = function longNeq(a, b) { + return typeof a === 'number' + ? typeof b === 'number' + ? a !== b + : (a = LongBits.fromNumber(a)).lo !== b.low || a.hi !== b.high + : typeof b === 'number' + ? (b = LongBits.fromNumber(b)).lo !== a.low || b.hi !== a.high + : a.low !== b.low || a.high !== b.high; +}; + +/** + * Resolves lazy type references. + * @param {Object} root Root object + * @param {string[][]} lazyTypes Lazy type references + * @returns {Object} `root` + */ +runtime.resolve = function resolve(root, lazyTypes) { + lazyTypes.forEach(function(types) { + types.forEach(function(path, i) { + if (!path) + return; + path = path.split('.'); + var ptr = root; + while (path.length) + ptr = ptr[path.shift()]; + types[i] = ptr; + }); + }); + return root; +}; diff --git a/src/index.js b/src/index.js index 1d6c486b7..185d3e3f6 100644 --- a/src/index.js +++ b/src/index.js @@ -33,6 +33,7 @@ protobuf.Reader = require("./reader"); protobuf.BufferReader = protobuf.Reader.BufferReader; protobuf.encoder = require("./encoder"); protobuf.decoder = require("./decoder"); +protobuf.verifier = require("./verifier"); // Reflection protobuf.ReflectionObject = require("./object"); diff --git a/src/type.js b/src/type.js index dbd4bf67d..10d71f0fb 100644 --- a/src/type.js +++ b/src/type.js @@ -16,7 +16,7 @@ var Enum = require("./enum"), Writer = require("./writer"), encoder = require("./encoder"), decoder = require("./decoder"), - Verifier = require("./verifier"), + verifier = require("./verifier"), inherits = require("./inherits"), util = require("./util"); @@ -387,9 +387,10 @@ TypePrototype.decodeDelimited = function decodeDelimited(readerOrBuffer) { * @returns {?string} `null` if valid, otherwise the reason why it is not */ TypePrototype.verify = function verify(message) { - var verifier = new Verifier(this); - this.verify = codegen.supported - ? verifier.generate() - : verifier.verify; - return this.verify(message); + return (this.verify = codegen.supported + ? verifier.generate(this).eof(this.getFullName() + "$verify", { + types : this.getFieldsArray().map(function(fld) { return fld.resolvedType; }) + }) + : verifier.fallback + ).call(this, message); }; diff --git a/src/verifier.js b/src/verifier.js index 83b553eb7..cc9c34baa 100644 --- a/src/verifier.js +++ b/src/verifier.js @@ -1,62 +1,22 @@ "use strict"; -module.exports = Verifier; + +/** + * Runtime message verifier using code generation on top of reflection. + * @namespace + */ +var verifier = exports; var Enum = require("./enum"), Type = require("./type"), util = require("./util"); /** - * Constructs a new verifier for the specified message type. - * @classdesc Runtime message verifier using code generation on top of reflection. - * @constructor - * @param {Type} type Message type - */ -function Verifier(type) { - - /** - * Message type. - * @type {Type} - */ - this.type = type; -} - -/** @alias Verifier.prototype */ -var VerifierPrototype = Verifier.prototype; - -// This is here to mimic Type so that fallback functions work without having to bind() -Object.defineProperties(VerifierPrototype, { - - /** - * Fields of this verifier's message type as an array for iteration. - * @name Verifier#fieldsArray - * @type {Field[]} - * @readonly - */ - fieldsArray: { - get: VerifierPrototype.getFieldsArray = function getFieldsArray() { - return this.type.getFieldsArray(); - } - }, - - /** - * Full name of this verifier's message type. - * @name Verifier#fullName - * @type {string} - * @readonly - */ - fullName: { - get: VerifierPrototype.getFullName = function getFullName() { - return this.type.getFullName(); - } - } -}); - -/** - * Verifies a runtime message of this verifier's message type. + * Verifies a runtime message of `this` message type. * @param {Prototype|Object} message Runtime message or plain object to verify * @returns {?string} `null` if valid, otherwise the reason why it is not + * @this {Type} */ -VerifierPrototype.verify = function verify_fallback(message) { +verifier.fallback = function fallback(message) { var fields = this.getFieldsArray(), i = 0; while (i < fields.length) { @@ -82,12 +42,13 @@ VerifierPrototype.verify = function verify_fallback(message) { }; /** - * Generates a verifier specific to this verifier's message type. - * @returns {function} Verifier function with an identical signature to {@link Verifier#verify} + * Generates a verifier specific to the specified message type. + * @param {Type} mtype Message type + * @returns {util.CodegenAppender} Unscoped codegen instance */ -VerifierPrototype.generate = function generate() { +verifier.generate = function generate(mtype) { /* eslint-disable no-unexpected-multiline */ - var fields = this.type.getFieldsArray(); + var fields = mtype.getFieldsArray(); var gen = util.codegen("m"); var hasReasonVar = false; @@ -97,14 +58,14 @@ VerifierPrototype.generate = function generate() { if (field.required) { gen ("if(m%s===undefined)", prop) - ("return 'missing required field %s in %s'", field.name, this.type.getFullName()); + ("return 'missing required field %s in %s'", field.name, mtype.getFullName()); } else if (field.resolvedType instanceof Enum) { var values = util.toArray(field.resolvedType.values); gen ("switch(m%s){", prop) ("default:") - ("return 'invalid enum value %s = '+m%s+' in %s'", field.name, prop, this.type.getFullName()); + ("return 'invalid enum value %s = '+m%s+' in %s'", field.name, prop, mtype.getFullName()); for (var j = 0, l = values.length; j < l; ++j) gen ("case %d:", values[j]); gen @@ -114,7 +75,7 @@ VerifierPrototype.generate = function generate() { if (field.required) gen ("if(!m%s)", prop) - ("return 'missing required field %s in %s'", field.name, this.type.getFullName()); + ("return 'missing required field %s in %s'", field.name, mtype.getFullName()); if (!hasReasonVar) { gen("var r"); hasReasonVar = true; } gen @@ -123,10 +84,6 @@ VerifierPrototype.generate = function generate() { } } return gen - ("return null") - - .eof(this.type.getFullName() + "$verify", { - types : fields.map(function(fld) { return fld.resolvedType; }) - }); + ("return null"); /* eslint-enable no-unexpected-multiline */ }; diff --git a/tests/data/package.js b/tests/data/package.js new file mode 100644 index 000000000..8e9989fd8 --- /dev/null +++ b/tests/data/package.js @@ -0,0 +1,420 @@ +module.exports = (function() { + "use strict"; + + // Minimal static codegen runtime + var $runtime = require("../../runtime"); + + // Lazily resolved type references + var $lazyTypes = []; + + /** @alias exports */ + var $root = {}; + + /** @alias Package */ + $root.Package = (function() { + + /** + * Constructs a new Package. + * @exports Package + * @constructor + * @param {Object} [properties] Properties to set + */ + function Package(properties) { + if (properties) { + var keys = Object.keys(properties); + for (var i = 0; i < keys.length; ++i) + this[keys[i]] = properties[keys[i]]; + } + } + + Package.prototype.name = ""; + Package.prototype.version = ""; + Package.prototype.description = ""; + Package.prototype.author = ""; + Package.prototype.license = ""; + Package.prototype.repository = null; + Package.prototype.bugs = ""; + Package.prototype.homepage = ""; + Package.prototype.main = ""; + Package.prototype.types = ""; + + /** + * Encodes the specified Package. + * @function + * @param {Package|Object} message Package or plain object to encode + * @param {Writer} [writer] Writer to encode to + * @returns {Writer} Writer + */ + Package.encode = (function() { + /* eslint-disable */ + var Writer = $runtime.Writer; + var util = $runtime.util; + var types; $lazyTypes.push(types = [null,null,null,null,null,"Package.Repository",null,null,null,null,null,null,null,null,null,null]); + return function encode(m,w) { + w||(w=Writer()) + if(m['name']!==undefined&&m['name']!=="") + w.tag(1,2).string(m['name']) + if(m['version']!==undefined&&m['version']!=="") + w.tag(2,2).string(m['version']) + if(m['description']!==undefined&&m['description']!=="") + w.tag(3,2).string(m['description']) + if(m['author']!==undefined&&m['author']!=="") + w.tag(4,2).string(m['author']) + if(m['license']!==undefined&&m['license']!=="") + w.tag(5,2).string(m['license']) + if(m['repository']!==undefined&&m['repository']!==null) + types[5].encode(m['repository'],w.fork()).len&&w.ldelim(6)||w.reset() + if(m['bugs']!==undefined&&m['bugs']!=="") + w.tag(7,2).string(m['bugs']) + if(m['homepage']!==undefined&&m['homepage']!=="") + w.tag(8,2).string(m['homepage']) + if(m['keywords']) + for(var i=0;i