package io.vertx.serviceproxy.sockjs.generator;

import io.vertx.codegen.*;
import io.vertx.codegen.doc.Token;
import io.vertx.codegen.format.CamelCase;
import io.vertx.codegen.format.SnakeCase;
import io.vertx.codegen.type.*;
import io.vertx.codegen.writer.CodeWriter;
import io.vertx.serviceproxy.generator.model.ProxyMethodInfo;
import io.vertx.serviceproxy.generator.model.ProxyModel;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static io.vertx.codegen.type.ClassKind.*;

public class SockjsServiceProxyJSGenerator extends AbstractSockjsServiceProxyGenerator {
  SockjsServiceProxyJSGenerator() {
    this.name = "sockjs_service_proxies";
    this.kinds = Collections.singleton("proxy");
  }

  @Override
  public String filename(ProxyModel model) {
    ClassTypeInfo type = model.getType();
    return "resources/" + type.getModuleName() + "-js/" + Helper.convertCamelCaseToUnderscores(type.getRaw().getSimpleName()) + "-proxy.js";
  }

  
Generate the module name of a type
/** * Generate the module name of a type */
private String getModuleName(ClassTypeInfo type) { return type.getModuleName() + "-js/" + CamelCase.INSTANCE.to(SnakeCase.INSTANCE, type.getSimpleName()); } private void genMethod(ProxyModel model, String methodName, boolean genStatic, @SuppressWarnings("SameParameterValue") Predicate<MethodInfo> methodFilter, CodeWriter writer) { ClassTypeInfo type = model.getType(); String simpleName = type.getSimpleName(); Map<String, List<MethodInfo>> methodsByName = model.getMethodMap(); List<MethodInfo> methodList = methodsByName.get(methodName); if (methodFilter != null) { List<MethodInfo> methodTmpl = methodList; methodList = new ArrayList<>(); for (MethodInfo method : methodTmpl) { if (methodFilter.test(method)) { methodList.add(method); } } } if (methodList.size() > 0) { boolean overloaded = methodList.size() > 1; MethodInfo method = methodList.get(methodList.size() - 1); if (genStatic == method.isStaticMethod()) { writer.println("/**"); if (method.getDoc() != null) { Token.toHtml(method.getDoc().getTokens(), "", this::renderLinkToHtml, "\n", writer); } writer.println(); writer.print(" "); if (genStatic) { writer.format("@memberof module:%s", getModuleName(type)).println(); } else { writer.println("@public"); } boolean first = true; for (ParamInfo param : method.getParams()) { if (first) { first = false; } else { writer.println(); } writer.format(" @param %s {%s} ", param.getName(), getJSDocType(param.getType())); if (param.getDescription() != null) { Token.toHtml(param.getDescription().getTokens(), "", this::renderLinkToHtml, "", writer); writer.print(" "); } } writer.println(); if (method.getReturnType().getKind() != VOID) { writer.format(" @return {%s}", getJSDocType(method.getReturnType())); if (method.getReturnDescription() != null) { writer.print(" "); Token.toHtml(method.getReturnDescription().getTokens(), "", this::renderLinkToHtml, "", writer); } writer.println(); } writer.println(" */"); writer.format("%s.%s = ", genStatic ? simpleName : "this", methodName); if (overloaded) { writer.println(" function() {"); } else { writer.format(" function(%s) {\n", (method.getParams().stream().map(ParamInfo::getName).collect(Collectors.joining(", ")))); } int mcnt = 0; writer.indent(); writer.println("var __args = arguments;"); for (MethodInfo m : methodList) { writer.print(mcnt++ == 0 ? "if" : "else if"); int paramSize = m.getParams().size(); writer.format(" (__args.length === %s", paramSize); int cnt = 0; if (paramSize > 0) { writer.print(" && "); } first = true; for (ParamInfo param : m.getParams()) { if (first) { first = false; } else { writer.print(" && "); } switch (param.getType().getKind()) { case PRIMITIVE: case BOXED_PRIMITIVE: if (param.isNullable()) { writer.print("("); } writer.format("typeof __args[%s] ===", cnt); String paramSimpleName = param.getType().getSimpleName(); if ("boolean".equalsIgnoreCase(paramSimpleName)) { writer.print("'boolean'"); } else if ("char".equals(paramSimpleName) || "Character".equals(paramSimpleName)) { writer.print("'string'"); } else { writer.print("'number'"); } if (param.isNullable()) { writer.format(" || __args[%s] == null)", cnt); } break; case STRING: case ENUM: if (param.isNullable()) { writer.print("("); } writer.format("typeof __args[%s] === 'string'", cnt); if (param.isNullable()) { writer.format(" || __args[%s] == null)", cnt); } break; case CLASS_TYPE: writer.format("typeof __args[%s] === 'function'", cnt); break; case API: writer.format("typeof __args[%s] === 'object' && ", cnt); if (param.isNullable()) { writer.format("(__args[%s] == null || ", cnt); } writer.format("__args[%s]._jdel", cnt); if (param.isNullable()) { writer.print(")"); } break; case JSON_ARRAY: case LIST: case SET: writer.format("typeof __args[%s] === 'object' && ", cnt); if (param.isNullable()) { writer.print("("); } writer.format("__args[%s] instanceof Array", cnt); if (param.isNullable()) { writer.format(" || __args[%s] == null)", cnt); } break; case HANDLER: if (param.isNullable()) { writer.print("("); } writer.format("typeof __args[%s] === 'function'", cnt); if (param.isNullable()) { writer.format(" || __args[%s] == null)", cnt); } break; case OBJECT: if (param.getType().isVariable() && ((TypeVariableInfo) param.getType()).isClassParam()) { writer.format("j_%s.accept(__args[%s])", param.getType().getName(), cnt); } else { writer.format("typeof __args[%s] !== 'function'", cnt); } break; case FUNCTION: writer.format("typeof __args[%s] === 'function'", cnt); break; case THROWABLE: writer.format("typeof __args[%s] === 'object'", cnt); break; default: if (!param.isNullable()) { writer.print("("); } writer.format("typeof __args[%s] === 'object'", cnt); if (!param.isNullable()) { writer.format(" && __args[%s] != null)", cnt); } } cnt++; } writer.println(") {"); writer.indent(); genMethodAdapter(model, m, writer); writer.unindent(); writer.print("}"); } writer.unindent(); writer.println(" else throw new TypeError('function invoked with invalid arguments');"); writer.println("};"); writer.println(); } } } @Override public String render(ProxyModel model, int index, int size, Map<String, Object> session) { StringWriter sw = new StringWriter(); CodeWriter writer = new CodeWriter(sw); ClassTypeInfo type = model.getType(); String simpleName = type.getSimpleName(); genLicenses(writer); writer.println(); writer.format("/// <reference path=\"./%s-proxy.d.ts\" />", Helper.convertCamelCaseToUnderscores(type.getRaw().getSimpleName())).println(); writer.println(); writer.format("/** @module %s */", getModuleName(type)).println(); List<String> imports = new ArrayList<>(); //generate the module loader shim writer.println("!function (factory) {"); writer.indent().println("if (typeof require === 'function' && typeof module !== 'undefined') {"); // write imports as commonJS writer.indent(); imports.clear(); for (ApiTypeInfo referencedType : model.getReferencedTypes()) { if(referencedType.isProxyGen()) { String refedType = referencedType.getSimpleName(); imports.add(refedType); writer.format("var %s = require('./%s-proxy');", refedType, getModuleName(referencedType)).println(); } } writer.format("factory(%s);", String.join(", ", imports)).println(); writer.unindent().println("} else if (typeof define === 'function' && define.amd) {"); writer.indent().println("// AMD loader"); imports.clear(); for (ApiTypeInfo referencedType : model.getReferencedTypes()) { if(referencedType.isProxyGen()) { imports.add("'" + getModuleName(referencedType) + "-proxy'"); } } writer.format("define('%s-proxy', [%s], factory);", getModuleName(type), String.join(", ", imports)); writer.println(); writer.unindent().println("} else {"); writer.indent().println("// plain old include"); imports.clear(); for (ApiTypeInfo referencedType : model.getReferencedTypes()) { if(referencedType.isProxyGen()) { String refedType = referencedType.getSimpleName(); imports.add("this." + refedType); } } writer.format("%s = factory(%s);", simpleName, String.join(", ", imports)).println(); writer.unindent().println("}"); imports.clear(); for (ApiTypeInfo referencedType : model.getReferencedTypes()) { if(referencedType.isProxyGen()) { String refedType = referencedType.getSimpleName(); imports.add(refedType); } } writer.unindent().format("}(function (%s) {", String.join(", ", imports)).println(); writer.indent(); writer.println(); genDoc(model, writer); //The constructor writer.format("var %s = function(eb, address) {", simpleName).println(); writer.indent().println("var j_eb = eb;"); writer.println("var j_address = address;"); writer.println("var closed = false;"); writer.println("var that = this;"); writer.println("var convCharCollection = function(coll) {"); writer.indent().println("var ret = [];"); writer.println("for (var i = 0;i < coll.length;i++) {"); writer.indent().println("ret.push(String.fromCharCode(coll[i]));"); writer.unindent().println("}"); writer.println("return ret;"); writer.unindent().println("};"); //Apply any supertypes for (TypeInfo superType : model.getSuperTypes()) { writer.format("%s.call(this, j_val);", superType.getRaw().getSimpleName()).println(); } writer.println(); //Now iterate through each unique method for (String methodName : model.getMethodMap().keySet()) { //Call out to actually generate the method, we only consider non static methods here genMethod(model, methodName, false, this::methodFilter, writer); } writer.unindent().println("};"); writer.println(); //Iterate through the methods again, this time only considering the static ones for (String methodName : model.getMethodMap().keySet()) { //Call out to generate the static method genMethod(model, methodName, true, this::methodFilter, writer); } writer.println("if (typeof exports !== 'undefined') {"); writer.indent().println("if (typeof module !== 'undefined' && module.exports) {"); writer.indent().format("exports = module.exports = %s;", simpleName).println(); writer.unindent().println("} else {"); writer.indent().format("exports.%s = %s;", simpleName, simpleName).println(); writer.unindent().println("}"); writer.unindent().println("} else {"); writer.indent().format("return %s;", simpleName).println(); writer.unindent().println("}"); writer.unindent().print("});"); return sw.toString(); } private void genMethodAdapter(ProxyModel model, MethodInfo m, CodeWriter writer) { ProxyMethodInfo method = (ProxyMethodInfo) m; writer.println("if (closed) {"); writer.indent().println("throw new Error('Proxy is closed');"); writer.unindent().println("}"); genMethodCall(method, writer); writer.println(";"); if (method.isProxyClose()) { writer.println("closed = true;"); } if (method.isFluent()) { writer.println("return that;"); } else { writer.println("return;"); } } private void genMethodCall(MethodInfo method, PrintWriter writer){ List<ParamInfo> params = method.getParams(); int psize = params.size(); ParamInfo lastParam = psize > 0 ? params.get(psize - 1) : null; boolean hasResultHandler = lastParam != null && lastParam.getType().getKind() == HANDLER && ((ParameterizedTypeInfo) lastParam.getType()).getArg(0).getKind() == ASYNC_RESULT; if (hasResultHandler) { psize--; } writer.print("j_eb.send(j_address, {"); boolean first = true; for (int pcnt = 0; pcnt < psize; pcnt++) { if (first) { first = false; } else { writer.print(", "); } ParamInfo param = params.get(pcnt); String paramTypeName = param.getType().getName(); String paramName = param.getName(); writer.format("\"%s\":", paramName); if ("java.lang.Character".equals(paramTypeName) || "char".equals(paramTypeName)) { writer.format("__args[%d].charCodeAt(0)", pcnt); } else { writer.format("__args[%d]", pcnt); } } writer.format("}, {\"action\":\"%s\"}", method.getName()); if (hasResultHandler) { ParameterizedTypeInfo handlerType = (ParameterizedTypeInfo) lastParam.getType(); ParameterizedTypeInfo asyncResultType = (ParameterizedTypeInfo) handlerType.getArg(0); TypeInfo resultType = asyncResultType.getArg(0); writer.format(", function(err, result) { __args[%d](err, result && ", psize); ClassKind resultKind = resultType.getKind(); if (resultType.getKind() == API) { writer.format("new %s(j_eb, result.headers.proxyaddr)", resultType.getSimpleName()); } else if ((resultKind == LIST || resultKind == SET) && "java.lang.Character".equals(((ParameterizedTypeInfo)resultType).getArg(0).getName())) { writer.print("convCharCollection(result.body)"); } else { writer.print("result.body"); } writer.print("); }"); } writer.print(")"); } void genDoc(ProxyModel model, CodeWriter writer) { writer.println("/**"); if (model.getIfaceComment() != null) { writer.println(Helper.removeTags(model.getIfaceComment())); } writer.println(" @class"); writer.println("*/"); } }