Skip to content

Commit 6916132

Browse files
authored
support custom type mapping (#644)
1 parent 4dd51a8 commit 6916132

File tree

9 files changed

+323
-72
lines changed

9 files changed

+323
-72
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.avaje.http.api;
2+
3+
import static java.lang.annotation.ElementType.MODULE;
4+
import static java.lang.annotation.ElementType.PACKAGE;
5+
import static java.lang.annotation.ElementType.TYPE;
6+
import static java.lang.annotation.RetentionPolicy.SOURCE;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Repeatable;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
14+
/**
15+
* Marks a type to be mapped.
16+
* <p>
17+
* The type needs to have a single constructor argument that is a String type,
18+
* or have a factory method that has a single argument that is a String type.
19+
*/
20+
@Retention(RetentionPolicy.CLASS)
21+
@Target({ElementType.TYPE})
22+
public @interface MappedParam {
23+
24+
/** Factory method name used to construct the type. Empty means use a constructor */
25+
String factoryMethod() default "";
26+
27+
/**
28+
* Import a type to be mapped.
29+
* <p>
30+
* The type needs to have a single constructor argument that is a String type,
31+
* or have a factory method that has a single argument that is a String type.
32+
*/
33+
@Repeatable(MappedParam.Import.Imports.class)
34+
@Retention(SOURCE)
35+
@Target({TYPE, PACKAGE, MODULE})
36+
@interface Import {
37+
38+
Class<?> value();
39+
40+
/** Factory method name used to construct the type. Empty means use a constructor */
41+
String factoryMethod() default "";
42+
43+
@Retention(SOURCE)
44+
@Target({TYPE, PACKAGE, MODULE})
45+
@interface Imports {
46+
47+
Import[] value();
48+
}
49+
}
50+
}

http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ public static int asInt(String value) {
9898
}
9999
}
100100

101+
/** Convert to type (not nullable). */
102+
public static <T> T asType(Function<String, T> typeConversion, String value) {
103+
checkNull(value);
104+
return typeConversion.apply(value);
105+
}
106+
101107
/**
102108
* Convert to enum.
103109
*/
@@ -288,9 +294,15 @@ public static Integer asInteger(String value) {
288294
}
289295
}
290296

291-
/**
292-
* Convert to enum of the given type.
293-
*/
297+
/** Convert to type */
298+
public static <T> T toType(Function<String, T> typeConversion, String value) {
299+
if (isNullOrEmpty(value)) {
300+
return null;
301+
}
302+
return typeConversion.apply(value);
303+
}
304+
305+
/** Convert to enum of the given type. */
294306
@SuppressWarnings({"rawtypes"})
295307
public static <T> Enum toEnum(Class<T> clazz, String value) {
296308
if (isNullOrEmpty(value)) {

http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
package io.avaje.http.generator.core;
22

3-
import static io.avaje.http.generator.core.ProcessingContext.doc;
4-
import static io.avaje.http.generator.core.ProcessingContext.elements;
5-
import static io.avaje.http.generator.core.ProcessingContext.isOpenApiAvailable;
6-
import static io.avaje.http.generator.core.ProcessingContext.logError;
7-
import static io.avaje.http.generator.core.ProcessingContext.typeElement;
3+
import static io.avaje.http.generator.core.ProcessingContext.*;
84
import static java.util.stream.Collectors.toMap;
95

106
import java.io.IOException;
@@ -14,6 +10,7 @@
1410
import java.util.HashSet;
1511
import java.util.Map;
1612
import java.util.Map.Entry;
13+
import java.util.Optional;
1714
import java.util.Set;
1815

1916
import javax.annotation.processing.AbstractProcessor;
@@ -22,9 +19,12 @@
2219
import javax.annotation.processing.SupportedOptions;
2320
import javax.lang.model.SourceVersion;
2421
import javax.lang.model.element.Element;
22+
import javax.lang.model.element.ExecutableElement;
23+
import javax.lang.model.element.Modifier;
2524
import javax.lang.model.element.TypeElement;
2625
import javax.lang.model.util.ElementFilter;
2726

27+
import io.avaje.http.generator.core.TypeMap.CustomHandler;
2828
import io.avaje.prism.GenerateAPContext;
2929
import io.avaje.prism.GenerateModuleInfoReader;
3030

@@ -54,7 +54,11 @@ public SourceVersion getSupportedSourceVersion() {
5454
@Override
5555
public Set<String> getSupportedAnnotationTypes() {
5656
return Set.of(
57-
PathPrism.PRISM_TYPE, ControllerPrism.PRISM_TYPE, OpenAPIDefinitionPrism.PRISM_TYPE);
57+
PathPrism.PRISM_TYPE,
58+
ControllerPrism.PRISM_TYPE,
59+
OpenAPIDefinitionPrism.PRISM_TYPE,
60+
MappedParamPrism.PRISM_TYPE,
61+
MapImportPrism.PRISM_TYPE);
5862
}
5963

6064
@Override
@@ -65,7 +69,6 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
6569

6670
try {
6771
var txtFilePath = APContext.getBuildResource(HTTP_CONTROLLERS_TXT);
68-
6972
if (txtFilePath.toFile().exists()) {
7073
Files.lines(txtFilePath).forEach(clientFQNs::add);
7174
}
@@ -75,8 +78,8 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
7578
}
7679
}
7780
} catch (IOException e) {
78-
e.printStackTrace();
7981
// not worth failing over
82+
logWarn("Error reading test controllers %s", e);
8083
}
8184
}
8285

