Created
April 6, 2016 14:37
-
-
Save matthieubulte/cb27e9102b06021521a7552cc62ea20e to your computer and use it in GitHub Desktop.
java to typescript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.codegen; | |
import java.lang.reflect.*; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.util.function.Function; | |
public class ClassSerializer { | |
private String indentation; | |
private String newLine; | |
private String namespace; | |
private final List<Function<Class, Boolean>> replaceRules; | |
private final StringBuilder builder; | |
public ClassSerializer(StringBuilder builder, String namespace) { | |
this(builder, namespace, " ", "\n"); | |
} | |
public ClassSerializer(StringBuilder builder, String namespace, String indentation, String newLine) { | |
this.builder = builder; | |
this.namespace = namespace; | |
this.indentation = indentation; | |
this.newLine = newLine; | |
replaceRules = new ArrayList<>(); | |
} | |
public void addRule(Function<Class, Boolean> rule) { | |
replaceRules.add(rule); | |
} | |
public void addClass(Class clazz) { | |
if (tryReplace(clazz)) return; | |
if (clazz.isAnnotation() || clazz.isMemberClass() || clazz.isAnonymousClass() || clazz.isAnnotation()) return; | |
builder.append("export module " + moduleName(clazz) + " {" + newLine); | |
if (clazz.isInterface()) serializeInterface(clazz); | |
else if (clazz.isEnum()) serializeEnum(clazz); | |
else serializeClass(clazz); | |
builder.append("}" + newLine); | |
} | |
public void addAlias(String from, String to) { | |
builder.append("export module " + namespace + " {" + newLine); | |
builder.append("export type " + from + " = " + to + ";" + newLine); | |
builder.append("}" + newLine); | |
} | |
private String moduleName(Class clazz) { | |
// until we have a solution to avoid having to rewrite for each declaration all the required imports | |
// we just limit the module name to the name of the global namespace. | |
// String path = clazz.getCanonicalName(); | |
// | |
// if (path == null) { | |
// return namespace; | |
// } | |
// | |
// int lastDot = path.lastIndexOf('.'); | |
// | |
// if (lastDot < 0) { | |
// return namespace; | |
// } | |
// | |
// return namespace + "." + path.substring(0, lastDot); | |
return namespace; | |
} | |
private boolean tryReplace(Class clazz) { | |
for (Function<Class, Boolean> rule : replaceRules) { | |
if (rule.apply(clazz)) return true; | |
} | |
return false; | |
} | |
/** | |
* A class gets transformed into an interface where each field of the class becomes an optional field of the interface. This is done | |
* because most of the commands and DTOs can have empty fields. | |
* The translation will maintain superclasses and generics, and methods will obviously disappear. | |
* | |
* class Foo { | |
* public int test; | |
* public String toast; | |
* | |
* public Foo(int test, String toast) { | |
* this.test = test; | |
* this.toast = toast; | |
* } | |
* } | |
* | |
* becomes | |
* | |
* export interface Foo { | |
* test?: number; | |
* toast?: string; | |
* } | |
* | |
* @param clazz | |
*/ | |
private void serializeClass(Class clazz) { | |
builder.append("export interface "); | |
serializeClassSignature(clazz); | |
builder.append(" {" + newLine); | |
for (Field field : clazz.getDeclaredFields()) { | |
builder.append(indentation); | |
builder.append(field.getName()); | |
// NOTE: every class field is made optional to make it easier for the user. | |
builder.append("?:"); | |
typeOf(field.getGenericType(), false); | |
builder.append(";" + newLine); | |
} | |
builder.append("}" + newLine); | |
} | |
/** | |
* An enumeration gets transformed into a type alias and a module containing constant enumeration values. | |
* | |
* enum Test { | |
* FOO, | |
* BAR | |
* } | |
* | |
* will become | |
* | |
* export type Test = string; | |
* export module test { | |
* FOO: Test = "Foo"; | |
* BAR: Test = "Bar"; | |
* } | |
* | |
* @param clazz | |
*/ | |
private void serializeEnum(Class clazz) { | |
String typeName = clazz.getSimpleName(); | |
String moduleName = typeName.toLowerCase(); | |
builder.append("export type "); | |
builder.append(typeName); | |
builder.append(" = string;" + newLine); | |
builder.append("export module "); | |
builder.append(moduleName); | |
builder.append("{" + newLine); | |
for (Field field : clazz.getDeclaredFields()) { | |
if (!field.getType().isArray()) { | |
String fieldName = field.getName(); | |
builder.append(indentation); | |
builder.append("export const " + fieldName + " : " + typeName + " = '" + fieldName + "';" + newLine); | |
} | |
} | |
builder.append("}" + newLine); | |
} | |
/** | |
* An interface simply gets translated into a type alias, as methods are not preserved during | |
* translation. | |
* | |
* interface Foo { | |
* // ... methods | |
* } | |
* | |
* becomes | |
* | |
* export interface Foo {} | |
* | |
* @param clazz | |
*/ | |
private void serializeInterface(Class clazz) { | |
builder.append("export interface "); | |
builder.append(clazz.getSimpleName()); | |
builder.append("{}" + newLine); | |
} | |
private void serializeClassSignature(Class clazz) { | |
builder.append(clazz.getSimpleName()); | |
// class generic parameters | |
Type[] variables = clazz.getTypeParameters(); | |
boolean hasTypeVaribles = variables.length > 0; | |
if (hasTypeVaribles) { | |
builder.append("<"); | |
typeOf(variables[0], true); | |
} | |
for (int i = 1; i < variables.length; i++) { | |
builder.append(", "); | |
typeOf(variables[i], true); | |
} | |
if (hasTypeVaribles) { | |
builder.append(">"); | |
} | |
// super class | |
if (!clazz.getGenericSuperclass().equals(Object.class)) { | |
builder.append(" extends "); | |
typeOf(clazz.getGenericSuperclass(), true); | |
} | |
} | |
private void typeOfGenericArrayType(GenericArrayType array, boolean showBounds) { | |
typeOf(array.getGenericComponentType(), showBounds); | |
builder.append("[]"); | |
} | |
private void typeOfParametrizedType(ParameterizedType parametrized, boolean showBounds) { | |
Type[] arguments = parametrized.getActualTypeArguments(); | |
boolean hasTypeArgument = arguments.length > 0; | |
builder.append(((Class)parametrized.getRawType()).getSimpleName()); | |
if (hasTypeArgument) { | |
builder.append("<"); | |
typeOf(arguments[0], showBounds); | |
} | |
for (int i = 1; i < arguments.length; i++) { | |
builder.append(", "); | |
typeOf(arguments[i], showBounds); | |
} | |
if (hasTypeArgument) { | |
builder.append(">"); | |
} | |
} | |
private void typeOfTypeVariable(TypeVariable variable, boolean showBounds) { | |
Type[] bounds = variable.getBounds(); | |
boolean hasBound = bounds.length > 0 && !bounds[0].equals(Object.class); | |
builder.append(variable.getName()); | |
if (!showBounds) return; | |
if (hasBound) { | |
builder.append(" extends "); | |
typeOf(bounds[0], showBounds); | |
} | |
for (int i = 1; i < bounds.length; i++) { | |
builder.append(" & "); | |
typeOf(bounds[i], showBounds); | |
} | |
} | |
private void typeOfWildcardType(WildcardType wildcard, boolean showBounds) { | |
Type[] bounds = wildcard.getUpperBounds(); | |
boolean hasBound = bounds.length > 0 && !bounds[0].equals(Object.class); | |
if (hasBound && bounds.length > 1) { | |
System.err.println("Wildcard can only have one upper bound, using 'any' instead. Erroneous WildcardType is " + wildcard); | |
} else { | |
typeOf(bounds[0], showBounds); | |
} | |
} | |
private void typeOf(Type t, boolean showBounds) { | |
if (t instanceof GenericArrayType) { | |
typeOfGenericArrayType((GenericArrayType) t, showBounds); | |
} | |
else if (t instanceof ParameterizedType) { | |
typeOfParametrizedType((ParameterizedType) t, showBounds); | |
} | |
else if (t instanceof TypeVariable) { | |
typeOfTypeVariable((TypeVariable) t, showBounds); | |
} | |
else if (t instanceof WildcardType) { | |
typeOfWildcardType((WildcardType) t, showBounds); | |
} | |
else if (t instanceof Class) { | |
builder.append(((Class) t).getSimpleName()); | |
} | |
else { | |
System.err.println(t); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.codegen; | |
import java.lang.reflect.Method; | |
import java.net.URL; | |
import java.net.URLClassLoader; | |
import java.nio.file.Files; | |
import java.nio.file.Paths; | |
import java.nio.file.StandardOpenOption; | |
import java.util.ArrayList; | |
import java.util.Enumeration; | |
import java.util.List; | |
import java.util.jar.JarEntry; | |
import java.util.jar.JarFile; | |
public class Main { | |
public static List<Class> getClasseNames(String jarName) throws Exception { | |
List<Class> classes = new ArrayList<Class>(); | |
JarFile jarFile = new JarFile(jarName); | |
Enumeration entries = jarFile.entries(); | |
URL[] urls = { new URL("jar:file:" + jarName +"!/") }; | |
URLClassLoader cl = URLClassLoader.newInstance(urls); | |
while (entries.hasMoreElements()) { | |
JarEntry je = (JarEntry) entries.nextElement(); | |
if(je.isDirectory() || !je.getName().endsWith(".class")){ | |
continue; | |
} | |
// -6 because of ".class" | |
String className = je.getName().substring(0,je.getName().length()-6); | |
className = className.replace('/', '.'); | |
try { | |
Class c = cl.loadClass(className); | |
classes.add(c); | |
} catch (Error e) { System.out.println(className); } | |
} | |
return classes; | |
} | |
public static void main(String[] args) throws Exception { | |
String namespace = "backend-api"; | |
StringBuilder builder = new StringBuilder(); | |
ClassSerializer serializer = new ClassSerializer(builder, namespace); | |
// number aliases | |
serializer.addAlias("Long", "number"); | |
serializer.addAlias("long", "number"); | |
serializer.addAlias("Double", "number"); | |
serializer.addAlias("double", "number"); | |
serializer.addAlias("Integer", "number"); | |
serializer.addAlias("int", "number"); | |
serializer.addAlias("Float", "number"); | |
serializer.addAlias("float", "number"); | |
// misc | |
serializer.addAlias("String", "string"); | |
serializer.addAlias("UUID", "string"); | |
serializer.addAlias("Money", "number"); | |
serializer.addAlias("JsonNode", "any"); | |
serializer.addAlias("MediaType", "any"); | |
serializer.addAlias("Pattern", "any"); | |
serializer.addAlias("DateTime", "number"); | |
serializer.addAlias("LocalDate", "number"); | |
// containers | |
serializer.addAlias("Set<T>", "T[]"); | |
serializer.addAlias("ImmutableSet<T>", "T[]"); | |
serializer.addAlias("HashSet<T>", "T[]"); | |
serializer.addAlias("List<T>", "T[]"); | |
serializer.addAlias("ImmutableList<T>", "T[]"); | |
serializer.addAlias("ArrayList<T>", "T[]"); | |
builder.append("export interface HashMap<K, V> { }\n"); | |
builder.append("export interface Map<K, V> { }\n"); | |
// all ids are replaced by a type alias to UUID | |
serializer.addRule((Class clazz) -> { | |
if (clazz.getSimpleName() != null && clazz.getSimpleName().endsWith("Id")) { | |
serializer.addAlias(clazz.getSimpleName(), "UUID"); | |
return true; | |
} | |
return false; | |
}); | |
// TODO: custom containers are replaced by references to List<T> | |
getClasseNames("./java-classes.jar") | |
.stream() | |
.forEach(clazz -> serializer.addClass(clazz)); | |
Files.write( | |
Paths.get("./api.ts"), | |
builder.toString().getBytes(), | |
StandardOpenOption.WRITE, StandardOpenOption.CREATE | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment