Skip to content

Instantly share code, notes, and snippets.

@matthieubulte
Created April 6, 2016 14:37
Show Gist options
  • Save matthieubulte/cb27e9102b06021521a7552cc62ea20e to your computer and use it in GitHub Desktop.
Save matthieubulte/cb27e9102b06021521a7552cc62ea20e to your computer and use it in GitHub Desktop.
java to typescript
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);
}
}
}
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