Skip to content

Commit 28c1be5

Browse files
committed
support custom type mapping
1 parent 118eaa0 commit 28c1be5

File tree

10 files changed

+340
-41
lines changed

10 files changed

+340
-41
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/** Marks a type to be mapped. */
14+
@Retention(RetentionPolicy.CLASS)
15+
@Target({ElementType.TYPE})
16+
public @interface MappedParam {
17+
18+
/** Factory method name used to construct the type. Empty means use a constructor */
19+
String factoryMethod() default "";
20+
21+
@Retention(SOURCE)
22+
@Target({TYPE, PACKAGE, MODULE})
23+
@interface Import {
24+
25+
Class<?> value();
26+
27+
/** Factory method name used to construct the type. Empty means use a constructor */
28+
String factoryMethod() default "";
29+
}
30+
}

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. */
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 (not nullable) */
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)) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.avaje.http.api;
2+
3+
import static java.lang.annotation.ElementType.FIELD;
4+
import static java.lang.annotation.ElementType.PARAMETER;
5+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
6+
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.Target;
9+
10+
/** Marks a method parameter to be a path variable. */
11+
@Retention(RUNTIME)
12+
@Target({PARAMETER, FIELD})
13+
public @interface PathVariable {
14+
15+
/**
16+
* The name of the path variable.
17+
*
18+
* <p>If left blank the method parameter name is used.
19+
*/
20+
String value();
21+
}

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

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.HashSet;
1515
import java.util.Map;
1616
import java.util.Map.Entry;
17+
import java.util.Optional;
1718
import java.util.Set;
1819

1920
import javax.annotation.processing.AbstractProcessor;
@@ -22,9 +23,11 @@
2223
import javax.annotation.processing.SupportedOptions;
2324
import javax.lang.model.SourceVersion;
2425
import javax.lang.model.element.Element;
26+
import javax.lang.model.element.Modifier;
2527
import javax.lang.model.element.TypeElement;
2628
import javax.lang.model.util.ElementFilter;
2729

30+
import io.avaje.http.generator.core.TypeMap.CustomHandler;
2831
import io.avaje.prism.GenerateAPContext;
2932
import io.avaje.prism.GenerateModuleInfoReader;
3033

@@ -54,7 +57,11 @@ public SourceVersion getSupportedSourceVersion() {
5457
@Override
5558
public Set<String> getSupportedAnnotationTypes() {
5659
return Set.of(
57-
PathPrism.PRISM_TYPE, ControllerPrism.PRISM_TYPE, OpenAPIDefinitionPrism.PRISM_TYPE);
60+
PathPrism.PRISM_TYPE,
61+
ControllerPrism.PRISM_TYPE,
62+
OpenAPIDefinitionPrism.PRISM_TYPE,
63+
MappedParamPrism.PRISM_TYPE,
64+
MapImportPrism.PRISM_TYPE);
5865
}
5966

6067
@Override
@@ -88,6 +95,20 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
8895
if (round.errorRaised()) {
8996
return false;
9097
}
98+
99+
for (final var type : ElementFilter.typesIn(getElements(round, MappedParamPrism.PRISM_TYPE))) {
100+
var prism = MappedParamPrism.getInstanceOn(type);
101+
102+
registerParamMapping(type, prism.factoryMethod());
103+
}
104+
105+
for (final var type : getElements(round, MapImportPrism.PRISM_TYPE)) {
106+
107+
var prism = MapImportPrism.getInstanceOn(type);
108+
109+
registerParamMapping(APContext.asTypeElement(prism.value()), prism.factoryMethod());
110+
}
111+
91112
var pathElements = round.getElementsAnnotatedWith(typeElement(PathPrism.PRISM_TYPE));
92113
APContext.setProjectModuleElement(annotations, round);
93114
if (contextPathString == null) {
@@ -136,6 +157,37 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
136157
return false;
137158
}
138159

