Skip to content

Commit

Permalink
New syntax for generic classes in customTypeMappings parameter (#384)
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtechhabarta committed Jul 6, 2019
1 parent 0262d2b commit 2c57231
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,59 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;


public class CustomMappingTypeProcessor implements TypeProcessor {

private final Map<String, String> customMappings;
private final Map<String, Settings.CustomTypeMapping> customMappings;

public CustomMappingTypeProcessor(Map<String, String> customMappings) {
this.customMappings = customMappings;
public CustomMappingTypeProcessor(List<Settings.CustomTypeMapping> customMappings) {
this.customMappings = customMappings.stream().collect(Collectors.toMap(
mapping -> mapping.javaType.rawName,
mapping -> mapping));
}

@Override
public Result processType(Type javaType, Context context) {
if (javaType instanceof Class) {
final Class<?> javaClass = (Class<?>) javaType;
final String tsTypeName = customMappings.get(javaClass.getName());
if (tsTypeName != null) {
return new Result(new TsType.BasicType(tsTypeName));
final Settings.CustomTypeMapping mapping = customMappings.get(javaClass.getName());
if (mapping != null) {
return new Result(new TsType.BasicType(mapping.tsType.rawName));
}
}
if (javaType instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) javaType;
if (parameterizedType.getRawType() instanceof Class) {
final Class<?> javaClass = (Class<?>) parameterizedType.getRawType();
final String tsTypeName = customMappings.get(javaClass.getName());
if (tsTypeName != null) {
final Settings.CustomTypeMapping mapping = customMappings.get(javaClass.getName());
if (mapping != null) {
final List<Class<?>> discoveredClasses = new ArrayList<>();
final List<TsType> tsTypeArguments = new ArrayList<>();
for (Type typeArgument : parameterizedType.getActualTypeArguments()) {
final Function<Integer, TsType> processGenericParameter = index -> {
final Type typeArgument = parameterizedType.getActualTypeArguments()[index];
final TypeProcessor.Result typeArgumentResult = context.processType(typeArgument);
tsTypeArguments.add(typeArgumentResult.getTsType());
discoveredClasses.addAll(typeArgumentResult.getDiscoveredClasses());
return typeArgumentResult.getTsType();
};
if (mapping.tsType.typeParameters != null) {
final List<TsType> tsTypeArguments = new ArrayList<>();
for (String typeParameter : mapping.tsType.typeParameters) {
final int index = mapping.javaType.typeParameters.indexOf(typeParameter);
final TsType tsType = processGenericParameter.apply(index);
tsTypeArguments.add(tsType);
}
return new Result(new TsType.GenericBasicType(mapping.tsType.rawName, tsTypeArguments), discoveredClasses);
} else {
final int index = mapping.javaType.typeParameters.indexOf(mapping.tsType.rawName);
if (index != -1) {
final TsType tsType = processGenericParameter.apply(index);
return new Result(tsType, discoveredClasses);
} else {
return new Result(new TsType.BasicType(mapping.tsType.rawName), discoveredClasses);
}
}
return new Result(new TsType.GenericBasicType(tsTypeName, tsTypeArguments), discoveredClasses);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
import cz.habarta.typescript.generator.util.Utils;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.TypeVariable;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
Expand All @@ -26,6 +26,8 @@
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;


/**
Expand Down Expand Up @@ -62,7 +64,9 @@ public class Settings {
public List<String> referencedFiles = new ArrayList<>();
public List<String> importDeclarations = new ArrayList<>();
public Map<String, String> customTypeMappings = new LinkedHashMap<>();
private List<CustomTypeMapping> validatedCustomTypeMappings = null;
public Map<String, String> customTypeAliases = new LinkedHashMap<>();
private List<CustomTypeAlias> validatedCustomTypeAliases = null;
public DateMapping mapDate; // default is DateMapping.asDate
public EnumMapping mapEnum; // default is EnumMapping.asUnion
public boolean nonConstEnums = false;
Expand Down Expand Up @@ -119,6 +123,36 @@ public static class ConfiguredExtension {
public Map<String, String> configuration;
}

public static class CustomTypeMapping {
public final GenericName javaType;
public final GenericName tsType;

public CustomTypeMapping(GenericName javaType, GenericName tsType) {
this.javaType = javaType;
this.tsType = tsType;
}
}

public static class CustomTypeAlias {
public final GenericName tsType;
public final String tsDefinition;

public CustomTypeAlias(GenericName tsType, String tsDefinition) {
this.tsType = tsType;
this.tsDefinition = tsDefinition;
}
}

public static class GenericName {
public final String rawName;
public final List<String> typeParameters;

public GenericName(String rawName, List<String> typeParameters) {
this.rawName = Objects.requireNonNull(rawName);
this.typeParameters = typeParameters;
}
}

private static class TypeScriptGeneratorURLClassLoader extends URLClassLoader {

private final String name;
Expand Down Expand Up @@ -208,30 +242,6 @@ public static Map<String, String> convertToMap(List<String> mappings) {
return result;
}

public static Pair<String, List<String>> parseGenericTypeName(String type) {
final Matcher matcher = Pattern.compile("([^<]+)<([^>]+)>").matcher(type);
final String name;
final List<String> typeParameters;
if (matcher.matches()) { // is generic?
name = matcher.group(1);
typeParameters = Arrays.asList(matcher.group(2).split(","));
} else {
name = type;
typeParameters = null;
}
if (!ModelCompiler.isValidIdentifierName(name)) {
throw new RuntimeException(String.format("Invalid identifier: '%s'", name));
}
if (typeParameters != null) {
for (String typeParameter : typeParameters) {
if (!ModelCompiler.isValidIdentifierName(typeParameter)) {
throw new RuntimeException(String.format("Invalid generic type parameter: '%s'", typeParameter));
}
}
}
return Pair.of(name, typeParameters);
}

public void validate() {
if (outputKind == null) {
throw new RuntimeException("Required 'outputKind' parameter is not configured. " + seeLink());
Expand Down Expand Up @@ -260,9 +270,8 @@ public void validate() {
if (jackson2Configuration != null && jsonLibrary != JsonLibrary.jackson2) {
throw new RuntimeException("'jackson2Configuration' parameter is only applicable to 'jackson2' library.");
}
for (Map.Entry<String, String> entry : customTypeAliases.entrySet()) {
parseGenericTypeName(entry.getValue());
}
getValidatedCustomTypeMappings();
getValidatedCustomTypeAliases();
for (EmitterExtension extension : extensions) {
final String extensionName = extension.getClass().getSimpleName();
final DeprecationText deprecation = extension.getClass().getAnnotation(DeprecationText.class);
Expand Down Expand Up @@ -381,6 +390,98 @@ public void validate() {
}
}

public List<CustomTypeMapping> getValidatedCustomTypeMappings() {
if (validatedCustomTypeMappings == null) {
validatedCustomTypeMappings = new ArrayList<>();
for (Map.Entry<String, String> entry : customTypeMappings.entrySet()) {
final String javaName = entry.getKey();
final String tsName = entry.getValue();
try {
final GenericName genericJavaName = parseGenericName(javaName);
final GenericName genericTsName = parseGenericName(tsName);
validateTypeParameters(genericJavaName.typeParameters);
validateTypeParameters(genericTsName.typeParameters);
final Class<?> cls = loadClass(classLoader, genericJavaName.rawName, Object.class);
final int required = cls.getTypeParameters().length;
final int specified = genericJavaName.typeParameters != null ? genericJavaName.typeParameters.size() : 0;
if (specified != required) {
final String parameters = Stream.of(cls.getTypeParameters())
.map(TypeVariable::getName)
.collect(Collectors.joining(", "));
final String signature = cls.getName() + (parameters.isEmpty() ? "" : "<" + parameters + ">");
throw new RuntimeException(String.format(
"Wrong number of specified generic parameters, required: %s, found: %s. Correct format is: '%s'",
required, specified, signature));
}
if (genericTsName.typeParameters != null) {
final Set<String> parameters = Stream.of(cls.getTypeParameters())
.map(TypeVariable::getName)
.collect(Collectors.toSet());
for (String parameter : genericTsName.typeParameters) {
if (!parameters.contains(parameter)) {
throw new RuntimeException(String.format("Unknown generic type parameter '%s'", parameter));
}
}
}
validatedCustomTypeMappings.add(new CustomTypeMapping(genericJavaName, genericTsName));
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to parse configured custom type mapping '%s:%s': %s", javaName, tsName, e.getMessage()), e);
}
}
}
return validatedCustomTypeMappings;
}

public List<CustomTypeAlias> getValidatedCustomTypeAliases() {
if (validatedCustomTypeAliases == null) {
validatedCustomTypeAliases = new ArrayList<>();
for (Map.Entry<String, String> entry : customTypeAliases.entrySet()) {
final String tsName = entry.getKey();
final String tsDefinition = entry.getValue();
try {
final GenericName genericTsName = parseGenericName(tsName);
if (!ModelCompiler.isValidIdentifierName(genericTsName.rawName)) {
throw new RuntimeException(String.format("Invalid identifier: '%s'", genericTsName.rawName));
}
validateTypeParameters(genericTsName.typeParameters);
validatedCustomTypeAliases.add(new CustomTypeAlias(genericTsName, tsDefinition));
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to parse configured custom type alias '%s:%s': %s", tsName, tsDefinition, e.getMessage()), e);
}
}
}
return validatedCustomTypeAliases;
}

private static GenericName parseGenericName(String name) {
// Class<T1, T2>
// Class[T1, T2]
final Matcher matcher = Pattern.compile("([^<\\[]+)(<|\\[)([^>\\]]+)(>|\\])").matcher(name);
final String rawName;
final List<String> typeParameters;
if (matcher.matches()) { // is generic?
rawName = matcher.group(1);
typeParameters = Stream.of(matcher.group(3).split(","))
.map(String::trim)
.collect(Collectors.toList());
} else {
rawName = name;
typeParameters = null;
}
return new GenericName(rawName, typeParameters);
}

private static void validateTypeParameters(List<String> typeParameters) {
if (typeParameters == null) {
return;
}
for (String typeParameter : typeParameters) {
if (!ModelCompiler.isValidIdentifierName(typeParameter)) {
throw new RuntimeException(String.format("Invalid generic type parameter: '%s'", typeParameter));
}
}
}

private static void reportConfigurationChange(String extensionName, String parameterName, String parameterValue) {
TypeScriptGenerator.getLogger().info(String.format("Configuration: '%s' extension set '%s' parameter to '%s'", extensionName, parameterName, parameterValue));
}
Expand Down Expand Up @@ -578,7 +679,7 @@ private static <T> List<T> loadInstances(ClassLoader classLoader, List<String> c
private static <T> T loadInstance(ClassLoader classLoader, String className, Class<T> requiredType) {
try {
TypeScriptGenerator.getLogger().verbose("Loading class " + className);
return requiredType.cast(classLoader.loadClass(className).newInstance());
return requiredType.cast(classLoader.loadClass(className).getConstructor().newInstance());
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ private TypeProcessor createTypeProcessor(List<TypeProcessor> specificTypeProces
if (settings.customTypeProcessor != null) {
processors.add(settings.customTypeProcessor);
}
processors.add(new CustomMappingTypeProcessor(settings.customTypeMappings));
processors.add(new CustomMappingTypeProcessor(settings.getValidatedCustomTypeMappings()));
processors.addAll(specificTypeProcessors);
processors.add(new DefaultTypeProcessor());
final TypeProcessor typeProcessor = new TypeProcessor.Chain(processors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,13 +423,14 @@ private TsType typeFromJava(SymbolTable symbolTable, Type javaType, Object typeC

private TsModel addCustomTypeAliases(SymbolTable symbolTable, TsModel tsModel) {
final List<TsAliasModel> aliases = new ArrayList<>(tsModel.getTypeAliases());
for (Map.Entry<String, String> entry : settings.customTypeAliases.entrySet()) {
final Pair<String, List<String>> pair = Settings.parseGenericTypeName(entry.getKey());
final Symbol name = symbolTable.getSyntheticSymbol(pair.getValue1());
final List<TsType.GenericVariableType> typeParameters = pair.getValue2().stream()
.map(TsType.GenericVariableType::new)
.collect(Collectors.toList());
final TsType definition = new TsType.VerbatimType(entry.getValue());
for (Settings.CustomTypeAlias customTypeAlias : settings.getValidatedCustomTypeAliases()) {
final Symbol name = symbolTable.getSyntheticSymbol(customTypeAlias.tsType.rawName);
final List<TsType.GenericVariableType> typeParameters = customTypeAlias.tsType.typeParameters != null
? customTypeAlias.tsType.typeParameters.stream()
.map(TsType.GenericVariableType::new)
.collect(Collectors.toList())
: null;
final TsType definition = new TsType.VerbatimType(customTypeAlias.tsDefinition);
aliases.add(new TsAliasModel(null, name, typeParameters, definition, null));
}
return tsModel.withTypeAliases(aliases);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
public class CustomTypeAliasesTest {

@Test
public void test() {
public void testGeneric() {
final Settings settings = TestUtils.settings();
settings.outputKind = TypeScriptOutputKind.module;
settings.customTypeAliases = Collections.singletonMap("Id<T>", "string");
settings.customTypeMappings = Collections.singletonMap(IdRepresentation.class.getName(), "Id");
settings.customTypeMappings = Collections.singletonMap("cz.habarta.typescript.generator.CustomTypeAliasesTest$IdRepresentation<T>", "Id<T>");
final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(MyEntityRepresentation.class));
Assert.assertTrue(output.contains("id: Id<MyEntityRepresentation>"));
Assert.assertTrue(output.contains("export type Id<T> = string"));
Expand All @@ -27,4 +27,12 @@ private static class IdRepresentation<T> {
public String id;
}

@Test
public void testNonGeneric() {
final Settings settings = TestUtils.settings();
settings.customTypeAliases = Collections.singletonMap("Id", "string");
final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from());
Assert.assertTrue(output.contains("type Id = string"));
}

}
Loading

0 comments on commit 2c57231

Please sign in to comment.