Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions http-api/src/main/java/io/avaje/http/api/MappedParam.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.avaje.http.api;

import static java.lang.annotation.ElementType.MODULE;
import static java.lang.annotation.ElementType.PACKAGE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.SOURCE;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks a type to be mapped.
* <p>
* The type needs to have a single constructor argument that is a String type,
* or have a factory method that has a single argument that is a String type.
*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE})
public @interface MappedParam {

/** Factory method name used to construct the type. Empty means use a constructor */
String factoryMethod() default "";

/**
* Import a type to be mapped.
* <p>
* The type needs to have a single constructor argument that is a String type,
* or have a factory method that has a single argument that is a String type.
*/
@Repeatable(MappedParam.Import.Imports.class)
@Retention(SOURCE)
@Target({TYPE, PACKAGE, MODULE})
@interface Import {

Class<?> value();

/** Factory method name used to construct the type. Empty means use a constructor */
String factoryMethod() default "";

@Retention(SOURCE)
@Target({TYPE, PACKAGE, MODULE})
@interface Imports {

Import[] value();
}
}
}
18 changes: 15 additions & 3 deletions http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ public static int asInt(String value) {
}
}

/** Convert to type (not nullable). */
public static <T> T asType(Function<String, T> typeConversion, String value) {
checkNull(value);
return typeConversion.apply(value);
}

/**
* Convert to enum.
*/
Expand Down Expand Up @@ -288,9 +294,15 @@ public static Integer asInteger(String value) {
}
}

/**
* Convert to enum of the given type.
*/
/** Convert to type */
public static <T> T toType(Function<String, T> typeConversion, String value) {
if (isNullOrEmpty(value)) {
return null;
}
return typeConversion.apply(value);
}

/** Convert to enum of the given type. */
@SuppressWarnings({"rawtypes"})
public static <T> Enum toEnum(Class<T> clazz, String value) {
if (isNullOrEmpty(value)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package io.avaje.http.generator.core;

import static io.avaje.http.generator.core.ProcessingContext.doc;
import static io.avaje.http.generator.core.ProcessingContext.elements;
import static io.avaje.http.generator.core.ProcessingContext.isOpenApiAvailable;
import static io.avaje.http.generator.core.ProcessingContext.logError;
import static io.avaje.http.generator.core.ProcessingContext.typeElement;
import static io.avaje.http.generator.core.ProcessingContext.*;
import static java.util.stream.Collectors.toMap;

import java.io.IOException;
Expand All @@ -14,6 +10,7 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
Expand All @@ -22,9 +19,12 @@
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;

import io.avaje.http.generator.core.TypeMap.CustomHandler;
import io.avaje.prism.GenerateAPContext;
import io.avaje.prism.GenerateModuleInfoReader;

Expand Down Expand Up @@ -54,7 +54,11 @@ public SourceVersion getSupportedSourceVersion() {
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(
PathPrism.PRISM_TYPE, ControllerPrism.PRISM_TYPE, OpenAPIDefinitionPrism.PRISM_TYPE);
PathPrism.PRISM_TYPE,
ControllerPrism.PRISM_TYPE,
OpenAPIDefinitionPrism.PRISM_TYPE,
MappedParamPrism.PRISM_TYPE,
MapImportPrism.PRISM_TYPE);
}

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

try {
var txtFilePath = APContext.getBuildResource(HTTP_CONTROLLERS_TXT);

if (txtFilePath.toFile().exists()) {
Files.lines(txtFilePath).forEach(clientFQNs::add);
}
Expand All @@ -75,8 +78,8 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
}
}
} catch (IOException e) {
e.printStackTrace();
// not worth failing over
logWarn("Error reading test controllers %s", e);
}
}

Expand All @@ -88,6 +91,17 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
if (round.errorRaised()) {
return false;
}

for (final var type : ElementFilter.typesIn(getElements(round, MappedParamPrism.PRISM_TYPE))) {
var prism = MappedParamPrism.getInstanceOn(type);
registerParamMapping(type, prism.factoryMethod());
}

for (final var type : getElements(round, MapImportPrism.PRISM_TYPE)) {
var prism = MapImportPrism.getInstanceOn(type);
registerParamMapping(APContext.asTypeElement(prism.value()), prism.factoryMethod());
}

