From 28c1be546ff186745a9f84e8c6a28dbb0989f37e Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Wed, 24 Sep 2025 23:15:34 -0400 Subject: [PATCH 1/6] support custom type mapping --- .../java/io/avaje/http/api/MappedParam.java | 30 +++++++++ .../io/avaje/http/api/PathTypeConversion.java | 18 +++++- .../java/io/avaje/http/api/PathVariable.java | 21 +++++++ .../http/generator/core/BaseProcessor.java | 54 +++++++++++++++- .../http/generator/core/ElementReader.java | 62 +++++++++++++++---- .../avaje/http/generator/core/ParamPrism.java | 46 ++++++++++++++ .../io/avaje/http/generator/core/TypeMap.java | 42 ++++++++++--- .../io/avaje/http/generator/core/Util.java | 28 +++++++-- .../http/generator/core/package-info.java | 33 ++++++---- .../main/java/org/example/TestController.java | 47 +++++++++++++- 10 files changed, 340 insertions(+), 41 deletions(-) create mode 100644 http-api/src/main/java/io/avaje/http/api/MappedParam.java create mode 100644 http-api/src/main/java/io/avaje/http/api/PathVariable.java create mode 100644 http-generator-core/src/main/java/io/avaje/http/generator/core/ParamPrism.java diff --git a/http-api/src/main/java/io/avaje/http/api/MappedParam.java b/http-api/src/main/java/io/avaje/http/api/MappedParam.java new file mode 100644 index 000000000..e8d16793e --- /dev/null +++ b/http-api/src/main/java/io/avaje/http/api/MappedParam.java @@ -0,0 +1,30 @@ +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.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Marks a type to be mapped. */ +@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 ""; + + @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 ""; + } +} diff --git a/http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java b/http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java index 55706c0b1..4138e7383 100644 --- a/http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java +++ b/http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java @@ -98,6 +98,12 @@ public static int asInt(String value) { } } + /** Convert to type. */ + public static T asType(Function typeConversion, String value) { + checkNull(value); + return typeConversion.apply(value); + } + /** * Convert to enum. */ @@ -288,9 +294,15 @@ public static Integer asInteger(String value) { } } - /** - * Convert to enum of the given type. - */ + /** Convert to type (not nullable) */ + public static T toType(Function typeConversion, String value) { + if (isNullOrEmpty(value)) { + return null; + } + return typeConversion.apply(value); + } + + /** Convert to enum of the given type. */ @SuppressWarnings({"rawtypes"}) public static Enum toEnum(Class clazz, String value) { if (isNullOrEmpty(value)) { diff --git a/http-api/src/main/java/io/avaje/http/api/PathVariable.java b/http-api/src/main/java/io/avaje/http/api/PathVariable.java new file mode 100644 index 000000000..454885001 --- /dev/null +++ b/http-api/src/main/java/io/avaje/http/api/PathVariable.java @@ -0,0 +1,21 @@ +package io.avaje.http.api; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** Marks a method parameter to be a path variable. */ +@Retention(RUNTIME) +@Target({PARAMETER, FIELD}) +public @interface PathVariable { + + /** + * The name of the path variable. + * + *

If left blank the method parameter name is used. + */ + String value(); +} diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java index 8ea2f1872..acb7ca914 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java @@ -14,6 +14,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; @@ -22,9 +23,11 @@ import javax.annotation.processing.SupportedOptions; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; +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; @@ -54,7 +57,11 @@ public SourceVersion getSupportedSourceVersion() { @Override public Set 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 @@ -88,6 +95,20 @@ public boolean process(Set 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) { @@ -136,6 +157,37 @@ public boolean process(Set annotations, RoundEnvironment return false; } + private Set 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) + && m.getParameters().size() == 1 + && m.getParameters() + .get(0) + .asType() + .toString() + .equals(String.class.getTypeName())) + .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))) { diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java index d030763d1..c91f58e16 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java @@ -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; @@ -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); @@ -101,8 +104,33 @@ private void beanParamImports(String rawType) { } TypeHandler initTypeHandler() { + + 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 typeOp = Optional.ofNullable(type).or(() -> Optional.of(UType.parse(element.asType()))); final var mainTypeEnum = typeOp @@ -119,7 +147,8 @@ TypeHandler initTypeHandler() { final var isMap = !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()); @@ -142,7 +171,7 @@ TypeHandler initTypeHandler() { } } - return TypeMap.get(rawType); + return handler; } private boolean useValidation() { @@ -195,6 +224,14 @@ private void readAnnotations(Element element, ParamType defaultType) { return; } + final var pathVar = PathVariablePrism.getInstanceOn(element); + if (pathVar != null) { + this.paramName = nameFrom(pathVar.value(), Util.initcapSnake(snakeName)); + this.paramType = ParamType.PATHPARAM; + this.paramDefault = null; + return; + } + final var matrixParam = MatrixParamPrism.getInstanceOn(element); if (matrixParam != null) { this.matrixParamName = nameFrom(matrixParam.value(), varName); @@ -312,7 +349,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; } @@ -347,9 +384,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) + typeHandler == null ? null - : (requiredParam) ? typeHandler.asMethod() : typeHandler.toMethod(); + : requiredParam ? typeHandler.asMethod() : typeHandler.toMethod(); if (asMethod != null) { writer.append(asMethod); } @@ -362,7 +399,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); } @@ -382,7 +419,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("); } @@ -398,9 +436,11 @@ private boolean setValue(Append writer, PathSegments segments, String shortType) return true; } - private void writeForm(Append writer, String shortType, String varName, ParamType defaultParamType) { + private void writeForm( + Append writer, String shortType, String varName, ParamType defaultParamType) { final TypeElement formBeanType = typeElement(rawType); - final BeanParamReader form = new BeanParamReader(formBeanType, varName, shortType, defaultParamType); + final BeanParamReader form = + new BeanParamReader(formBeanType, varName, shortType, defaultParamType); form.write(writer); } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ParamPrism.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ParamPrism.java new file mode 100644 index 000000000..9cc30bf43 --- /dev/null +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ParamPrism.java @@ -0,0 +1,46 @@ +package io.avaje.http.generator.core; + +import java.util.Optional; + +import javax.lang.model.element.Element; + +import io.avaje.prism.GeneratePrism; + +@GeneratePrism( + value = io.avaje.http.api.PathVariable.class, + publicAccess = true, + superInterfaces = ParamPrism.class) +@GeneratePrism( + value = io.avaje.http.api.QueryParam.class, + publicAccess = true, + superInterfaces = ParamPrism.class) +@GeneratePrism( + value = io.avaje.http.api.Cookie.class, + publicAccess = true, + superInterfaces = ParamPrism.class) +@GeneratePrism( + value = io.avaje.http.api.FormParam.class, + publicAccess = true, + superInterfaces = ParamPrism.class) +@GeneratePrism( + value = io.avaje.http.api.Header.class, + publicAccess = true, + superInterfaces = ParamPrism.class) +@GeneratePrism( + value = io.avaje.http.api.MatrixParam.class, + publicAccess = true, + superInterfaces = ParamPrism.class) +public interface ParamPrism { + + static boolean isPresent(Element e) { + return Optional.empty() + .or(() -> PathVariablePrism.getOptionalOn(e)) + .or(() -> QueryParamPrism.getOptionalOn(e)) + .or(() -> CookiePrism.getOptionalOn(e)) + .or(() -> FormParamPrism.getOptionalOn(e)) + .or(() -> HeaderPrism.getOptionalOn(e)) + .or(() -> MatrixParamPrism.getOptionalOn(e)) + .isPresent(); + } + +} diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java index 8b6452f5b..2c295c69b 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java @@ -14,7 +14,7 @@ final class TypeMap { private static final Map types = new HashMap<>(); - private static void add(TypeHandler h) { + static void add(TypeHandler h) { types.put(h.importTypes().get(0), h); } @@ -342,13 +342,12 @@ private CollectionHandler(TypeHandler handler, boolean set, boolean isEnum) { this.importTypes.add("io.avaje.http.api.PathTypeConversion"); this.shortName = handler.shortName(); String _toMethod = - (set ? "set" : "list") - + "(" - + (isEnum - ? "qp -> " + handler.toMethod() + " qp)" - : "PathTypeConversion::as" + shortName) - + ", "; - + (set ? "set" : "list") + + "(" + + (isEnum || handler instanceof CustomHandler + ? "qp -> " + handler.toMethod() + " qp)" + : "PathTypeConversion::as" + shortName) + + ", "; this.toMethod = _toMethod.replace("PathTypeConversion::asString", "Object::toString"); } @@ -397,7 +396,7 @@ private OptionalHandler(TypeHandler handler, boolean isEnum) { } static String buildToMethod(TypeHandler handler, boolean isEnum) { - if (isEnum) { + if (isEnum || handler instanceof CustomHandler) { return "optional(qp -> " + handler.toMethod() + " qp), "; } if ("String".equals(handler.shortName())) { @@ -433,6 +432,31 @@ public String toMethod() { } } + static final class CustomHandler extends ObjectHandler { + private final UType type; + private final String factory; + + CustomHandler(UType type, String factory) { + super(type.mainType(), type.shortName()); + this.type = type; + this.factory = factory; + } + + @Override + public String toMethod() { + return "toType(" + + type.shortTypeNested() + + "::" + + (factory.isBlank() ? "new" : factory) + + ", "; + } + + @Override + public String asMethod() { + return "asType(" + type.shortTypeNested() + "::" + (factory.isBlank() ? "new" : factory); + } + } + static abstract class ObjectHandler implements TypeHandler { private final String importType; diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java index 3a6a79b63..f7109de41 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java @@ -1,18 +1,23 @@ package io.avaje.http.generator.core; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; import javax.lang.model.util.SimpleAnnotationValueVisitor8; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class Util { // whitespace not in quotes @@ -266,4 +271,17 @@ public List visitEnumConstant(VariableElement roleEnum, Object o) { return fullRoles; } } + + static Optional stringConstructor(TypeElement typeElement) { + return ElementFilter.constructorsIn(typeElement.getEnclosedElements()).stream() + .filter( + m -> + m.getParameters().size() == 1 + && m.getParameters() + .get(0) + .asType() + .toString() + .equals(String.class.getTypeName())) + .findAny(); + } } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java index 50ff9161b..549958b5e 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java @@ -1,18 +1,19 @@ /** Generate the prisms to access annotation info */ -@GeneratePrism(value = io.avaje.http.api.Controller.class, publicAccess = true, superInterfaces = WebAPIPrism.class) -@GeneratePrism(value = io.avaje.http.api.Client.class, publicAccess = true, superInterfaces = WebAPIPrism.class) +@GeneratePrism( + value = io.avaje.http.api.Controller.class, + publicAccess = true, + superInterfaces = WebAPIPrism.class) +@GeneratePrism( + value = io.avaje.http.api.Client.class, + publicAccess = true, + superInterfaces = WebAPIPrism.class) @GeneratePrism(value = io.avaje.http.api.BeanParam.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Ignore.class, publicAccess = true) -@GeneratePrism(value = io.avaje.http.api.QueryParam.class, publicAccess = true) -@GeneratePrism(value = io.avaje.http.api.Cookie.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.BodyString.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Default.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Delete.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Form.class, publicAccess = true) -@GeneratePrism(value = io.avaje.http.api.FormParam.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Get.class, publicAccess = true) -@GeneratePrism(value = io.avaje.http.api.Header.class, publicAccess = true) -@GeneratePrism(value = io.avaje.http.api.MatrixParam.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Patch.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Path.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Post.class, publicAccess = true) @@ -22,14 +23,24 @@ @GeneratePrism(value = io.avaje.http.api.Filter.class) @GeneratePrism(value = io.avaje.http.api.InstrumentServerContext.class) @GeneratePrism(value = io.avaje.http.api.ExceptionHandler.class) +@GeneratePrism(value = io.avaje.http.api.MappedParam.class) +@GeneratePrism(value = io.avaje.http.api.MappedParam.Import.class, name = "MapImportPrism") @GeneratePrism(value = io.swagger.v3.oas.annotations.OpenAPIDefinition.class, publicAccess = true) @GeneratePrism(value = io.swagger.v3.oas.annotations.Operation.class, publicAccess = true) @GeneratePrism(value = io.swagger.v3.oas.annotations.tags.Tag.class, publicAccess = true) @GeneratePrism(value = io.swagger.v3.oas.annotations.tags.Tags.class, publicAccess = true) -@GeneratePrism(value = io.swagger.v3.oas.annotations.security.SecurityScheme.class, publicAccess = true) -@GeneratePrism(value = io.swagger.v3.oas.annotations.security.SecuritySchemes.class, publicAccess = true) -@GeneratePrism(value = io.swagger.v3.oas.annotations.security.SecurityRequirement.class, publicAccess = true) -@GeneratePrism(value = io.swagger.v3.oas.annotations.security.SecurityRequirements.class, publicAccess = true) +@GeneratePrism( + value = io.swagger.v3.oas.annotations.security.SecurityScheme.class, + publicAccess = true) +@GeneratePrism( + value = io.swagger.v3.oas.annotations.security.SecuritySchemes.class, + publicAccess = true) +@GeneratePrism( + value = io.swagger.v3.oas.annotations.security.SecurityRequirement.class, + publicAccess = true) +@GeneratePrism( + value = io.swagger.v3.oas.annotations.security.SecurityRequirements.class, + publicAccess = true) @GeneratePrism(value = io.avaje.http.api.OpenAPIResponse.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.OpenAPIResponses.class, publicAccess = true) @GeneratePrism(value = io.swagger.v3.oas.annotations.Hidden.class, publicAccess = true) diff --git a/tests/test-nima-jsonb/src/main/java/org/example/TestController.java b/tests/test-nima-jsonb/src/main/java/org/example/TestController.java index bbb0db331..9c6611ab7 100644 --- a/tests/test-nima-jsonb/src/main/java/org/example/TestController.java +++ b/tests/test-nima-jsonb/src/main/java/org/example/TestController.java @@ -1,7 +1,10 @@ package org.example; import java.io.InputStream; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import io.avaje.http.api.BodyString; import io.avaje.http.api.Controller; @@ -12,6 +15,7 @@ import io.avaje.http.api.FormParam; import io.avaje.http.api.Get; import io.avaje.http.api.InstrumentServerContext; +import io.avaje.http.api.MappedParam; import io.avaje.http.api.Path; import io.avaje.http.api.Post; import io.avaje.http.api.Produces; @@ -162,4 +166,45 @@ Person maybePerson(boolean maybe) { List maybePersonList(boolean maybe) { return maybe ? List.of(new Person(9, "hi")) : null; // Collections.emptyList(); } + + @MappedParam + @MappedParam.Import(Simple2.class) + record Simple(String name) {} + + record Simple2(String name) {} + + @Form + @Get("/typeForm") + String typeForm(Simple s, Simple2 type) { + return type.name(); + } + + @MappedParam(factoryMethod = "build") + record Static(String name) { + static Static build(String name) { + return null; + } + } + + @Get("/typeFormParam") + String typeFormParam(@FormParam String s, @FormParam Static type) { + return type.name(); + } + + @Get("/typeQuery") + String typeQuery(@QueryParam @Default("FFA") Static type) { + return type.name(); + } + + @Get("/typeQuery2") + String typeMultiQuery(@QueryParam @Default({"FFA", "PROXY"}) Set type) { + return type.toString(); + } + + record Implied(String name) {} + + @Post("/typeQueryImplied") + String typeQueryImplied(String s, @QueryParam Implied type) { + return type.name(); + } } From 9049d4ca802e12cfd11b55bd19a4d19734d0b341 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 6 Oct 2025 17:06:13 +1300 Subject: [PATCH 2/6] Remove @PathVariable as it is not required yet --- .../java/io/avaje/http/api/PathVariable.java | 21 ------------------- .../http/generator/core/ElementReader.java | 8 ------- .../avaje/http/generator/core/ParamPrism.java | 5 ----- 3 files changed, 34 deletions(-) delete mode 100644 http-api/src/main/java/io/avaje/http/api/PathVariable.java diff --git a/http-api/src/main/java/io/avaje/http/api/PathVariable.java b/http-api/src/main/java/io/avaje/http/api/PathVariable.java deleted file mode 100644 index 454885001..000000000 --- a/http-api/src/main/java/io/avaje/http/api/PathVariable.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.avaje.http.api; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** Marks a method parameter to be a path variable. */ -@Retention(RUNTIME) -@Target({PARAMETER, FIELD}) -public @interface PathVariable { - - /** - * The name of the path variable. - * - *

If left blank the method parameter name is used. - */ - String value(); -} diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java index c91f58e16..f290fdda4 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java @@ -224,14 +224,6 @@ private void readAnnotations(Element element, ParamType defaultType) { return; } - final var pathVar = PathVariablePrism.getInstanceOn(element); - if (pathVar != null) { - this.paramName = nameFrom(pathVar.value(), Util.initcapSnake(snakeName)); - this.paramType = ParamType.PATHPARAM; - this.paramDefault = null; - return; - } - final var matrixParam = MatrixParamPrism.getInstanceOn(element); if (matrixParam != null) { this.matrixParamName = nameFrom(matrixParam.value(), varName); diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ParamPrism.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ParamPrism.java index 9cc30bf43..29a433f86 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ParamPrism.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ParamPrism.java @@ -6,10 +6,6 @@ import io.avaje.prism.GeneratePrism; -@GeneratePrism( - value = io.avaje.http.api.PathVariable.class, - publicAccess = true, - superInterfaces = ParamPrism.class) @GeneratePrism( value = io.avaje.http.api.QueryParam.class, publicAccess = true, @@ -34,7 +30,6 @@ public interface ParamPrism { static boolean isPresent(Element e) { return Optional.empty() - .or(() -> PathVariablePrism.getOptionalOn(e)) .or(() -> QueryParamPrism.getOptionalOn(e)) .or(() -> CookiePrism.getOptionalOn(e)) .or(() -> FormParamPrism.getOptionalOn(e)) From a7b8f36840b128b62ceddd89c24f0f6e7a24c5d7 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 6 Oct 2025 17:10:07 +1300 Subject: [PATCH 3/6] Adjust javadoc only - asType() is the (not nullable) one. --- .../src/main/java/io/avaje/http/api/PathTypeConversion.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java b/http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java index 4138e7383..7db576a04 100644 --- a/http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java +++ b/http-api/src/main/java/io/avaje/http/api/PathTypeConversion.java @@ -98,7 +98,7 @@ public static int asInt(String value) { } } - /** Convert to type. */ + /** Convert to type (not nullable). */ public static T asType(Function typeConversion, String value) { checkNull(value); return typeConversion.apply(value); @@ -294,7 +294,7 @@ public static Integer asInteger(String value) { } } - /** Convert to type (not nullable) */ + /** Convert to type */ public static T toType(Function typeConversion, String value) { if (isNullOrEmpty(value)) { return null; From 49ce9b4060a36de3093cc7a404abe7ac9a885a78 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Mon, 6 Oct 2025 00:39:13 -0400 Subject: [PATCH 4/6] Update MappedParam.java --- .../src/main/java/io/avaje/http/api/MappedParam.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/http-api/src/main/java/io/avaje/http/api/MappedParam.java b/http-api/src/main/java/io/avaje/http/api/MappedParam.java index e8d16793e..4a16d7e08 100644 --- a/http-api/src/main/java/io/avaje/http/api/MappedParam.java +++ b/http-api/src/main/java/io/avaje/http/api/MappedParam.java @@ -6,6 +6,7 @@ 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; @@ -18,6 +19,7 @@ /** Factory method name used to construct the type. Empty means use a constructor */ String factoryMethod() default ""; + @Repeatable(MappedParam.Import.Imports.class) @Retention(SOURCE) @Target({TYPE, PACKAGE, MODULE}) @interface Import { @@ -26,5 +28,12 @@ /** 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(); + } } } From cdb55022f2ac069782674c21f76350372d84203b Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 6 Oct 2025 21:29:34 +1300 Subject: [PATCH 5/6] Javadoc, format, extract methods --- .../java/io/avaje/http/api/MappedParam.java | 13 ++- .../http/generator/core/BaseProcessor.java | 84 ++++++++----------- .../http/generator/core/ElementReader.java | 63 +++++++------- .../io/avaje/http/generator/core/TypeMap.java | 9 +- .../io/avaje/http/generator/core/Util.java | 23 ++--- .../http/generator/core/package-info.java | 30 ++----- .../main/java/org/example/TestController.java | 2 +- 7 files changed, 104 insertions(+), 120 deletions(-) diff --git a/http-api/src/main/java/io/avaje/http/api/MappedParam.java b/http-api/src/main/java/io/avaje/http/api/MappedParam.java index 4a16d7e08..023eb2a7c 100644 --- a/http-api/src/main/java/io/avaje/http/api/MappedParam.java +++ b/http-api/src/main/java/io/avaje/http/api/MappedParam.java @@ -11,7 +11,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** Marks a type to be mapped. */ +/** + * Marks a type to be mapped. + *

+ * 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 { @@ -19,6 +24,12 @@ /** Factory method name used to construct the type. Empty means use a constructor */ String factoryMethod() default ""; + /** + * Import a type to be mapped. + *

+ * 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}) diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java index acb7ca914..d576f7ffa 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java @@ -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; @@ -23,6 +19,7 @@ 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; @@ -57,11 +54,11 @@ public SourceVersion getSupportedSourceVersion() { @Override public Set getSupportedAnnotationTypes() { return Set.of( - PathPrism.PRISM_TYPE, - ControllerPrism.PRISM_TYPE, - OpenAPIDefinitionPrism.PRISM_TYPE, - MappedParamPrism.PRISM_TYPE, - MapImportPrism.PRISM_TYPE); + PathPrism.PRISM_TYPE, + ControllerPrism.PRISM_TYPE, + OpenAPIDefinitionPrism.PRISM_TYPE, + MappedParamPrism.PRISM_TYPE, + MapImportPrism.PRISM_TYPE); } @Override @@ -72,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); } @@ -82,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); } } @@ -98,14 +94,11 @@ public boolean process(Set annotations, RoundEnvironment 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()); } @@ -132,9 +125,7 @@ public boolean process(Set 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); } @@ -159,38 +150,40 @@ public boolean process(Set annotations, RoundEnvironment private Set getElements(RoundEnvironment round, String name) { return Optional.ofNullable(typeElement(name)) - .map(round::getElementsAnnotatedWith) - .orElse(Set.of()); + .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)")); + .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) - && m.getParameters().size() == 1 - && m.getParameters() - .get(0) - .asType() - .toString() - .equals(String.class.getTypeName())) - .findAny() - .ifPresentOrElse( - c -> TypeMap.add(new CustomHandler(UType.parse(type.asType()), factoryMethod)), - () -> logError(type, "Missing static factory method %s(String s)", factoryMethod)); - }} + .filter(m -> m.getSimpleName().contentEquals(factoryMethod) + && m.getModifiers().contains(Modifier.STATIC) + && m.getParameters().size() == 1 + && firstParamIsString(m)) + .findAny() + .ifPresentOrElse( + c -> TypeMap.add(new CustomHandler(UType.parse(type.asType()), factoryMethod)), + () -> logError(type, "Missing static factory method %s(String s)", factoryMethod)); + } + } + + private static boolean firstParamIsString(ExecutableElement m) { + return m.getParameters() + .get(0) + .asType() + .toString() + .equals(String.class.getTypeName()); + } 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); } } @@ -199,19 +192,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); } } @@ -226,7 +216,6 @@ private void writeAdapter(TypeElement controller) { final var reader = new ControllerReader(controller, contextPath); reader.read(true); try { - writeControllerAdapter(reader); writeClientAdapter(reader); @@ -236,7 +225,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()) diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java index f290fdda4..1c322b074 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ElementReader.java @@ -104,63 +104,58 @@ private void beanParamImports(String rawType) { } TypeHandler initTypeHandler() { - var handler = TypeMap.get(rawType); - - final var typeOp = - Optional.ofNullable(type).or(() -> Optional.of(UType.parse(element.asType()))); + 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()))); + .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); + 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(); + 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; @@ -376,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); } @@ -428,11 +423,9 @@ private boolean setValue(Append writer, PathSegments segments, String shortType) return true; } - private void writeForm( - Append writer, String shortType, String varName, ParamType defaultParamType) { + private void writeForm(Append writer, String shortType, String varName, ParamType defaultParamType) { final TypeElement formBeanType = typeElement(rawType); - final BeanParamReader form = - new BeanParamReader(formBeanType, varName, shortType, defaultParamType); + final BeanParamReader form = new BeanParamReader(formBeanType, varName, shortType, defaultParamType); form.write(writer); } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java index 2c295c69b..a7741f2f7 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java @@ -433,6 +433,7 @@ public String toMethod() { } static final class CustomHandler extends ObjectHandler { + private final UType type; private final String factory; @@ -445,10 +446,10 @@ static final class CustomHandler extends ObjectHandler { @Override public String toMethod() { return "toType(" - + type.shortTypeNested() - + "::" - + (factory.isBlank() ? "new" : factory) - + ", "; + + type.shortTypeNested() + + "::" + + (factory.isBlank() ? "new" : factory) + + ", "; } @Override diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java index f7109de41..402eceb54 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java @@ -19,7 +19,7 @@ import javax.lang.model.util.ElementFilter; import javax.lang.model.util.SimpleAnnotationValueVisitor8; -public class Util { +public final class Util { // whitespace not in quotes private static final Pattern WHITE_SPACE_REGEX = Pattern.compile("\\s+(?=([^\"]*\"[^\"]*\")*[^\"]*$)"); @@ -274,14 +274,17 @@ public List visitEnumConstant(VariableElement roleEnum, Object o) { static Optional stringConstructor(TypeElement typeElement) { return ElementFilter.constructorsIn(typeElement.getEnclosedElements()).stream() - .filter( - m -> - m.getParameters().size() == 1 - && m.getParameters() - .get(0) - .asType() - .toString() - .equals(String.class.getTypeName())) - .findAny(); + .filter(m -> + m.getParameters().size() == 1 + && firstParamIsString(m)) + .findAny(); + } + + private static boolean firstParamIsString(ExecutableElement m) { + return m.getParameters() + .get(0) + .asType() + .toString() + .equals(String.class.getTypeName()); } } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java index 549958b5e..e71d98d9a 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java @@ -1,12 +1,8 @@ -/** Generate the prisms to access annotation info */ -@GeneratePrism( - value = io.avaje.http.api.Controller.class, - publicAccess = true, - superInterfaces = WebAPIPrism.class) -@GeneratePrism( - value = io.avaje.http.api.Client.class, - publicAccess = true, - superInterfaces = WebAPIPrism.class) +/** + * Generate the prisms to access annotation info + */ +@GeneratePrism(value = io.avaje.http.api.Controller.class, publicAccess = true, superInterfaces = WebAPIPrism.class) +@GeneratePrism(value = io.avaje.http.api.Client.class, publicAccess = true, superInterfaces = WebAPIPrism.class) @GeneratePrism(value = io.avaje.http.api.BeanParam.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Ignore.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.BodyString.class, publicAccess = true) @@ -29,18 +25,10 @@ @GeneratePrism(value = io.swagger.v3.oas.annotations.Operation.class, publicAccess = true) @GeneratePrism(value = io.swagger.v3.oas.annotations.tags.Tag.class, publicAccess = true) @GeneratePrism(value = io.swagger.v3.oas.annotations.tags.Tags.class, publicAccess = true) -@GeneratePrism( - value = io.swagger.v3.oas.annotations.security.SecurityScheme.class, - publicAccess = true) -@GeneratePrism( - value = io.swagger.v3.oas.annotations.security.SecuritySchemes.class, - publicAccess = true) -@GeneratePrism( - value = io.swagger.v3.oas.annotations.security.SecurityRequirement.class, - publicAccess = true) -@GeneratePrism( - value = io.swagger.v3.oas.annotations.security.SecurityRequirements.class, - publicAccess = true) +@GeneratePrism(value = io.swagger.v3.oas.annotations.security.SecurityScheme.class, publicAccess = true) +@GeneratePrism(value = io.swagger.v3.oas.annotations.security.SecuritySchemes.class, publicAccess = true) +@GeneratePrism(value = io.swagger.v3.oas.annotations.security.SecurityRequirement.class, publicAccess = true) +@GeneratePrism(value = io.swagger.v3.oas.annotations.security.SecurityRequirements.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.OpenAPIResponse.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.OpenAPIResponses.class, publicAccess = true) @GeneratePrism(value = io.swagger.v3.oas.annotations.Hidden.class, publicAccess = true) diff --git a/tests/test-nima-jsonb/src/main/java/org/example/TestController.java b/tests/test-nima-jsonb/src/main/java/org/example/TestController.java index 9c6611ab7..afcd656b6 100644 --- a/tests/test-nima-jsonb/src/main/java/org/example/TestController.java +++ b/tests/test-nima-jsonb/src/main/java/org/example/TestController.java @@ -182,7 +182,7 @@ String typeForm(Simple s, Simple2 type) { @MappedParam(factoryMethod = "build") record Static(String name) { static Static build(String name) { - return null; + return new Static(name); } } From 18464cf08508840f892b37bed6ad97fbb4ad42f2 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Mon, 6 Oct 2025 21:36:21 +1300 Subject: [PATCH 6/6] Add Util.singleStringParam() helper method --- .../io/avaje/http/generator/core/BaseProcessor.java | 11 +---------- .../main/java/io/avaje/http/generator/core/Util.java | 10 ++++++---- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java index d576f7ffa..3cb1e3472 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java @@ -165,8 +165,7 @@ private final void registerParamMapping(final TypeElement type, String factoryMe ElementFilter.methodsIn(type.getEnclosedElements()).stream() .filter(m -> m.getSimpleName().contentEquals(factoryMethod) && m.getModifiers().contains(Modifier.STATIC) - && m.getParameters().size() == 1 - && firstParamIsString(m)) + && Util.singleStringParam(m)) .findAny() .ifPresentOrElse( c -> TypeMap.add(new CustomHandler(UType.parse(type.asType()), factoryMethod)), @@ -174,14 +173,6 @@ && firstParamIsString(m)) } } - private static boolean firstParamIsString(ExecutableElement m) { - return m.getParameters() - .get(0) - .asType() - .toString() - .equals(String.class.getTypeName()); - } - private void readOpenApiDefinition(RoundEnvironment round) { for (final Element element : round.getElementsAnnotatedWith(typeElement(OpenAPIDefinitionPrism.PRISM_TYPE))) { doc().readApiDefinition(element); diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java index 402eceb54..8ba9204a1 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/Util.java @@ -274,13 +274,15 @@ public List visitEnumConstant(VariableElement roleEnum, Object o) { static Optional stringConstructor(TypeElement typeElement) { return ElementFilter.constructorsIn(typeElement.getEnclosedElements()).stream() - .filter(m -> - m.getParameters().size() == 1 - && firstParamIsString(m)) + .filter(Util::singleStringParam) .findAny(); } - private static boolean firstParamIsString(ExecutableElement m) { + static boolean singleStringParam(ExecutableElement m) { + return m.getParameters().size() == 1 && firstParamIsString(m); + } + + static boolean firstParamIsString(ExecutableElement m) { return m.getParameters() .get(0) .asType()