Library which provides new features on top of TypeScript Generator: annotation processor support (which eliminates the requirement for a maven plugin) and bean validation Java annotations to TypeScript decorators translation.
Input:
@Value
public class Foo {
@Size(min = 1, max = 2)
@NotEmpty
@NotNull
@Valid
private final String bar;
}
Result:
/* tslint:disable */
/* eslint-disable */
import { IsDefined, IsNotEmpty, MinLength, ValidateNested, MaxLength } from 'class-validator';
import { CommonValidationMessages } from './CommonValidationMessages';
export class Foo {
@MaxLength(2, { message: CommonValidationMessages.MaxLength(2) })
@MinLength(1, { message: CommonValidationMessages.MinLength(1) })
@IsNotEmpty({ message: CommonValidationMessages.IsNotEmpty })
@IsDefined({ message: CommonValidationMessages.IsDefined })
@ValidateNested()
bar: string;
}
class-transformer together with Infobip Jackson Extension is used to handle hierarchies.
Example for a hierarchy is a multi level hierarchy for inbound and outbound messages.
Input:
@Getter
@AllArgsConstructor
enum Channel {
SMS(InboundSmsMessage.class, OutboundSmsMessage.class);
private final Class<? extends InboundMessage> inboundMessageType;
private final Class<? extends OutboundMessage> outboundMessageType;
}
@Getter
@AllArgsConstructor
enum Direction implements TypeProvider {
INBOUND(InboundMessage.class),
OUTBOUND(OutboundMessage.class);
private final Class<? extends Message> type;
}
@Getter
@AllArgsConstructor
public enum CommonContentType implements TypeProvider, ContentType {
TEXT(TextContent.class);
private final Class<? extends CommonContent> type;
}
@JsonTypeResolveWith(InboundMessageJsonTypeResolver.class)
interface InboundMessage extends Message {
@Override
default Direction getDirection() {
return Direction.INBOUND;
}
}
@JsonTypeResolveWith(MessageJsonTypeResolver.class)
interface Message {
Direction getDirection();
Channel getChannel();
}
@JsonTypeResolveWith(OutboundMessageJsonTypeResolver.class)
interface OutboundMessage extends Message {
@Override
default Direction getDirection() {
return Direction.OUTBOUND;
}
}
public interface CommonContent extends SimpleJsonHierarchy<CommonContentType>, Content<CommonContentType> {
}
public interface Content<T extends ContentType> {
T getType();
}
public interface ContentType {
}
@Value
class TextContent implements CommonContent {
@NotNull
@NotEmpty
private final String text;
@Override
public CommonContentType getType() {
return CommonContentType.TEXT;
}
}
@Value
class InboundSmsMessage implements InboundMessage {
private final CommonContent content;
@Override
public Channel getChannel() {
return Channel.SMS;
}
}
@Value
class OutboundSmsMessage implements OutboundMessage {
private final CommonContent content;
@Override
public Channel getChannel() {
return Channel.SMS;
}
}
Result:
/* tslint:disable */
/* eslint-disable */
import 'reflect-metadata';
import { Type } from 'class-transformer';
import { IsDefined, IsNotEmpty } from 'class-validator';
import { CommonValidationMessages } from './CommonValidationMessages';
export enum Channel {
SMS = 'SMS',
}
export enum Direction {
INBOUND = 'INBOUND',
OUTBOUND = 'OUTBOUND',
}
export enum CommonContentType {
TEXT = 'TEXT',
}
export interface InboundMessage extends Message {
}
export interface Message {
channel: Channel;
direction: Direction;
}
export interface OutboundMessage extends Message {
}
export interface CommonContent extends Content<CommonContentType> {
type: CommonContentType;
}
export interface Content<T> {
type: T;
}
export interface ContentType {
}
export class TextContent implements CommonContent {
readonly type: CommonContentType = CommonContentType.TEXT;
@IsDefined({ message: CommonValidationMessages.IsDefined })
@IsNotEmpty({ message: CommonValidationMessages.IsNotEmpty })
text: string;
}
export class InboundSmsMessage implements InboundMessage {
readonly channel: Channel = Channel.SMS;
direction: Direction;
@Type(() => Object, {
discriminator: {
property: 'type', subTypes: [
{ value: TextContent, name: CommonContentType.TEXT }
]
}
})
content: CommonContent;
}
export class OutboundSmsMessage implements OutboundMessage {
readonly channel: Channel = Channel.SMS;
direction: Direction;
@Type(() => Object, {
discriminator: {
property: 'type', subTypes: [
{ value: TextContent, name: CommonContentType.TEXT }
]
}
})
content: CommonContent;
}
In order to link custom java validation annotation with appropriate decorator, java validation annotation must be marked @CustomTypeScriptDecorator annotation.
Also in class which extends from TypeScriptFileGenerator two methods must be overridden:
After providing the above information, TypeScriptFileGenerator will take a scan project for custom annotations and will perform logic to link annotations wit appropriate TypeScript decorators.
Annotation implementation:
@CustomTypeScriptDecorator(
typeScriptDecorator = "ComplexValidator",
decoratorParameterListExtractor = DecoratorParameterListExtractorImpl.class,
type = ComplexCustomValidation.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ComplexCustomValidator.class)
public @interface ComplexCustomValidation {
String message() default "must be valid element";
int length();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Input:
@Value
public class Foo {
@ComplexCustomValidation(length = 100)
private final String bar;
}
Result:
/* tslint:disable */
/* eslint-disable */
import { CommonValidationMessages } from './CommonValidationMessages';
import { localize } from './Localization';
export class Foo {
@ComplexValidator(100, { message: localize('must be valid element') })
bar: string;
}
Running TypeScript Generator Extension will result in two additional files:
After initial generation, this two files can be changed in order to adjust custom validation messages, or to implement a way how localization is supported. After manually making changes, you can tell TypeScript Generator Extension not to generate this files and more in the future. You can achieve this by overriding:
@Override
protected void writeCommonValidationMessagesTypeScriptFile(String code, Path filePath) {}
@Override
protected void writeLocalization(String code, Path filePath) {}
Disclaimer: in order for annotation processor to work model classes and generator configuration have to be compiled before annotation processor is run. In practice this means that they have to be in separate modules.
Main advantage of this approach: easier extension, reusability and no requirement to run Maven to generate TypeScript!
Most, if not all, options available to TypeScript Generator Maven Plugin are also available to the annotation processor.
Setup:
In Maven module where Java model is defined add the following dependency:
<dependency>
<groupId>com.infobip</groupId>
<artifactId>infobip-typescript-generator-extension-api</artifactId>
<version>${infobip-typescript-generator-extension.version}</version>
</dependency>
Configure the generator by extending TypeScriptFileGenerator:
public class SimpleTypeScriptFileGenerator extends TypeScriptFileGenerator {
public SimpleTypeScriptFileGenerator(Path basePath) {
super(basePath);
}
@Override
public Input getInput() {
return Input.from(Foo.class);
}
@Override
public Path outputFilePath(Path basePath) {
Path lib = basePath.getParent().getParent().resolve("dist");
try {
Files.createDirectories(lib);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return lib.resolve("Simple.ts");
}
@Override
protected Path getDecoratorBasePath() {
return getBasePath().getParent().getParent().resolve("src/main/typescript/decorators");
}
}
Custom java validation annotations must be marked with @CustomTSDecorator annotation. If a custom validation annotation name is not the same as a decorator name, you can specify decorator by using typeScriptDecorator annotation property.
Project only supports class-validator custom decorators custom decorators
By overriding getDecoratorBasePath() you are specifying path to typescript decorators which relates to custom java validations:
@Override
protected Path getDecoratorBasePath() {
return getBasePath().getParent().getParent().resolve("src/main/typescript/decorators");
}
Define a separate module where annotation processing will occur (this module depends on model module) with following dependency:
<dependency>
<groupId>com.infobip</groupId>
<artifactId>infobip-typescript-generator-extension-api</artifactId>
<version>${infobip-typescript-generator-extension.version}</version>
</dependency>
Add the annotation configuration class (this is only used to trigger the annotation processing with annotation):
@GenerateTypescript(generator = SimpleTypeScriptFileGenerator.class)
public class SimpleTypeScriptFileGeneratorConfiguration {
}
For more complex examples look at infobip-typescript-generator-extension-model-showcase and at infobip-typescript-generator-extension-annotation-processor-showcase.
Generated typescript can be seen in dist folder. In production you'd probably add dist to .gitignore, here it's not mainly to be used a an showcase of how the end result looks like.
Since there's no maven plugin it's possible to run TypeScript Generator with multiple different configurations in same project! Aforementioned showcase folders use this to test and showcase different parts of functionality.
If you have an idea for a new feature or want to report a bug please use the issue tracker.
Pull requests are welcome!
This library is licensed under the Apache License, Version 2.0.