var pathElements = round.getElementsAnnotatedWith(typeElement(PathPrism.PRISM_TYPE));
APContext.setProjectModuleElement(annotations, round);
if (contextPathString == null) {
Expand All @@ -111,9 +125,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
readSecuritySchemes(round);
}

for (final var controller :
ElementFilter.typesIn(
round.getElementsAnnotatedWith(typeElement(ControllerPrism.PRISM_TYPE)))) {
for (final var controller : ElementFilter.typesIn(round.getElementsAnnotatedWith(typeElement(ControllerPrism.PRISM_TYPE)))) {
writeAdapter(controller);
}

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

private Set<? extends Element> getElements(RoundEnvironment round, String name) {
return Optional.ofNullable(typeElement(name))
.map(round::getElementsAnnotatedWith)
.orElse(Set.of());
}

private final void registerParamMapping(final TypeElement type, String factoryMethod) {
if (factoryMethod.isBlank()) {
Util.stringConstructor(type)
.ifPresentOrElse(
c -> TypeMap.add(new CustomHandler(UType.parse(type.asType()), "")),
() -> logError(type, "Missing constructor %s(String s)"));

} else {
ElementFilter.methodsIn(type.getEnclosedElements()).stream()
.filter(m -> m.getSimpleName().contentEquals(factoryMethod)
&& m.getModifiers().contains(Modifier.STATIC)
&& Util.singleStringParam(m))
.findAny()
.ifPresentOrElse(
c -> TypeMap.add(new CustomHandler(UType.parse(type.asType()), factoryMethod)),
() -> logError(type, "Missing static factory method %s(String s)", factoryMethod));
}
}

private void readOpenApiDefinition(RoundEnvironment round) {
for (final Element element :
round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) {
for (final Element element : round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) {
doc().readApiDefinition(element);
}
}
Expand All @@ -147,19 +183,16 @@ private void readTagDefinitions(RoundEnvironment round) {
for (final Element element : round.getElementsAnnotatedWith(typeElement(TagPrism.PRISM_TYPE))) {
doc().addTagDefinition(element);
}
for (final Element element :
round.getElementsAnnotatedWith(typeElement(TagsPrism.PRISM_TYPE))) {
for (final Element element : round.getElementsAnnotatedWith(typeElement(TagsPrism.PRISM_TYPE))) {
doc().addTagsDefinition(element);
}
}

private void readSecuritySchemes(RoundEnvironment round) {
for (final Element element :
round.getElementsAnnotatedWith(typeElement(SecuritySchemePrism.PRISM_TYPE))) {
for (final Element element : round.getElementsAnnotatedWith(typeElement(SecuritySchemePrism.PRISM_TYPE))) {
doc().addSecurityScheme(element);
}
for (final Element element :
round.getElementsAnnotatedWith(typeElement(SecuritySchemesPrism.PRISM_TYPE))) {
for (final Element element : round.getElementsAnnotatedWith(typeElement(SecuritySchemesPrism.PRISM_TYPE))) {
doc().addSecuritySchemes(element);
}
}
Expand All @@ -174,7 +207,6 @@ private void writeAdapter(TypeElement controller) {
final var reader = new ControllerReader(controller, contextPath);
reader.read(true);
try {

writeControllerAdapter(reader);
writeClientAdapter(reader);

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

private void writeClientAdapter(ControllerReader reader) {

try {
if (reader.beanType().getInterfaces().isEmpty()
&& "java.lang.Object".equals(reader.beanType().getSuperclass().toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;

import io.avaje.http.generator.core.TypeMap.CustomHandler;
import io.avaje.http.generator.core.openapi.MethodDocBuilder;
import io.avaje.http.generator.core.openapi.MethodParamDocBuilder;

Expand Down Expand Up @@ -76,7 +77,9 @@ public class ElementReader {
if (!contextType) {
readAnnotations(element, defaultType);
useValidation = useValidation();
HttpValidPrism.getOptionalOn(element.getEnclosingElement()).map(HttpValidPrism::groups).stream()
HttpValidPrism.getOptionalOn(element.getEnclosingElement())
.map(HttpValidPrism::groups)
.stream()
.flatMap(List::stream)
.map(TypeMirror::toString)
.forEach(validationGroups::add);
Expand All @@ -101,37 +104,58 @@ private void beanParamImports(String rawType) {
}

TypeHandler initTypeHandler() {
if (specialParam) {
final var typeOp = Optional.ofNullable(type).or(() -> Optional.of(UType.parse(element.asType())));
var handler = TypeMap.get(rawType);
final var typeOp = Optional.ofNullable(type).or(() -> Optional.of(UType.parse(element.asType())));

var customType = typeOp.orElseThrow();
var actual = customType.isGeneric() ? UType.parse(customType.param0()) : customType;

if (handler == null) {
Optional.ofNullable(APContext.typeElement(customType.full()))
.flatMap(MappedParamPrism::getOptionalOn)
.ifPresent(p -> TypeMap.add(new CustomHandler(actual, p.factoryMethod())));

handler = TypeMap.get(rawType);
}

if (handler == null && ParamPrism.isPresent(element)) {
handler =
Optional.ofNullable(APContext.typeElement(customType.full()))
.flatMap(Util::stringConstructor)
.map(m -> new CustomHandler(actual, ""))
.orElse(null);
}

if (specialParam) {
final var mainTypeEnum =
typeOp
.flatMap(t -> Optional.ofNullable(typeElement(t.mainType())))
.map(TypeElement::getKind)
.filter(ElementKind.ENUM::equals)
.isPresent();
typeOp
.flatMap(t -> Optional.ofNullable(typeElement(t.mainType())))
.map(TypeElement::getKind)
.filter(ElementKind.ENUM::equals)
.isPresent();

final var isCollection =
typeOp
.filter(t -> t.isGeneric() && !t.mainType().startsWith("java.util.Map"))
.isPresent();
typeOp
.filter(t -> t.isGeneric() && !t.mainType().startsWith("java.util.Map"))
.isPresent();

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

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

if (mainTypeEnum) {
return TypeMap.enumParamHandler(typeOp.orElseThrow());
} else if (isCollection || isOptional) {
final var isEnumContainer =
typeOp
.flatMap(t -> Optional.ofNullable(typeElement(t.param0())))
.map(TypeElement::getKind)
.filter(ElementKind.ENUM::equals)
.isPresent();
typeOp
.flatMap(t -> Optional.ofNullable(typeElement(t.param0())))
.map(TypeElement::getKind)
.filter(ElementKind.ENUM::equals)
.isPresent();

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

return TypeMap.get(rawType);
return handler;
}

private boolean useValidation() {
Expand Down Expand Up @@ -312,7 +336,7 @@ void writeValidate(Append writer) {
}

void writeCtxGet(Append writer, PathSegments segments) {
if (isPlatformContext() || (paramType == ParamType.BODY && platform().isBodyMethodParam())) {
if (isPlatformContext() || paramType == ParamType.BODY && platform().isBodyMethodParam()) {
// body passed as method parameter (Helidon)
return;
}
Expand Down Expand Up @@ -347,9 +371,9 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
// path or matrix parameter
final boolean requiredParam = segment.isRequired(varName);
final String asMethod =
(typeHandler == null)
? null
: (requiredParam) ? typeHandler.asMethod() : typeHandler.toMethod();
typeHandler == null
? null
: requiredParam ? typeHandler.asMethod() : typeHandler.toMethod();
if (asMethod != null) {
writer.append(asMethod);
}
Expand All @@ -362,7 +386,7 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
}
}

final String asMethod = (typeHandler == null) ? null : typeHandler.toMethod();
final String asMethod = typeHandler == null ? null : typeHandler.toMethod();
if (asMethod != null) {
writer.append(asMethod);
}
Expand All @@ -382,7 +406,8 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
} else if (hasParamDefault()) {
platform().writeReadParameter(writer, paramType, paramName, paramDefault.get(0));
} else {
final var checkNull = notNullKotlin || (paramType == ParamType.FORMPARAM && typeHandler.isPrimitive());
final var checkNull =
notNullKotlin || paramType == ParamType.FORMPARAM && typeHandler.isPrimitive();
if (checkNull) {
writer.append("checkNull(");
}
Expand Down
Loading