Skip to content

infobip/infobip-typescript-generator-extension

Repository files navigation

Infobip TypeScript Generator Extension

Maven Central Coverage Status Known Vulnerabilities

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.

Contents

  1. Bean Validation to class-validator translation
  2. Custom Validation to class-validator translation
  3. Localization.ts & CommonValidationMessages.ts
  4. Time type mappings
  5. Annotation processor
  6. Contributing
  7. License

Bean Validation to class-validator translation

Simple Object

Input:

public record Foo(

        @Size(min = 1, max = 2)
        @NotEmpty
        @NotNull
        @Valid
        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;
}

Hierarchy

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:

enum Channel {
    SMS(InboundSmsMessage.class, OutboundSmsMessage.class);

    private final Class<? extends InboundMessage> inboundMessageType;
    private final Class<? extends OutboundMessage> outboundMessageType;

    Channel(Class<? extends InboundMessage> inboundMessageType,
            Class<? extends OutboundMessage> outboundMessageType) {
        this.inboundMessageType = inboundMessageType;
        this.outboundMessageType = outboundMessageType;
    }

    public Class<? extends InboundMessage> getInboundMessageType() {
        return inboundMessageType;
    }

    public Class<? extends OutboundMessage> getOutboundMessageType() {
        return outboundMessageType;
    }
}

enum Direction implements TypeProvider<Message> {
    INBOUND(InboundMessage.class),
    OUTBOUND(OutboundMessage.class);

    private final Class<? extends Message> type;

    Direction(Class<? extends Message> type) {
        this.type = type;
    }

    @Override
    public Class<? extends Message> getType() {
        return type;
    }
}

public enum CommonContentType implements TypeProvider<CommonContent>, ContentType {
    TEXT(TextContent.class);

    private final Class<? extends CommonContent> type;

    CommonContentType(Class<? extends CommonContent> type) {
        this.type = type;
    }

    @Override
    public Class<? extends CommonContent> getType() {
        return 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 {

}

record TextContent(

        @NotNull
        @NotEmpty
        String text

) implements CommonContent {

    @Override
    public CommonContentType getType() {
        return CommonContentType.TEXT;
    }
}

record InboundSmsMessage(

        CommonContent content

) implements InboundMessage {

    @Override
    public Channel getChannel() {
        return Channel.SMS;
    }
}

record OutboundSmsMessage(

        CommonContent content

) implements OutboundMessage {

    @Override
    public Channel getChannel() {
        return Channel.SMS;
    }
}

Result:

/* tslint:disable */
/* eslint-disable */
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;
}

Custom Validation to class-validator translation

@CustomTypeScriptDecorator

In order to link custom java validation annotation with appropriate decorator, java validation annotation must be marked @CustomTypeScriptDecorator annotation.

  • @CustomTypeScriptDecorator has:
    • typeScriptDecorator optional parameter:
      • if a provided annotation is going to be linked to a decorator with a specified name
      • else it is going to be linked to a decorator with the same name as an annotation
    • decoratorParameterListExtractor optional parameter:
      • provides an implementation of a class which extracts annotation parameters
      • provided class must implement DecoratorParameterListExtractor interface

Also in class which extends from TypeScriptFileGenerator two methods must be overridden:

  • getCustomValidationAnnotationSettings:
    • Which will provide settings needed for locating custom java annotations. Setting will provide name of java package from where annotation scanning will begin.
  • getCustomDecoratorBasePath:
    • Which will provide base path to TypeScript decorators

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.

Limitations

  1. From class-validation only Custom Validation Decorators are supported, reason behind supporting only Custom Validation Decorators and not supporting [Custom Validation Classes](Custom Validation Decorators) as well, is that in first approach we are able to link custom java annotation with TypescriptDecorator by just looking at decorator's file name, while in second approach we would need to parse a whole file in order to find where is a decorator defined.
  2. TypeScript decorator file name and decorator must be the same, reason behind this is similar to previous point, if we decide to not follow given convention we would need to parse the whole Typescript file in order to find a given decorator. This would introduce additional complexity. This also means that one decorator is located in one TypeScript file.
  3. All decorators will be copied in dist location under validation folder.
  4. getCustomValidationAnnotationSettings must be overridden in class which extends from TypeScriptFileGenerator. This is done to restrict scope of ** ClassGraph** annotation scanning. By default, ClassGraph will scan all classes in the class path and will try to extract annotations from them, if restriction is not performed given operation could result in OutOfMemoryError.

Example

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:

public record Foo(

        @ComplexCustomValidation(length = 100)
        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;
}

Localization.ts & CommonValidationMessages.ts

Running TypeScript Generator Extension will result in two additional files:

  • Localization.ts
    • Blueprint for Localization functionality
  • CommonValidationMessages.ts
    • Blueprint for common validation messages

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:

  • writeCommonValidationMessagesTypeScriptFile for CommonValidationMessages.ts:
    • @Override
      protected void writeCommonValidationMessagesTypeScriptFile(String code, Path filePath) {}
  • writeLocalization Localization.ts:
    • @Override
      protected void writeLocalization(String code, Path filePath) {}

Time type mappings

Time type mappings are mapped to string by default.

import java.time.*;

public record Foo(

        Instant instant,

        LocalDateTime localDateTime,

        ZonedDateTime zonedDateTime,

        Duration duration

) {


}
/* tslint:disable */
/* eslint-disable */

export class Foo {
    instant: string;
    localDateTime: string;
    zonedDateTime: string;
    duration: string;
}

Annotation processor

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:

  1. 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>
  2. 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");
         }
    }
  3. 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.

  4. Project only supports class-validator custom decorators custom decorators

  5. 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");
         }
  6. 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>
  7. 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.

Contributing

If you have an idea for a new feature or want to report a bug please use the issue tracker.

Pull requests are welcome!

License

This library is licensed under the Apache License, Version 2.0.