@@ -88,6 +91,17 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
8891
if (round.errorRaised()) {
8992
return false;
9093
}
94+
95+
for (final var type : ElementFilter.typesIn(getElements(round, MappedParamPrism.PRISM_TYPE))) {
96+
var prism = MappedParamPrism.getInstanceOn(type);
97+
registerParamMapping(type, prism.factoryMethod());
98+
}
99+
100+
for (final var type : getElements(round, MapImportPrism.PRISM_TYPE)) {
101+
var prism = MapImportPrism.getInstanceOn(type);
102+
registerParamMapping(APContext.asTypeElement(prism.value()), prism.factoryMethod());
103+
}
104+
91105
var pathElements = round.getElementsAnnotatedWith(typeElement(PathPrism.PRISM_TYPE));
92106
APContext.setProjectModuleElement(annotations, round);
93107
if (contextPathString == null) {
@@ -111,9 +125,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
111125
readSecuritySchemes(round);
112126
}
113127

114-
for (final var controller :
115-
ElementFilter.typesIn(
116-
round.getElementsAnnotatedWith(typeElement(ControllerPrism.PRISM_TYPE)))) {
128+
for (final var controller : ElementFilter.typesIn(round.getElementsAnnotatedWith(typeElement(ControllerPrism.PRISM_TYPE)))) {
117129
writeAdapter(controller);
118130
}
119131

@@ -136,9 +148,33 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
136148
return false;
137149
}
138150

