poseidon
Version:
Simplify Node Callback APIs with an optimized promise layer that doesnt compromise on performance.
183 lines (163 loc) • 7.72 kB
text/coffeescript
esprima = require 'esprima'
escodegen = require 'escodegen'
Promise = require 'bluebird'
fs = require 'fs'
class Poseidon
constructor:(configuration = {}) ->
@configuration = configuration
return
generate: (path) ->
files = []
for className, classSchema of @configuration
file = {}
file.name = "#{className.toLowerCase()}.js"
code = file.code = []
dependencies = ["var Promise = require('bluebird');"]
for dependency, dependencyPath of classSchema.require
dependencies.push "var #{dependency} = require('#{dependencyPath}');"
# Generate class constructor
code.push """
function #{className}(#{classSchema.constructor.params.join(", ")}) {
#{classSchema.constructor.body}
}
"""
for functionName, functionSchema of classSchema.functions
# Set default options
functionSchema.wrap ?= true
functionSchema.params ?= []
# Generate class method
hunk = ["""
#{className}.prototype.#{functionName} = function (#{if functionSchema.body? then "#{functionSchema.params.join(", ")}){\n" else "){\n var args = arguments;"}
"""]
if classSchema.type is 'promise'
instanceIdentifier = "instanceValue"
else
instanceIdentifier = "this.instance"
# Use custom body if specified
if functionSchema.body?
hunk.push(functionSchema.body)
else
# Create deferred and callback if wrap option is true
if functionSchema.wrap or classSchema.type is 'promise'
hunk.push "var deferred = Promise.pending();"
if not functionSchema.wrap
hunk.push "var result;"
if classSchema.type is 'promise'
hunk.push """
this.instance.catch(function(err){
deferred.reject(err);
return deferred.promise;
}).then(function (instanceValue) {
"""
if functionSchema.wrap
# Code to wrap return values
castValues = []
if functionSchema.return?
for index, constructorName of functionSchema.return
if constructorName isnt null
if constructorName instanceof Object
if constructorName.array?
castValues.push "arguments[#{parseInt(index)+1}] = arguments[#{parseInt(index)+1}].map(function(item) { return new #{constructorName.name}(item); });"
else
castValues.push "arguments[#{parseInt(index)+1}] = new #{constructorName}(arguments[#{parseInt(index)+1}]);"
hunk.push """
var callback = function () {
if (arguments[0]) {
if (arguments.length === 1 || arguments[1] == null) {
deferred.reject(arguments[0]);
} else {
deferred.reject(Array.prototype.slice.call(arguments, 0));
}
} else {
#{castValues.join("\n")}
switch(arguments.length) {
case 0:
deferred.resolve();
break;
case 2:
deferred.resolve(arguments[1]);
break;
case 3:
deferred.resolve([arguments[1], arguments[2]]);
break;
case 4:
deferred.resolve([arguments[1], arguments[2], arguments[3]]);
break;
case 5:
deferred.resolve([arguments[1], arguments[2], arguments[3], arguments[4]]);
break;
case 6:
deferred.resolve([arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]]);
break;
default:
deferred.resolve(Array.prototype.slice.call(null, arguments, 1));
break;
}
}
};
"""
# Generate optimized function call
hunk.push """
switch(args.length) {
case 0:
#{if functionSchema.wrap then "" else "result = "}#{instanceIdentifier}.#{functionName}(#{if functionSchema.wrap then "callback" else ""});
break;
case 1:
#{if functionSchema.wrap then "" else "result = "}#{instanceIdentifier}.#{functionName}(args[0]#{if functionSchema.wrap then ", callback" else ""});
break;
case 2:
#{if functionSchema.wrap then "" else "result = "}#{instanceIdentifier}.#{functionName}(args[0], args[1]#{if functionSchema.wrap then ", callback" else ""});
break;
case 3:
#{if functionSchema.wrap then "" else "result = "}#{instanceIdentifier}.#{functionName}(args[0], args[1], args[2]#{if functionSchema.wrap then ", callback" else ""});
break;
case 4:
#{if functionSchema.wrap then "" else "result = "}#{instanceIdentifier}.#{functionName}(args[0], args[1], args[2], args[3]#{if functionSchema.wrap then ", callback" else ""});
break;
case 5:
#{if functionSchema.wrap then "" else "result = "}#{instanceIdentifier}.#{functionName}(args[0], args[1], args[2], args[3], args[4]#{if functionSchema.wrap then ", callback" else ""});
break;
default:
#{if functionSchema.wrap then "" else "result = "}#{instanceIdentifier}.#{functionName}.apply(#{instanceIdentifier}, #{if functionSchema.wrap then "Array.prototype.slice.call(null, args).concat(callback)" else "args"});
break;
}
#{if not functionSchema.wrap and classSchema.type is 'promise' and functionSchema.return? then "result = new #{functionSchema.return[0]}(result);" else ""}
#{if not functionSchema.wrap and classSchema.type is 'promise' then "deferred.resolve(result);" else ""}
"""
if classSchema.type is 'promise'
hunk.push "});"
# Return promise if wrap option is set
if functionSchema.wrap or classSchema.type is 'promise'
if not functionSchema.wrap and functionSchema.return?.length > 1 then throw new Error("Only 1 return value allowed when no callback is present")
hunk.push "return deferred.promise;"
else
# If no wrap option is allowed only 1 value can be set in return
if functionSchema.return?
if functionSchema.return.length > 1 then throw new Error("Only 1 return value allowed when no callback is present")
hunk.push "return new #{functionSchema.return[0]}(result);"
else if functionSchema.chain
hunk.push "return this;"
else
hunk.push "return result;"
# End of function call
hunk.push "};"
code.push hunk.join("\n")
code.push "module.exports = #{className};"
try
# Parse code to ensure generated javascript is valid
code = "#{dependencies.join("\n")}\n#{code.join("\n")}"
# Pretty format code
file.code = escodegen.generate(esprima.parse(code))
files.push file
catch err
codeLines = code.split("\n")
throw new Error("Error generating class #{className}\n#{err.message}\n#{codeLines[err.lineNumber-1]}")
if path?
write = Promise.promisify(fs.writeFile, fs)
Promise.map files, (file) ->
write("#{path}/#{file.name}", file.code)
.then ->
Promise.resolve files
else
files
module.exports = Poseidon