160+
private Set<? extends Element> getElements(RoundEnvironment round, String name) {
161+
return Optional.ofNullable(typeElement(name))
162+
.map(round::getElementsAnnotatedWith)
163+
.orElse(Set.of());
164+
}
165+
166+
private final void registerParamMapping(final TypeElement type, String factoryMethod) {
167+
if (factoryMethod.isBlank()) {
168+
Util.stringConstructor(type)
169+
.ifPresentOrElse(
170+
c -> TypeMap.add(new CustomHandler(UType.parse(type.asType()), "")),
171+
() -> logError(type, "Missing constructor %s(String s)"));
172+
173+
} else {
174+
ElementFilter.methodsIn(type.getEnclosedElements()).stream()
175+
.filter(
176+
m ->
177+
m.getSimpleName().contentEquals(factoryMethod)
178+
&& m.getModifiers().contains(Modifier.STATIC)
179+
&& m.getParameters().size() == 1
180+
&& m.getParameters()
181+
.get(0)
182+
.asType()
183+
.toString()
184+
.equals(String.class.getTypeName()))
185+
.findAny()
186+
.ifPresentOrElse(
187+
c -> TypeMap.add(new CustomHandler(UType.parse(type.asType()), factoryMethod)),
188+
() -> logError(type, "Missing static factory method %s(String s)", factoryMethod));
189+
}}
190+
139191
private void readOpenApiDefinition(RoundEnvironment round) {
140192
for (final Element element :
141193
round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) {

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

Lines changed: 51 additions & 11 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,8 +104,33 @@ private void beanParamImports(String rawType) {
101104
}
102105

103106
TypeHandler initTypeHandler() {
107+
108+
var handler = TypeMap.get(rawType);
109+
110+
final var typeOp =
111+
Optional.ofNullable(type).or(() -> Optional.of(UType.parse(element.asType())));
112+
113+
var customType = typeOp.orElseThrow();
114+
var actual = customType.isGeneric() ? UType.parse(customType.param0()) : customType;
115+
116+
if (handler == null) {
117+
Optional.ofNullable(APContext.typeElement(customType.full()))
118+
.flatMap(MappedParamPrism::getOptionalOn)
119+
.ifPresent(p -> TypeMap.add(new CustomHandler(actual, p.factoryMethod())));
120+
121+
handler = TypeMap.get(rawType);
122+
}
123+
124+
if (handler == null && ParamPrism.isPresent(element)) {
125+
126+
handler =
127+
Optional.ofNullable(APContext.typeElement(customType.full()))
128+
.flatMap(Util::stringConstructor)
129+
.map(m -> new CustomHandler(actual, ""))
130+
.orElse(null);
131+
}
132+
104133
if (specialParam) {
105-
final var typeOp = Optional.ofNullable(type).or(() -> Optional.of(UType.parse(element.asType())));
106134

107135
final var mainTypeEnum =
108136
typeOp
@@ -119,7 +147,8 @@ TypeHandler initTypeHandler() {
119147
final var isMap =
120148
!isCollection && typeOp.filter(t -> t.mainType().startsWith("java.util.Map")).isPresent();
121149

122-
final var isOptional = typeOp.filter(t -> t.mainType().startsWith("java.util.Optional")).isPresent();
150+
final var isOptional =
151+
typeOp.filter(t -> t.mainType().startsWith("java.util.Optional")).isPresent();
123152

124153
if (mainTypeEnum) {
125154
return TypeMap.enumParamHandler(typeOp.orElseThrow());
@@ -142,7 +171,7 @@ TypeHandler initTypeHandler() {
142171
}
143172
}
144173

145-
return TypeMap.get(rawType);
174+
return handler;
146175
}
147176

148177
private boolean useValidation() {
@@ -195,6 +224,14 @@ private void readAnnotations(Element element, ParamType defaultType) {
195224
return;
196225
}
197226

227+
final var pathVar = PathVariablePrism.getInstanceOn(element);
228+
if (pathVar != null) {
229+
this.paramName = nameFrom(pathVar.value(), Util.initcapSnake(snakeName));
230+
this.paramType = ParamType.PATHPARAM;
231+
this.paramDefault = null;
232+
return;
233+
}
234+
198235
final var matrixParam = MatrixParamPrism.getInstanceOn(element);
199236
if (matrixParam != null) {
200237
this.matrixParamName = nameFrom(matrixParam.value(), varName);
@@ -312,7 +349,7 @@ void writeValidate(Append writer) {
312349
}
313350

314351
void writeCtxGet(Append writer, PathSegments segments) {
315-
if (isPlatformContext() || (paramType == ParamType.BODY && platform().isBodyMethodParam())) {
352+
if (isPlatformContext() || paramType == ParamType.BODY && platform().isBodyMethodParam()) {
316353
// body passed as method parameter (Helidon)
317354
return;
318355
}
@@ -347,9 +384,9 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
347384
// path or matrix parameter
348385
final boolean requiredParam = segment.isRequired(varName);
349386
final String asMethod =
350-
(typeHandler == null)
387+
typeHandler == null
351388
? null
352-
: (requiredParam) ? typeHandler.asMethod() : typeHandler.toMethod();
389+
: requiredParam ? typeHandler.asMethod() : typeHandler.toMethod();
353390
if (asMethod != null) {
354391
writer.append(asMethod);
355392
}
@@ -362,7 +399,7 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
362399
}
363400
}
364401

365-
final String asMethod = (typeHandler == null) ? null : typeHandler.toMethod();
402+
final String asMethod = typeHandler == null ? null : typeHandler.toMethod();
366403
if (asMethod != null) {
367404
writer.append(asMethod);
368405
}
@@ -382,7 +419,8 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
382419
} else if (hasParamDefault()) {
383420
platform().writeReadParameter(writer, paramType, paramName, paramDefault.get(0));
384421
} else {
385-
final var checkNull = notNullKotlin || (paramType == ParamType.FORMPARAM && typeHandler.isPrimitive());
422+
final var checkNull =
423+
notNullKotlin || paramType == ParamType.FORMPARAM && typeHandler.isPrimitive();
386424
if (checkNull) {
387425
writer.append("checkNull(");
388426
}
@@ -398,9 +436,11 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
398436
return true;
399437
}
400438

401-
private void writeForm(Append writer, String shortType, String varName, ParamType defaultParamType) {
439+
private void writeForm(
440+
Append writer, String shortType, String varName, ParamType defaultParamType) {
402441
final TypeElement formBeanType = typeElement(rawType);
403-
final BeanParamReader form = new BeanParamReader(formBeanType, varName, shortType, defaultParamType);
442+
final BeanParamReader form =
443+
new BeanParamReader(formBeanType, varName, shortType, defaultParamType);
404444
form.write(writer);
405445
}
406446

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.avaje.http.generator.core;
2+
3+
import java.util.Optional;
4+
5+
import javax.lang.model.element.Element;
6+
7+
import io.avaje.prism.GeneratePrism;
8+
9+
@GeneratePrism(
10+
value = io.avaje.http.api.PathVariable.class,
11+
publicAccess = true,
12+
superInterfaces = ParamPrism.class)
13+
@GeneratePrism(
14+
value = io.avaje.http.api.QueryParam.class,
15+
publicAccess = true,
16+
superInterfaces = ParamPrism.class)
17+
@GeneratePrism(
18+
value = io.avaje.http.api.Cookie.class,
19+
publicAccess = true,
20+
superInterfaces = ParamPrism.class)
21+
@GeneratePrism(
22+
value = io.avaje.http.api.FormParam.class,
23+
publicAccess = true,
24+
superInterfaces = ParamPrism.class)
25+
@GeneratePrism(
26+
value = io.avaje.http.api.Header.class,
27+
publicAccess = true,
28+
superInterfaces = ParamPrism.class)
29+
@GeneratePrism(
30+
value = io.avaje.http.api.MatrixParam.class,
31+
publicAccess = true,
32+
superInterfaces = ParamPrism.class)
33+
public interface ParamPrism {
34+
35+
static boolean isPresent(Element e) {
36+
return Optional.<ParamPrism>empty()
37+
.or(() -> PathVariablePrism.getOptionalOn(e))
38+
.or(() -> QueryParamPrism.getOptionalOn(e))
39+
.or(() -> CookiePrism.getOptionalOn(e))
40+
.or(() -> FormParamPrism.getOptionalOn(e))
41+
.or(() -> HeaderPrism.getOptionalOn(e))
42+
.or(() -> MatrixParamPrism.getOptionalOn(e))
43+
.isPresent();
44+
}
45+
46+
}

0 commit comments

Comments
 (0)