151+
private Set<? extends Element> getElements(RoundEnvironment round, String name) {
152+
return Optional.ofNullable(typeElement(name))
153+
.map(round::getElementsAnnotatedWith)
154+
.orElse(Set.of());
155+
}
156+
157+
private final void registerParamMapping(final TypeElement type, String factoryMethod) {
158+
if (factoryMethod.isBlank()) {
159+
Util.stringConstructor(type)
160+
.ifPresentOrElse(
161+
c -> TypeMap.add(new CustomHandler(UType.parse(type.asType()), "")),
162+
() -> logError(type, "Missing constructor %s(String s)"));
163+
164+
} else {
165+
ElementFilter.methodsIn(type.getEnclosedElements()).stream()
166+
.filter(m -> m.getSimpleName().contentEquals(factoryMethod)
167+
&& m.getModifiers().contains(Modifier.STATIC)
168+
&& Util.singleStringParam(m))
169+
.findAny()
170+
.ifPresentOrElse(
171+
c -> TypeMap.add(new CustomHandler(UType.parse(type.asType()), factoryMethod)),
172+
() -> logError(type, "Missing static factory method %s(String s)", factoryMethod));
173+
}
174+
}
175+
139176
private void readOpenApiDefinition(RoundEnvironment round) {
140-
for (final Element element :
141-
round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) {
177+
for (final Element element : round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) {
142178
doc().readApiDefinition(element);
143179
}
144180
}
@@ -147,19 +183,16 @@ private void readTagDefinitions(RoundEnvironment round) {
147183
for (final Element element : round.getElementsAnnotatedWith(typeElement(TagPrism.PRISM_TYPE))) {
148184
doc().addTagDefinition(element);
149185
}
150-
for (final Element element :
151-
round.getElementsAnnotatedWith(typeElement(TagsPrism.PRISM_TYPE))) {
186+
for (final Element element : round.getElementsAnnotatedWith(typeElement(TagsPrism.PRISM_TYPE))) {
152187
doc().addTagsDefinition(element);
153188
}
154189
}
155190

156191
private void readSecuritySchemes(RoundEnvironment round) {
157-
for (final Element element :
158-
round.getElementsAnnotatedWith(typeElement(SecuritySchemePrism.PRISM_TYPE))) {
192+
for (final Element element : round.getElementsAnnotatedWith(typeElement(SecuritySchemePrism.PRISM_TYPE))) {
159193
doc().addSecurityScheme(element);
160194
}
161-
for (final Element element :
162-
round.getElementsAnnotatedWith(typeElement(SecuritySchemesPrism.PRISM_TYPE))) {
195+
for (final Element element : round.getElementsAnnotatedWith(typeElement(SecuritySchemesPrism.PRISM_TYPE))) {
163196
doc().addSecuritySchemes(element);
164197
}
165198
}
@@ -174,7 +207,6 @@ private void writeAdapter(TypeElement controller) {
174207
final var reader = new ControllerReader(controller, contextPath);
175208
reader.read(true);
176209
try {
177-
178210
writeControllerAdapter(reader);
179211
writeClientAdapter(reader);
180212

@@ -184,7 +216,6 @@ private void writeAdapter(TypeElement controller) {
184216
}
185217

186218
private void writeClientAdapter(ControllerReader reader) {
187-
188219
try {
189220
if (reader.beanType().getInterfaces().isEmpty()
190221
&& "java.lang.Object".equals(reader.beanType().getSuperclass().toString())

http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import javax.lang.model.element.*;
1515
import javax.lang.model.type.TypeMirror;
1616

17+
import io.avaje.http.generator.core.TypeMap.CustomHandler;
1718
import io.avaje.http.generator.core.openapi.MethodDocBuilder;
1819
import io.avaje.http.generator.core.openapi.MethodParamDocBuilder;
1920

@@ -76,7 +77,9 @@ public class ElementReader {
7677
if (!contextType) {
7778
readAnnotations(element, defaultType);
7879
useValidation = useValidation();
79-
HttpValidPrism.getOptionalOn(element.getEnclosingElement()).map(HttpValidPrism::groups).stream()
80+
HttpValidPrism.getOptionalOn(element.getEnclosingElement())
81+
.map(HttpValidPrism::groups)
82+
.stream()
8083
.flatMap(List::stream)
8184
.map(TypeMirror::toString)
8285
.forEach(validationGroups::add);
@@ -101,37 +104,58 @@ private void beanParamImports(String rawType) {
101104
}
102105

103106
TypeHandler initTypeHandler() {
104-
if (specialParam) {
105-
final var typeOp = Optional.ofNullable(type).or(() -> Optional.of(UType.parse(element.asType())));
107+
var handler = TypeMap.get(rawType);
108+
final var typeOp = Optional.ofNullable(type).or(() -> Optional.of(UType.parse(element.asType())));
109+
110+
var customType = typeOp.orElseThrow();
111+
var actual = customType.isGeneric() ? UType.parse(customType.param0()) : customType;
112+
113+
if (handler == null) {
114+
Optional.ofNullable(APContext.typeElement(customType.full()))
115+
.flatMap(MappedParamPrism::getOptionalOn)
116+
.ifPresent(p -> TypeMap.add(new CustomHandler(actual, p.factoryMethod())));
106117

118+
handler = TypeMap.get(rawType);
119+
}
120+
121+
if (handler == null && ParamPrism.isPresent(element)) {
122+
handler =
123+
Optional.ofNullable(APContext.typeElement(customType.full()))
124+
.flatMap(Util::stringConstructor)
125+
.map(m -> new CustomHandler(actual, ""))
126+
.orElse(null);
127+
}
128+
129+
if (specialParam) {
107130
final var mainTypeEnum =
108-
typeOp
109-
.flatMap(t -> Optional.ofNullable(typeElement(t.mainType())))
110-
.map(TypeElement::getKind)
111-
.filter(ElementKind.ENUM::equals)
112-
.isPresent();
131+
typeOp
132+
.flatMap(t -> Optional.ofNullable(typeElement(t.mainType())))
133+
.map(TypeElement::getKind)
134+
.filter(ElementKind.ENUM::equals)
135+
.isPresent();
113136

114137
final var isCollection =
115-
typeOp
116-
.filter(t -> t.isGeneric() && !t.mainType().startsWith("java.util.Map"))
117-
.isPresent();
138+
typeOp
139+
.filter(t -> t.isGeneric() && !t.mainType().startsWith("java.util.Map"))
140+
.isPresent();
118141

119142
final var isMap =
120-
!isCollection && typeOp.filter(t -> t.mainType().startsWith("java.util.Map")).isPresent();
143+
!isCollection && typeOp.filter(t -> t.mainType().startsWith("java.util.Map")).isPresent();
121144

122-
final var isOptional = typeOp.filter(t -> t.mainType().startsWith("java.util.Optional")).isPresent();
145+
final var isOptional =
146+
typeOp.filter(t -> t.mainType().startsWith("java.util.Optional")).isPresent();
123147

124148
if (mainTypeEnum) {
125149
return TypeMap.enumParamHandler(typeOp.orElseThrow());
126150
} else if (isCollection || isOptional) {
127151
final var isEnumContainer =
128-
typeOp
129-
.flatMap(t -> Optional.ofNullable(typeElement(t.param0())))
130-
.map(TypeElement::getKind)
131-
.filter(ElementKind.ENUM::equals)
132-
.isPresent();
152+
typeOp
153+
.flatMap(t -> Optional.ofNullable(typeElement(t.param0())))
154+
.map(TypeElement::getKind)
155+
.filter(ElementKind.ENUM::equals)
156+
.isPresent();
133157

134-
if (isOptional) {//Needs to be checked first, as 'isCollection' is too broad
158+
if (isOptional) { // needs to be checked first, as 'isCollection' is too broad
135159
return TypeMap.optionalHandler(typeOp.orElseThrow(), isEnumContainer);
136160
}
137161
this.isParamCollection = true;
@@ -142,7 +166,7 @@ TypeHandler initTypeHandler() {
142166
}
143167
}
144168

145-
return TypeMap.get(rawType);
169+
return handler;
146170
}
147171

148172
private boolean useValidation() {
@@ -312,7 +336,7 @@ void writeValidate(Append writer) {
312336
}
313337

314338
void writeCtxGet(Append writer, PathSegments segments) {
315-
if (isPlatformContext() || (paramType == ParamType.BODY && platform().isBodyMethodParam())) {
339+
if (isPlatformContext() || paramType == ParamType.BODY && platform().isBodyMethodParam()) {
316340
// body passed as method parameter (Helidon)
317341
return;
318342
}
@@ -347,9 +371,9 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
347371
// path or matrix parameter
348372
final boolean requiredParam = segment.isRequired(varName);
349373
final String asMethod =
350-
(typeHandler == null)
351-
? null
352-
: (requiredParam) ? typeHandler.asMethod() : typeHandler.toMethod();
374+
typeHandler == null
375+
? null
376+
: requiredParam ? typeHandler.asMethod() : typeHandler.toMethod();
353377
if (asMethod != null) {
354378
writer.append(asMethod);
355379
}
@@ -362,7 +386,7 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
362386
}
363387
}
364388

365-
final String asMethod = (typeHandler == null) ? null : typeHandler.toMethod();
389+
final String asMethod = typeHandler == null ? null : typeHandler.toMethod();
366390
if (asMethod != null) {
367391
writer.append(asMethod);
368392
}
@@ -382,7 +406,8 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
382406
} else if (hasParamDefault()) {
383407
platform().writeReadParameter(writer, paramType, paramName, paramDefault.get(0));
384408
} else {
385-
final var checkNull = notNullKotlin || (paramType == ParamType.FORMPARAM && typeHandler.isPrimitive());
409+
final var checkNull =
410+
notNullKotlin || paramType == ParamType.FORMPARAM && typeHandler.isPrimitive();
386411
if (checkNull) {
387412
writer.append("checkNull(");
388413
}

0 commit comments

Comments
 (0)