Skip to content

Commit 5b681f1

Browse files
krzsvojtechhabarta
authored andcommitted
New extension: OnePossiblePropertyValueAssigningExtension (#388)
* New extension: OnePossiblePropertyValueAssigningExtension * Fixing javadoc (removing HTML tag)
1 parent 12943f7 commit 5b681f1

File tree

3 files changed

+288
-0
lines changed

3 files changed

+288
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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.Symbol;
8+
import cz.habarta.typescript.generator.compiler.SymbolTable;
9+
import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures;
10+
import cz.habarta.typescript.generator.emitter.TsAssignmentExpression;
11+
import cz.habarta.typescript.generator.emitter.TsBeanModel;
12+
import cz.habarta.typescript.generator.emitter.TsCallExpression;
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.TsMemberExpression;
18+
import cz.habarta.typescript.generator.emitter.TsModel;
19+
import cz.habarta.typescript.generator.emitter.TsModifierFlags;
20+
import cz.habarta.typescript.generator.emitter.TsPropertyModel;
21+
import cz.habarta.typescript.generator.emitter.TsStatement;
22+
import cz.habarta.typescript.generator.emitter.TsStringLiteral;
23+
import cz.habarta.typescript.generator.emitter.TsSuperExpression;
24+
import cz.habarta.typescript.generator.emitter.TsThisExpression;
25+
26+
import java.util.ArrayList;
27+
import java.util.Collection;
28+
import java.util.Collections;
29+
import java.util.List;
30+
import java.util.Optional;
31+
import java.util.stream.Collectors;
32+
33+
/**
34+
* The extension marks all properties which type allows only one possible value (for instance, enum with only one value
35+
* or {@link cz.habarta.typescript.generator.TsType.UnionType} with only one option) as read only and sets their
36+
* value in the constructor.
37+
* It may be useful while generating code for class hierarchy where each subclass has a discriminator property that can
38+
* only have one value. Hence, using the TypeScript code it will not be necessary to set this value manually.
39+
*
40+
* @author krzs
41+
*/
42+
public class OnePossiblePropertyValueAssigningExtension extends Extension {
43+
44+
@Override
45+
public EmitterExtensionFeatures getFeatures() {
46+
EmitterExtensionFeatures features = new EmitterExtensionFeatures();
47+
features.generatesRuntimeCode = true;
48+
return features;
49+
}
50+
51+
@Override
52+
public List<TransformerDefinition> getTransformers() {
53+
return Collections.singletonList(
54+
new TransformerDefinition(ModelCompiler.TransformationPhase.AfterDeclarationSorting,
55+
OnePossiblePropertyValueAssigningExtension::transformModel)
56+
);
57+
}
58+
59+
private static TsModel transformModel(SymbolTable symbolTable, TsModel model) {
60+
List<TsBeanModel> beans = model.getBeans().stream()
61+
.map(bean -> transformBean(bean, model))
62+
.collect(Collectors.toList());
63+
return model.withBeans(beans);
64+
}
65+
66+
private static TsBeanModel transformBean(TsBeanModel bean, TsModel model) {
67+
if (!bean.isClass() || bean.getConstructor() != null) {
68+
return bean;
69+
}
70+
71+
List<TsPropertyModel> newProperties = new ArrayList<>();
72+
Collection<TsExpressionStatement> valueAssignmentStatements = new ArrayList<>();
73+
74+
for (TsPropertyModel property : bean.getProperties()) {
75+
TsPropertyModel newProperty = property;
76+
77+
Optional<TsExpression> onlyValue = findOnlyValueForProperty(property, model);
78+
if (onlyValue.isPresent()) {
79+
newProperty = new TsPropertyModel(property.name, property.tsType,
80+
TsModifierFlags.None.setReadonly(), property.ownProperty, property.comments);
81+
82+
TsExpressionStatement assignmentStatement = createValueAssignmentStatement(newProperty, onlyValue.get());
83+
valueAssignmentStatements.add(assignmentStatement);
84+
}
85+
86+
newProperties.add(newProperty);
87+
}
88+
89+
TsBeanModel newBean = bean.withProperties(newProperties);
90+
if (!valueAssignmentStatements.isEmpty()) {
91+
TsConstructorModel constructor = createConstructor(bean, valueAssignmentStatements);
92+
newBean = newBean.withConstructor(constructor);
93+
}
94+
return newBean;
95+
}
96+
97+
private static TsConstructorModel createConstructor(TsBeanModel bean,
98+
Collection<TsExpressionStatement> valueAssignmentStatements) {
99+
List<TsStatement> body = new ArrayList<>();
100+
if (bean.getParent() != null) {
101+
body.add(new TsExpressionStatement(new TsCallExpression(new TsSuperExpression())));
102+
}
103+
104+
body.addAll(valueAssignmentStatements);
105+
106+
return new TsConstructorModel(TsModifierFlags.None, Collections.emptyList(), body, null);
107+
}
108+
109+
private static TsExpressionStatement createValueAssignmentStatement(TsPropertyModel property, TsExpression value) {
110+
TsMemberExpression leftHandSideExpression = new TsMemberExpression(new TsThisExpression(), property.name);
111+
TsExpression assignment = new TsAssignmentExpression(leftHandSideExpression, value);
112+
return new TsExpressionStatement(assignment);
113+
}
114+
115+
private static Optional<TsExpression> findOnlyValueForProperty(TsPropertyModel property, TsModel model) {
116+
TsType propertyType = property.tsType;
117+
if (propertyType instanceof TsType.UnionType) {
118+
return findOnlyValueForUnionType((TsType.UnionType) propertyType);
119+
}
120+
if (propertyType instanceof TsType.EnumReferenceType) {
121+
return findOnlyValueForEnumReferenceType(model, (TsType.EnumReferenceType) propertyType);
122+
}
123+
return Optional.empty();
124+
}
125+
126+
private static Optional<TsExpression> findOnlyValueForUnionType(TsType.UnionType unionType) {
127+
List<TsType> unionTypeElements = unionType.types;
128+
if (unionTypeElements.size() != 1) {
129+
return Optional.empty();
130+
}
131+
TsType onlyElement = unionTypeElements.iterator().next();
132+
if (!(onlyElement instanceof TsType.StringLiteralType)) {
133+
return Optional.empty();
134+
}
135+
TsType.StringLiteralType onlyValue = (TsType.StringLiteralType) onlyElement;
136+
TsStringLiteral expression = new TsStringLiteral(onlyValue.literal);
137+
return Optional.of(expression);
138+
}
139+
140+
private static Optional<TsExpression> findOnlyValueForEnumReferenceType(TsModel model,
141+
TsType.EnumReferenceType propertyType) {
142+
Symbol symbol = propertyType.symbol;
143+
Optional<TsEnumModel> enumModelOption = model.getOriginalStringEnums().stream()
144+
.filter(candidate -> candidate.getName().getFullName().equals(symbol.getFullName()))
145+
.findAny();
146+
if (!enumModelOption.isPresent()) {
147+
return Optional.empty();
148+
}
149+
TsEnumModel enumModel = enumModelOption.get();
150+
if (enumModel.getMembers().size() != 1) {
151+
return Optional.empty();
152+
}
153+
EnumMemberModel singleElement = enumModel.getMembers().iterator().next();
154+
Object enumValue = singleElement.getEnumValue();
155+
TsStringLiteral expression = new TsStringLiteral((String) enumValue);
156+
return Optional.of(expression);
157+
}
158+
159+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package cz.habarta.typescript.generator.ext;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
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+
15+
import org.junit.Assert;
16+
import org.junit.Test;
17+
18+
public class OnePossiblePropertyValueAssigningExtensionTest {
19+
private static final String BASE_PATH = "/ext/OnePossiblePropertyValueAssigningExtensionTest-";
20+
21+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "discriminator")
22+
static class BaseClass {
23+
24+
@JsonProperty
25+
private Long field1;
26+
27+
@JsonProperty
28+
private OneValueEnum field2;
29+
}
30+
31+
static class SubClass extends BaseClass {
32+
33+
@JsonProperty
34+
private String testField1;
35+
}
36+
37+
static class OtherSubClass extends BaseClass {
38+
39+
@JsonProperty
40+
private String testField2;
41+
42+
@JsonProperty
43+
private OneValueEnum enumField1;
44+
45+
@JsonProperty
46+
private TwoValueEnum enumField2;
47+
}
48+
49+
enum OneValueEnum {
50+
MY_VALUE
51+
}
52+
53+
enum TwoValueEnum {
54+
ONE,
55+
TWO
56+
}
57+
58+
@Test
59+
public void testGeneration() {
60+
Settings settings = createBaseSettings(new OnePossiblePropertyValueAssigningExtension());
61+
String result = generateTypeScript(settings, SubClass.class, OtherSubClass.class);
62+
63+
String expected = readResource("all.ts");
64+
65+
Assert.assertEquals(expected, result);
66+
}
67+
68+
private static Settings createBaseSettings(OnePossiblePropertyValueAssigningExtension extension) {
69+
Settings settings = new Settings();
70+
settings.sortDeclarations = true;
71+
settings.extensions.add(extension);
72+
settings.jsonLibrary = JsonLibrary.jackson2;
73+
settings.outputFileType = TypeScriptFileType.implementationFile;
74+
settings.outputKind = TypeScriptOutputKind.module;
75+
settings.mapClasses = ClassMapping.asClasses;
76+
settings.noFileComment = true;
77+
settings.noEslintDisable = true;
78+
return settings;
79+
}
80+
81+
private static String generateTypeScript(Settings settings, Type... types) {
82+
TypeScriptGenerator typeScriptGenerator = new TypeScriptGenerator(settings);
83+
String result = typeScriptGenerator.generateTypeScript(Input.from(types));
84+
return Utils.normalizeLineEndings(result, "\n");
85+
}
86+
87+
private String readResource(String suffix) {
88+
return Utils.readString(getClass().getResourceAsStream(BASE_PATH + suffix), "\n");
89+
}
90+
91+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* tslint:disable */
2+
3+
export class BaseClass {
4+
discriminator: "OnePossiblePropertyValueAssigningExtensionTest$SubClass" | "OnePossiblePropertyValueAssigningExtensionTest$OtherSubClass";
5+
field1: number;
6+
readonly field2: OneValueEnum;
7+
8+
constructor() {
9+
this.field2 = "MY_VALUE";
10+
}
11+
}
12+
13+
export class OtherSubClass extends BaseClass {
14+
readonly discriminator: "OnePossiblePropertyValueAssigningExtensionTest$OtherSubClass";
15+
readonly enumField1: OneValueEnum;
16+
enumField2: TwoValueEnum;
17+
testField2: string;
18+
19+
constructor() {
20+
super();
21+
this.discriminator = "OnePossiblePropertyValueAssigningExtensionTest$OtherSubClass";
22+
this.enumField1 = "MY_VALUE";
23+
}
24+
}
25+
26+
export class SubClass extends BaseClass {
27+
readonly discriminator: "OnePossiblePropertyValueAssigningExtensionTest$SubClass";
28+
testField1: string;
29+
30+
constructor() {
31+
super();
32+
this.discriminator = "OnePossiblePropertyValueAssigningExtensionTest$SubClass";
33+
}
34+
}
35+
36+
export type OneValueEnum = "MY_VALUE";
37+
38+
export type TwoValueEnum = "ONE" | "TWO";

0 commit comments

Comments
 (0)