Skip to content

Commit 0c1fe1a

Browse files
nivertiusvojtechhabarta
authored andcommitted
Extension for generating constructors with required properties (#324)
* Add extension that declares constructor for read-only properties * Generate initializers for single-entry enums * Normalize line endings in constructor generation extension test
1 parent 1ea97e9 commit 0c1fe1a

File tree

6 files changed

+322
-1
lines changed

6 files changed

+322
-1
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package cz.habarta.typescript.generator.ext;
2+
3+
import cz.habarta.typescript.generator.Extension;
4+
import cz.habarta.typescript.generator.TsType;
5+
import cz.habarta.typescript.generator.compiler.EnumMemberModel;
6+
import cz.habarta.typescript.generator.compiler.ModelCompiler;
7+
import cz.habarta.typescript.generator.compiler.ModelTransformer;
8+
import cz.habarta.typescript.generator.compiler.Symbol;
9+
import cz.habarta.typescript.generator.compiler.SymbolTable;
10+
import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures;
11+
import cz.habarta.typescript.generator.emitter.TsAssignmentExpression;
12+
import cz.habarta.typescript.generator.emitter.TsBeanModel;
13+
import cz.habarta.typescript.generator.emitter.TsConstructorModel;
14+
import cz.habarta.typescript.generator.emitter.TsEnumModel;
15+
import cz.habarta.typescript.generator.emitter.TsExpression;
16+
import cz.habarta.typescript.generator.emitter.TsExpressionStatement;
17+
import cz.habarta.typescript.generator.emitter.TsIdentifierReference;
18+
import cz.habarta.typescript.generator.emitter.TsMemberExpression;
19+
import cz.habarta.typescript.generator.emitter.TsModel;
20+
import cz.habarta.typescript.generator.emitter.TsModifierFlags;
21+
import cz.habarta.typescript.generator.emitter.TsParameterModel;
22+
import cz.habarta.typescript.generator.emitter.TsPropertyModel;
23+
import cz.habarta.typescript.generator.emitter.TsStatement;
24+
import cz.habarta.typescript.generator.emitter.TsStringLiteral;
25+
import cz.habarta.typescript.generator.emitter.TsThisExpression;
26+
27+
import java.util.ArrayList;
28+
import java.util.Arrays;
29+
import java.util.List;
30+
import java.util.Optional;
31+
32+
/**
33+
* Adds constructor with each required property to every generated class.
34+
*/
35+
public class RequiredPropertyConstructorExtension extends Extension {
36+
@Override
37+
public EmitterExtensionFeatures getFeatures() {
38+
EmitterExtensionFeatures features = new EmitterExtensionFeatures();
39+
features.generatesRuntimeCode = true;
40+
return features;
41+
}
42+
43+
@Override
44+
public List<TransformerDefinition> getTransformers() {
45+
return Arrays.asList(new TransformerDefinition(ModelCompiler.TransformationPhase.BeforeSymbolResolution, new ModelTransformer() {
46+
@Override
47+
public TsModel transformModel(SymbolTable symbolTable, TsModel model) {
48+
List<TsBeanModel> beans = new ArrayList<>();
49+
for (TsBeanModel bean : model.getBeans()) {
50+
TsBeanModel newBean = transformBean(bean, model);
51+
beans.add(newBean);
52+
}
53+
return model.withBeans(beans);
54+
}
55+
}));
56+
}
57+
58+
private static TsBeanModel transformBean(TsBeanModel bean, TsModel model) {
59+
if (!bean.isClass() || bean.getConstructor() != null) {
60+
return bean;
61+
}
62+
Optional<TsConstructorModel> constructorOption = createConstructor(bean, model);
63+
if (!constructorOption.isPresent()) {
64+
return bean;
65+
}
66+
return bean.withConstructor(constructorOption.get());
67+
}
68+
69+
private static Optional<TsConstructorModel> createConstructor(TsBeanModel bean, TsModel model) {
70+
List<TsParameterModel> parameters = new ArrayList<>();
71+
List<TsStatement> body = new ArrayList<>();
72+
if (bean.getParent() != null) {
73+
throw new IllegalStateException("Creating constructors for inherited beans is not currently supported");
74+
}
75+
for (TsPropertyModel property : bean.getProperties()) {
76+
if (!property.modifiers.isReadonly) {
77+
continue;
78+
}
79+
TsExpression assignmentExpression;
80+
Optional<TsExpression> predefinedValue = getPredefinedValueForProperty(property, model);
81+
if (predefinedValue.isPresent()) {
82+
assignmentExpression = predefinedValue.get();
83+
} else {
84+
parameters.add(new TsParameterModel(property.name, property.tsType));
85+
assignmentExpression = new TsIdentifierReference(property.name);
86+
}
87+
TsMemberExpression leftHandSideExpression = new TsMemberExpression(new TsThisExpression(), property.name);
88+
TsExpression assignment = new TsAssignmentExpression(leftHandSideExpression, assignmentExpression);
89+
TsExpressionStatement assignmentStatement = new TsExpressionStatement(assignment);
90+
body.add(assignmentStatement);
91+
}
92+
if(parameters.isEmpty() && body.isEmpty()) {
93+
return Optional.empty();
94+
}
95+
TsConstructorModel constructor = new TsConstructorModel(TsModifierFlags.None, parameters, body, null);
96+
return Optional.of(constructor);
97+
}
98+
99+
private static Optional<TsExpression> getPredefinedValueForProperty(TsPropertyModel property, TsModel model) {
100+
if (property.tsType instanceof TsType.UnionType) {
101+
List<TsType> unionTypeElements = ((TsType.UnionType) property.tsType).types;
102+
if (unionTypeElements.size() != 1) {
103+
return Optional.empty();
104+
}
105+
TsType onlyElement = unionTypeElements.iterator().next();
106+
if (!(onlyElement instanceof TsType.StringLiteralType)) {
107+
return Optional.empty();
108+
}
109+
TsType.StringLiteralType onlyValue = (TsType.StringLiteralType) onlyElement;
110+
TsStringLiteral expression = new TsStringLiteral(onlyValue.literal);
111+
return Optional.of(expression);
112+
}
113+
if (property.tsType instanceof TsType.EnumReferenceType) {
114+
Symbol symbol = ((TsType.EnumReferenceType) property.tsType).symbol;
115+
Optional<TsEnumModel> enumModelOption = model.getOriginalStringEnums().stream()
116+
.filter(candidate -> candidate.getName().getFullName().equals(symbol.getFullName()))
117+
.findAny();
118+
if (!enumModelOption.isPresent()) {
119+
return Optional.empty();
120+
}
121+
TsEnumModel enumModel = enumModelOption.get();
122+
if(enumModel.getMembers().size() != 1) {
123+
return Optional.empty();
124+
}
125+
EnumMemberModel singleElement = enumModel.getMembers().iterator().next();
126+
Object enumValue = singleElement.getEnumValue();
127+
TsStringLiteral expression = new TsStringLiteral((String) enumValue);
128+
return Optional.of(expression);
129+
}
130+
return Optional.empty();
131+
}
132+
133+
}

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ public static String readString(InputStream stream, String lineEndings) {
282282
return normalizeLineEndings(readString(stream), lineEndings);
283283
}
284284

285-
private static String normalizeLineEndings(String text, String lineEndings) {
285+
public static String normalizeLineEndings(String text, String lineEndings) {
286286
return text.replaceAll("\\r\\n|\\n|\\r", lineEndings);
287287
}
288288

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package cz.habarta.typescript.generator.ext;
2+
3+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
4+
import com.fasterxml.jackson.annotation.JsonTypeName;
5+
import cz.habarta.typescript.generator.ClassMapping;
6+
import cz.habarta.typescript.generator.Input;
7+
import cz.habarta.typescript.generator.JsonLibrary;
8+
import cz.habarta.typescript.generator.Settings;
9+
import cz.habarta.typescript.generator.TypeScriptFileType;
10+
import cz.habarta.typescript.generator.TypeScriptGenerator;
11+
import cz.habarta.typescript.generator.TypeScriptOutputKind;
12+
import cz.habarta.typescript.generator.util.Utils;
13+
import java.lang.reflect.Type;
14+
import org.junit.Assert;
15+
import org.junit.Test;
16+
17+
public class RequiredPropertyConstructorExtensionTest {
18+
19+
private static final String BASE_PATH = "/ext/RequiredPropertyConstructorExtensionTest-";
20+
21+
static class SimpleClass {
22+
public String field1;
23+
public PolymorphicClass field2;
24+
}
25+
26+
static class MultipleEnumContainerClass {
27+
public MultipleEntryEnum multiple;
28+
}
29+
30+
enum MultipleEntryEnum {
31+
ENTRY_1,
32+
ENTRY_2,
33+
ENTRY_3
34+
}
35+
36+
static class SingleEnumContainerClass {
37+
public SingleEntryEnum single;
38+
}
39+
40+
enum SingleEntryEnum {
41+
ENTRY_1
42+
}
43+
44+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "discriminator")
45+
interface SuperInterface {
46+
47+
}
48+
49+
@JsonTypeName("class-b")
50+
static class PolymorphicClass implements SuperInterface {
51+
public int field1;
52+
}
53+
54+
@JsonTypeName("class-c")
55+
static class SecondClass extends PolymorphicClass {
56+
public int field2;
57+
}
58+
59+
@Test
60+
public void testBasicWithReadOnly() {
61+
Settings settings = createBaseSettings();
62+
settings.declarePropertiesAsReadOnly = true;
63+
String result = generateTypeScript(settings, SimpleClass.class);
64+
65+
String expected = readResource("basicWithReadOnly.ts");
66+
67+
Assert.assertEquals(expected, result);
68+
}
69+
70+
@Test
71+
public void testBasicWithoutReadOnly() {
72+
Settings settings = createBaseSettings();
73+
settings.declarePropertiesAsReadOnly = false;
74+
String result = generateTypeScript(settings, SimpleClass.class);
75+
76+
String expected = readResource("basicWithoutReadOnly.ts");
77+
78+
Assert.assertEquals(expected, result);
79+
}
80+
81+
@Test
82+
public void testEnums() {
83+
Settings settings = createBaseSettings();
84+
settings.declarePropertiesAsReadOnly = true;
85+
86+
String result = generateTypeScript(settings, MultipleEnumContainerClass.class, SingleEnumContainerClass.class);
87+
88+
String expected = readResource("enums.ts");
89+
Assert.assertEquals(expected, result);
90+
}
91+
92+
@Test
93+
public void testInheritance() {
94+
Settings settings = createBaseSettings();
95+
settings.declarePropertiesAsReadOnly = true;
96+
97+
try {
98+
generateTypeScript(settings, SecondClass.class);
99+
Assert.fail("Expected exception");
100+
}
101+
catch (IllegalStateException expected) {
102+
Assert.assertEquals("Creating constructors for inherited beans is not currently supported", expected.getMessage());
103+
}
104+
}
105+
106+
private static String generateTypeScript(Settings settings, Type... types) {
107+
TypeScriptGenerator typeScriptGenerator = new TypeScriptGenerator(settings);
108+
String result = typeScriptGenerator.generateTypeScript(Input.from(types));
109+
return Utils.normalizeLineEndings(result, "\n");
110+
}
111+
112+
private static Settings createBaseSettings() {
113+
Settings settings = new Settings();
114+
settings.sortDeclarations = true;
115+
settings.extensions.add(new RequiredPropertyConstructorExtension());
116+
settings.jsonLibrary = JsonLibrary.jackson2;
117+
settings.outputFileType = TypeScriptFileType.implementationFile;
118+
settings.outputKind = TypeScriptOutputKind.module;
119+
settings.mapClasses = ClassMapping.asClasses;
120+
settings.noFileComment = true;
121+
return settings;
122+
}
123+
124+
private String readResource(String suffix) {
125+
return Utils.readString(getClass().getResourceAsStream(BASE_PATH + suffix), "\n");
126+
}
127+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* tslint:disable */
2+
3+
export class PolymorphicClass implements SuperInterface {
4+
readonly discriminator: "class-b";
5+
readonly field1: number;
6+
7+
constructor(field1: number) {
8+
this.discriminator = "class-b";
9+
this.field1 = field1;
10+
}
11+
}
12+
13+
export class SimpleClass {
14+
readonly field1: string;
15+
readonly field2: PolymorphicClass;
16+
17+
constructor(field1: string, field2: PolymorphicClass) {
18+
this.field1 = field1;
19+
this.field2 = field2;
20+
}
21+
}
22+
23+
export interface SuperInterface {
24+
readonly discriminator: "class-b";
25+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* tslint:disable */
2+
3+
export class PolymorphicClass implements SuperInterface {
4+
discriminator: "class-b";
5+
field1: number;
6+
}
7+
8+
export class SimpleClass {
9+
field1: string;
10+
field2: PolymorphicClass;
11+
}
12+
13+
export interface SuperInterface {
14+
discriminator: "class-b";
15+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* tslint:disable */
2+
3+
export class MultipleEnumContainerClass {
4+
readonly multiple: MultipleEntryEnum;
5+
6+
constructor(multiple: MultipleEntryEnum) {
7+
this.multiple = multiple;
8+
}
9+
}
10+
11+
export class SingleEnumContainerClass {
12+
readonly single: SingleEntryEnum;
13+
14+
constructor() {
15+
this.single = "ENTRY_1";
16+
}
17+
}
18+
19+
export type MultipleEntryEnum = "ENTRY_1" | "ENTRY_2" | "ENTRY_3";
20+
21+
export type SingleEntryEnum = "ENTRY_1";

0 commit comments

Comments
 (0)