diff --git a/typescript-generator-core/pom.xml b/typescript-generator-core/pom.xml index 72ee81237..a0e49f622 100644 --- a/typescript-generator-core/pom.xml +++ b/typescript-generator-core/pom.xml @@ -59,6 +59,11 @@ javax.ws.rs-api 2.1.1 + + jakarta.ws.rs + jakarta.ws.rs-api + 3.0.0 + io.github.classgraph classgraph diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JakartaRsApplicationScanner.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JakartaRsApplicationScanner.java new file mode 100644 index 000000000..0f44be32d --- /dev/null +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JakartaRsApplicationScanner.java @@ -0,0 +1,74 @@ + +package cz.habarta.typescript.generator; + +import cz.habarta.typescript.generator.parser.SourceType; +import io.github.classgraph.ScanResult; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Application; +import java.lang.reflect.Constructor; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + + +public class JakartaRsApplicationScanner { + + public static List> scanJakartaRsApplication(Class jaxrsApplicationClass, Predicate isClassNameExcluded) { + final ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(jaxrsApplicationClass.getClassLoader()); + TypeScriptGenerator.getLogger().info("Scanning JAX-RS application: " + jaxrsApplicationClass.getName()); + final Constructor constructor = jaxrsApplicationClass.getDeclaredConstructor(); + constructor.setAccessible(true); + final Application application = (Application) constructor.newInstance(); + final List> resourceClasses = new ArrayList<>(); + for (Class cls : application.getClasses()) { + if (cls.isAnnotationPresent(Path.class)) { + resourceClasses.add(cls); + } + } + return new JakartaRsApplicationScanner().scanJakartaRsApplication(jaxrsApplicationClass, resourceClasses, isClassNameExcluded); + } catch (ReflectiveOperationException e) { + throw reportError(e); + } finally { + Thread.currentThread().setContextClassLoader(originalContextClassLoader); + } + } + + public static List> scanAutomaticJakartaRsApplication(ScanResult scanResult, Predicate isClassNameExcluded) { + final List namesOfResourceClasses = scanResult.getClassesWithAnnotation(Path.class.getName()).getNames(); + final List> resourceClasses = Input.loadClasses(namesOfResourceClasses); + TypeScriptGenerator.getLogger().info(String.format("Found %d root resources.", resourceClasses.size())); + return new JakartaRsApplicationScanner().scanJakartaRsApplication(null, resourceClasses, isClassNameExcluded); + } + + private static RuntimeException reportError(ReflectiveOperationException e) { + final String url = "https://github.com/vojtechhabarta/typescript-generator/wiki/JAX-RS-Application"; + final String message = "Cannot load JAX-RS application. For more information see " + url + "."; + TypeScriptGenerator.getLogger().error(message); + return new RuntimeException(message, e); + } + + List> scanJakartaRsApplication(Class applicationClass, List> resourceClasses, Predicate isClassNameExcluded) { + Collections.sort(resourceClasses, new Comparator>() { + @Override + public int compare(Class o1, Class o2) { + return o1.getName().compareToIgnoreCase(o2.getName()); + } + }); + final List> sourceTypes = new ArrayList<>(); + if (applicationClass != null) { + sourceTypes.add(new SourceType(applicationClass)); + } + for (Class resourceClass : resourceClasses) { + if (isClassNameExcluded == null || !isClassNameExcluded.test(resourceClass.getName())) { + sourceTypes.add(new SourceType(resourceClass)); + } + } + return sourceTypes; + } + +} diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java index 8abe8e53f..d8deeebd4 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java @@ -8,6 +8,7 @@ import cz.habarta.typescript.generator.compiler.SymbolTable.CustomTypeNamingFunction; import cz.habarta.typescript.generator.emitter.EmitterExtension; import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures; +import cz.habarta.typescript.generator.parser.JakartaRsApplicationParser; import cz.habarta.typescript.generator.parser.JaxrsApplicationParser; import cz.habarta.typescript.generator.parser.RestApplicationParser; import cz.habarta.typescript.generator.parser.TypeParser; @@ -94,6 +95,8 @@ public class Settings { public boolean disableTaggedUnions = false; public boolean generateReadonlyAndWriteonlyJSDocTags = false; public boolean ignoreSwaggerAnnotations = false; + public boolean generateJakartaRsApplicationInterface = false; + public boolean generateJakartaRsApplicationClient; public boolean generateJaxrsApplicationInterface = false; public boolean generateJaxrsApplicationClient = false; public boolean generateSpringApplicationInterface = false; @@ -408,6 +411,10 @@ public void validate() { annotation.getName())); } } + + if (generateJakartaRsApplicationClient && outputFileType != TypeScriptFileType.implementationFile) { + throw new RuntimeException("'generateJaxrsApplicationClient' can only be used when generating implementation file ('outputFileType' parameter is 'implementationFile')."); + } if (generateJaxrsApplicationClient && outputFileType != TypeScriptFileType.implementationFile) { throw new RuntimeException("'generateJaxrsApplicationClient' can only be used when generating implementation file ('outputFileType' parameter is 'implementationFile')."); } @@ -761,9 +768,13 @@ public void setRestOptionsType(String restOptionsType) { public List getRestApplicationParserFactories() { if (restApplicationParserFactories == null) { final List factories = new ArrayList<>(); - if (isGenerateJaxrs() || !isGenerateSpring()) { + if (isGenerateJaxrs() || (!isGenerateSpring() && !isGenerateJakartaRs())) { factories.add(new JaxrsApplicationParser.Factory()); } + if (isGenerateJakartaRs() || (!isGenerateSpring() && !isGenerateJaxrs())) { + factories.add(new JakartaRsApplicationParser.Factory()); + } + if (isGenerateSpring()) { final String springClassName = "cz.habarta.typescript.generator.spring.SpringApplicationParser$Factory"; final Class springClass; @@ -786,6 +797,10 @@ public List getRestApplicationParserFactories() { } return restApplicationParserFactories; } + + public boolean isGenerateJakartaRs() { + return generateJakartaRsApplicationInterface || generateJakartaRsApplicationClient; + } public boolean isGenerateJaxrs() { return generateJaxrsApplicationInterface || generateJaxrsApplicationClient; @@ -796,7 +811,7 @@ public boolean isGenerateSpring() { } public boolean isGenerateRest() { - return isGenerateJaxrs() || isGenerateSpring(); + return isGenerateJakartaRs() || isGenerateJaxrs() || isGenerateSpring(); } public boolean areDefaultStringEnumsOverriddenByExtension() { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java index a4362d272..cedd58dbf 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java @@ -637,7 +637,7 @@ private void createRestClients(TsModel tsModel, SymbolTable symbolTable, ListemptyList(), null ); - final boolean bothInterfacesAndClients = settings.generateJaxrsApplicationInterface || settings.generateSpringApplicationInterface; + final boolean bothInterfacesAndClients = settings.generateJakartaRsApplicationInterface || settings.generateJaxrsApplicationInterface || settings.generateSpringApplicationInterface; final String groupingSuffix = bothInterfacesAndClients ? null : "Client"; final Map> groupedMethods = processRestMethods(tsModel, restApplications, symbolTable, groupingSuffix, responseSymbol, optionsType, true); for (Map.Entry> entry : groupedMethods.entrySet()) { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JakartaRsApplicationParser.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JakartaRsApplicationParser.java new file mode 100644 index 000000000..7a6a6020a --- /dev/null +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JakartaRsApplicationParser.java @@ -0,0 +1,346 @@ + +package cz.habarta.typescript.generator.parser; + +import cz.habarta.typescript.generator.JakartaRsApplicationScanner; +import cz.habarta.typescript.generator.Settings; +import cz.habarta.typescript.generator.TsType; +import cz.habarta.typescript.generator.TypeProcessor; +import cz.habarta.typescript.generator.TypeScriptGenerator; +import cz.habarta.typescript.generator.type.JTypeWithNullability; +import cz.habarta.typescript.generator.util.GenericsResolver; +import cz.habarta.typescript.generator.util.Pair; +import cz.habarta.typescript.generator.util.Utils; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.CookieParam; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.GenericEntity; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +public class JakartaRsApplicationParser extends RestApplicationParser { + + public static class Factory extends RestApplicationParser.Factory { + + @Override + public TypeProcessor getSpecificTypeProcessor() { + return (javaType, context) -> { + final Class rawClass = Utils.getRawClassOrNull(javaType); + if (rawClass != null) { + for (Map.Entry, TsType> entry : getStandardEntityClassesMapping().entrySet()) { + final Class cls = entry.getKey(); + final TsType type = entry.getValue(); + if (cls.isAssignableFrom(rawClass)) { + return type != null ? new TypeProcessor.Result(type) : null; + } + } + if (getDefaultExcludedClassNames().contains(rawClass.getName())) { + return new TypeProcessor.Result(TsType.Any); + } + } + return null; + }; + } + + @Override + public JakartaRsApplicationParser create(Settings settings, TypeProcessor commonTypeProcessor) { + return new JakartaRsApplicationParser(settings, commonTypeProcessor); + } + + }; + + public JakartaRsApplicationParser(Settings settings, TypeProcessor commonTypeProcessor) { + super(settings, commonTypeProcessor, new RestApplicationModel(RestApplicationType.JakartaRs)); + } + + @Override + public Result tryParse(SourceType sourceType) { + if (!(sourceType.type instanceof Class)) { + return null; + } + final Class cls = (Class) sourceType.type; + + // application + if (Application.class.isAssignableFrom(cls)) { + final ApplicationPath applicationPathAnnotation = cls.getAnnotation(ApplicationPath.class); + if (applicationPathAnnotation != null) { + model.setApplicationPath(applicationPathAnnotation.value()); + } + model.setApplicationName(cls.getSimpleName()); + final List> discoveredTypes = JakartaRsApplicationScanner.scanJakartaRsApplication(cls, isClassNameExcluded); + return new Result(discoveredTypes); + } + + // resource + final Path path = cls.getAnnotation(Path.class); + if (path != null) { + TypeScriptGenerator.getLogger().verbose("Parsing Jakarta RS resource: " + cls.getName()); + final Result result = new Result(); + parseResource(result, new ResourceContext(cls, path.value()), cls); + return result; + } + + return null; + } + + private void parseResource(Result result, ResourceContext context, Class resourceClass) { + // subContext + final Map pathParamTypes = new LinkedHashMap<>(); + for (Field field : resourceClass.getDeclaredFields()) { + final PathParam pathParamAnnotation = field.getAnnotation(PathParam.class); + if (pathParamAnnotation != null) { + pathParamTypes.put(pathParamAnnotation.value(), field.getType()); + } + } + final ResourceContext subContext = context.subPathParamTypes(pathParamTypes); + // parse resource methods + final List methods = Arrays.asList(resourceClass.getMethods()); + Collections.sort(methods, Utils.methodComparator()); + for (Method method : methods) { + parseResourceMethod(result, subContext, resourceClass, method); + } + } + + private void parseResourceMethod(Result result, ResourceContext context, Class resourceClass, Method method) { + final Path pathAnnotation = method.getAnnotation(Path.class); + // subContext + context = context.subPath(pathAnnotation != null ? pathAnnotation.value() : null); + final Map pathParamTypes = new LinkedHashMap<>(); + for (Parameter parameter : method.getParameters()) { + final PathParam pathParamAnnotation = parameter.getAnnotation(PathParam.class); + if (pathParamAnnotation != null) { + pathParamTypes.put(pathParamAnnotation.value(), parameter.getParameterizedType()); + } + } + context = context.subPathParamTypes(pathParamTypes); + // JAX-RS specification - 3.3 Resource Methods + final HttpMethod httpMethod = getHttpMethod(method); + if (httpMethod != null) { + // swagger + final SwaggerOperation swaggerOperation = settings.ignoreSwaggerAnnotations + ? new SwaggerOperation() + : Swagger.parseSwaggerAnnotations(method); + if (swaggerOperation.possibleResponses != null) { + for (SwaggerResponse response : swaggerOperation.possibleResponses) { + if (response.responseType != null) { + foundType(result, response.responseType, resourceClass, method.getName()); + } + } + } + if (swaggerOperation.hidden) { + return; + } + // path parameters + final List pathParams = new ArrayList<>(); + final PathTemplate pathTemplate = PathTemplate.parse(context.path); + for (PathTemplate.Part part : pathTemplate.getParts()) { + if (part instanceof PathTemplate.Parameter) { + final PathTemplate.Parameter parameter = (PathTemplate.Parameter) part; + final Type type = context.pathParamTypes.get(parameter.getOriginalName()); + final Type paramType = type != null ? type : String.class; + final Type resolvedParamType = GenericsResolver.resolveType(resourceClass, paramType, method.getDeclaringClass()); + pathParams.add(new MethodParameterModel(parameter.getValidName(), resolvedParamType)); + foundType(result, resolvedParamType, resourceClass, method.getName()); + } + } + // query parameters + final List queryParams = new ArrayList<>(); + for (Parameter param : method.getParameters()) { + final QueryParam queryParamAnnotation = param.getAnnotation(QueryParam.class); + if (queryParamAnnotation != null) { + queryParams.add(new RestQueryParam.Single(new MethodParameterModel(queryParamAnnotation.value(), param.getParameterizedType()), false)); + foundType(result, param.getParameterizedType(), resourceClass, method.getName()); + } + final BeanParam beanParamAnnotation = param.getAnnotation(BeanParam.class); + if (beanParamAnnotation != null) { + final Class beanParamClass = param.getType(); + final BeanModel paramBean = getQueryParameters(beanParamClass); + if (paramBean != null) { + queryParams.add(new RestQueryParam.Bean(paramBean)); + for (PropertyModel property : paramBean.getProperties()) { + foundType(result, property.getType(), beanParamClass, property.getName()); + } + } + } + } + // JAX-RS specification - 3.3.2.1 Entity Parameters + final List parameterTypes = settings.getTypeParser().getMethodParameterTypes(method); + final List> parameters = Utils.zip(Arrays.asList(method.getParameters()), parameterTypes); + final MethodParameterModel entityParameter = getEntityParameter(resourceClass, method, parameters); + if (entityParameter != null) { + foundType(result, entityParameter.getType(), resourceClass, method.getName()); + } + // JAX-RS specification - 3.3.3 Return Type + final Class returnType = method.getReturnType(); + final Type parsedReturnType = settings.getTypeParser().getMethodReturnType(method); + final Type plainReturnType = JTypeWithNullability.getPlainType(parsedReturnType); + final Type modelReturnType; + if (returnType == void.class) { + //for async response also use swagger + if (hasAnyAnnotation(method.getParameters(), Collections.singletonList(Suspended.class))) { + if (swaggerOperation.responseType != null) { + modelReturnType = swaggerOperation.responseType; + } else { + modelReturnType = Object.class; + } + } else { + modelReturnType = returnType; + } + } else if (returnType == Response.class) { + if (swaggerOperation.responseType != null) { + modelReturnType = swaggerOperation.responseType; + } else { + modelReturnType = Object.class; + } + } else if (plainReturnType instanceof ParameterizedType && returnType == GenericEntity.class) { + final ParameterizedType parameterizedReturnType = (ParameterizedType) plainReturnType; + modelReturnType = parameterizedReturnType.getActualTypeArguments()[0]; + } else { + modelReturnType = parsedReturnType; + } + final Type resolvedModelReturnType = GenericsResolver.resolveType(resourceClass, modelReturnType, method.getDeclaringClass()); + foundType(result, resolvedModelReturnType, resourceClass, method.getName()); + // comments + final List comments = Swagger.getOperationComments(swaggerOperation); + // create method + model.getMethods().add(new RestMethodModel(resourceClass, method.getName(), resolvedModelReturnType, method, + context.rootResource, httpMethod.value(), context.path, pathParams, queryParams, entityParameter, comments)); + } + // JAX-RS specification - 3.4.1 Sub Resources + if (pathAnnotation != null && httpMethod == null) { + parseResource(result, context, method.getReturnType()); + } + } + + private static HttpMethod getHttpMethod(Method method) { + for (Annotation annotation : method.getAnnotations()) { + final HttpMethod httpMethodAnnotation = annotation.annotationType().getAnnotation(HttpMethod.class); + if (httpMethodAnnotation != null) { + return httpMethodAnnotation; + } + } + return null; + } + + private static BeanModel getQueryParameters(Class paramBean) { + final List properties = new ArrayList<>(); + final List fields = Utils.getAllFields(paramBean); + for (Field field : fields) { + final QueryParam annotation = field.getAnnotation(QueryParam.class); + if (annotation != null) { + properties.add(new PropertyModel(annotation.value(), field.getGenericType(), /*optional*/true, null, field, null, null, null)); + } + } + try { + final BeanInfo beanInfo = Introspector.getBeanInfo(paramBean); + for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) { + final Method writeMethod = propertyDescriptor.getWriteMethod(); + if (writeMethod != null) { + final QueryParam annotation = writeMethod.getAnnotation(QueryParam.class); + if (annotation != null) { + properties.add(new PropertyModel(annotation.value(), propertyDescriptor.getPropertyType(), /*optional*/true, null, writeMethod, null, null, null)); + } + } + } + } catch (IntrospectionException e) { + TypeScriptGenerator.getLogger().warning(String.format("Cannot introspect '%s' class: " + e.getMessage(), paramBean)); + } + if (properties.isEmpty()) { + return null; + } else { + return new BeanModel(paramBean, null, null, null, null, null, properties, null); + } + } + + private MethodParameterModel getEntityParameter(Class resourceClass, Method method, List> parameters) { + for (Pair pair : parameters) { + if (!Utils.hasAnyAnnotation(annotationClass -> pair.getValue1().getAnnotation(annotationClass), Arrays.asList( + MatrixParam.class, + QueryParam.class, + PathParam.class, + CookieParam.class, + HeaderParam.class, + Suspended.class, + Context.class, + FormParam.class, + BeanParam.class + ))) { + final Type resolvedType = GenericsResolver.resolveType(resourceClass, pair.getValue2(), method.getDeclaringClass()); + return new MethodParameterModel(pair.getValue1().getName(), resolvedType); + } + } + return null; + } + + private static boolean hasAnyAnnotation(Parameter[] parameters, List> annotationClasses) { + return Stream.of(parameters) + .anyMatch(parameter -> Utils.hasAnyAnnotation(parameter::getAnnotation, annotationClasses)); + } + + private static Map, TsType> getStandardEntityClassesMapping() { + // JAX-RS specification - 4.2.4 Standard Entity Providers + if (standardEntityClassesMapping == null) { + final Map, TsType> map = new LinkedHashMap<>(); + // null value means that class is handled by DefaultTypeProcessor + map.put(byte[].class, TsType.Any); + map.put(java.lang.String.class, null); + map.put(java.io.InputStream.class, TsType.Any); + map.put(java.io.Reader.class, TsType.Any); + map.put(java.io.File.class, TsType.Any); + map.put(javax.activation.DataSource.class, TsType.Any); + map.put(javax.xml.transform.Source.class, TsType.Any); + map.put(javax.xml.bind.JAXBElement.class, null); + map.put(MultivaluedMap.class, TsType.Any); + map.put(StreamingOutput.class, TsType.Any); + map.put(java.lang.Boolean.class, null); + map.put(java.lang.Character.class, null); + map.put(java.lang.Number.class, null); + map.put(long.class, null); + map.put(int.class, null); + map.put(short.class, null); + map.put(byte.class, null); + map.put(double.class, null); + map.put(float.class, null); + map.put(boolean.class, null); + map.put(char.class, null); + standardEntityClassesMapping = map; + } + return standardEntityClassesMapping; + } + + private static Map, TsType> standardEntityClassesMapping; + + private static List getDefaultExcludedClassNames() { + return Arrays.asList( + "org.glassfish.jersey.media.multipart.FormDataBodyPart" + ); + } + +} diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationType.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationType.java index 209538e18..ceb45acaa 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationType.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationType.java @@ -8,7 +8,8 @@ public enum RestApplicationType { Jaxrs(settings -> settings.generateJaxrsApplicationInterface, settings -> settings.generateJaxrsApplicationClient), - Spring(settings -> settings.generateSpringApplicationInterface, settings -> settings.generateSpringApplicationClient); + Spring(settings -> settings.generateSpringApplicationInterface, settings -> settings.generateSpringApplicationClient), + JakartaRs(settings -> settings.generateJakartaRsApplicationInterface, settings -> settings.generateJakartaRsApplicationClient); private RestApplicationType(Function generateInterface, Function generateClient) { this.generateInterface = generateInterface; diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JakartaRsApplicationTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JakartaRsApplicationTest.java new file mode 100644 index 000000000..253d4e324 --- /dev/null +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JakartaRsApplicationTest.java @@ -0,0 +1,736 @@ + +package cz.habarta.typescript.generator; + +import com.fasterxml.jackson.core.type.TypeReference; +import cz.habarta.typescript.generator.compiler.ModelCompiler; +import cz.habarta.typescript.generator.parser.BeanModel; +import cz.habarta.typescript.generator.parser.JakartaRsApplicationParser; +import cz.habarta.typescript.generator.parser.Model; +import cz.habarta.typescript.generator.parser.SourceType; +import cz.habarta.typescript.generator.type.JGenericArrayType; +import cz.habarta.typescript.generator.type.JTypeWithNullability; +import io.github.classgraph.ClassGraph; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.CookieParam; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.GenericEntity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; +import java.io.File; +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import javax.activation.DataSource; +import javax.xml.bind.JAXBElement; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.Assert; +import org.junit.Test; + + +@SuppressWarnings("unused") +public class JakartaRsApplicationTest { + + @Test + public void testReturnedTypesFromApplication() { + final List> sourceTypes = JakartaRsApplicationScanner.scanJakartaRsApplication(TestApplication.class, null); + List types = getTypes(sourceTypes); + final List expectedTypes = Arrays.asList( + TestApplication.class, + TestResource1.class + ); + assertHasSameItems(expectedTypes, types); + } + + @Test + public void testReturnedTypesFromResource() { + Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + + JakartaRsApplicationParser jakartaRsApplicationParser = createJakartaRsApplicationParser(settings); + final JakartaRsApplicationParser.Result result = jakartaRsApplicationParser.tryParse(new SourceType<>(TestResource1.class)); + Assert.assertNotNull(result); + List types = getTypes(result.discoveredTypes); + final List expectedTypes = Arrays.asList( + A.class, + new TypeReference>(){}.getType(), + C.class, + new TypeReference>(){}.getType(), + List.class, + E.class, + new TypeReference>(){}.getType(), + G.class, + new TypeReference>(){}.getType(), + I.class, + JGenericArrayType.of(J[].class), + // types handled by DefaultTypeProcessor + String.class, Boolean.class, Character.class, Number.class, Integer.class, int.class, void.class + ); + assertHasSameItems(expectedTypes, types); + } + + @Test + public void testWithParsingWithExplicitApplication() { + final List> sourceTypes = JakartaRsApplicationScanner.scanJakartaRsApplication(TestApplication.class, null); + testWithParsing(sourceTypes, true); + } + + @Test + public void testWithParsingWithDefaultApplication() { + final List> sourceTypes = JakartaRsApplicationScanner.scanAutomaticJakartaRsApplication(new ClassGraph().enableAllInfo().scan(), null); + testWithParsing(sourceTypes, false); + } + + private void testWithParsing(List> types, boolean exactMatch) { + Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + + final Model model = new TypeScriptGenerator(settings).getModelParser().parseModel(types); + final ArrayList> classes = new ArrayList<>(); + for (BeanModel beanModel : model.getBeans()) { + classes.add(beanModel.getOrigin()); + } + final List> expectedClasses = Arrays.asList( + A.class, + B.class, + C.class, + D.class, + E.class, + F.class, + G.class, + H.class, + I.class, + J.class + ); + if (exactMatch) { + assertHasSameItems(expectedClasses, classes); + } else { + Assert.assertTrue(classes.containsAll(expectedClasses)); + } + } + + @Test + public void testExcludedResource() { + final Predicate excludeFilter = Settings.createExcludeFilter(Arrays.asList( + TestResource1.class.getName() + ), null); + final List> sourceTypes = JakartaRsApplicationScanner.scanJakartaRsApplication(TestApplication.class, excludeFilter); + final List types = getTypes(sourceTypes); + Assert.assertEquals(1, types.size()); + Assert.assertTrue(getTypes(sourceTypes).contains(TestApplication.class)); + } + + @Test + public void testExcludedType() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + + settings.setExcludeFilter(Arrays.asList( + A.class.getName(), + J.class.getName() + ), null); + final JakartaRsApplicationParser jakartaRsApplicationParser = createJakartaRsApplicationParser(settings); + final JakartaRsApplicationParser.Result result = jakartaRsApplicationParser.tryParse(new SourceType<>(TestResource1.class)); + Assert.assertNotNull(result); + Assert.assertTrue(!getTypes(result.discoveredTypes).contains(A.class)); + Assert.assertTrue(getTypes(result.discoveredTypes).contains(JGenericArrayType.of(J[].class))); + } + + private static JakartaRsApplicationParser createJakartaRsApplicationParser(Settings settings) { + final TypeProcessor typeProcessor = new TypeScriptGenerator(settings).getCommonTypeProcessor(); + final JakartaRsApplicationParser jakartaRsApplicationParser = new JakartaRsApplicationParser(settings, typeProcessor); + return jakartaRsApplicationParser; + } + + private List getTypes(final List> sourceTypes) { + final List types = new ArrayList<>(); + for (SourceType sourceType : sourceTypes) { + types.add(JTypeWithNullability.removeNullability(sourceType.type)); + } + return types; + } + + private static void assertHasSameItems(Collection expected, Collection actual) { + for (T value : expected) { + Assert.assertTrue("Value '" + value + "' is missing in " + actual, actual.contains(value)); + } + for (T value : actual) { + Assert.assertTrue("Value '" + value + "' not expected.", expected.contains(value)); + } + } + + private static class TestApplication extends Application { + @Override + public Set> getClasses() { + return new LinkedHashSet<>(Arrays.asList( + TestResource1.class + )); + } + } + + @Path("test") + static class TestResource1 { + @GET + public void getVoid() { + } + @GET + public Response getResponse() { + return null; + } + @GET + @Path("a") + public GenericEntity getA() { + return null; + } + @GET + public GenericEntity> getB() { + return null; + } + @GET + public C getC() { + return null; + } + @GET + public List getD() { + return null; + } + @SuppressWarnings("rawtypes") + @GET + public List getRawList() { + return null; + } + @GET + @Path("e") + public E getE() { + return null; + } + @Path("f") + public SubResource1 getSubResource1() { + return null; + } + @POST + public void setG(G g) { + } + @POST + public void setHs(Map hs) { + } + @POST + public void setI( + @MatrixParam("") String matrixParam, + @QueryParam("") String queryParam, + @PathParam("") String pathParam, + @CookieParam("") String cookieParam, + @Suspended AsyncResponse suspendedParam, + @HeaderParam("") String headerParam, + @Context String context, + @FormParam("") String formParam, + I entityI) { + } + @POST + @ApiOperation(value = "async", response = String.class) + public void setAsync( + @Suspended AsyncResponse suspendedParam + ) { + } + @POST + public void setJs(J[] js) { + } + @POST + public void setStandardEntity(byte[] value) {} + @POST + public void setStandardEntity(String value) {} + @POST + public void setStandardEntity(InputStream value) {} + @POST + public void setStandardEntity(Reader value) {} + @POST + public void setStandardEntity(File value) {} + @POST + public void setStandardEntity(DataSource value) {} + @POST + public void setStandardEntity(Source value) {} + @POST + public void setStandardEntity(DOMSource value) {} + @POST + public void setStandardEntity(JAXBElement value) {} + @POST + public void setStandardEntity(MultivaluedMap value) {} + @POST + public void setStandardEntity(StreamingOutput value) {} + @POST + public void setStandardEntity(Boolean value) {} + @POST + public void setStandardEntity(Character value) {} + @POST + public void setStandardEntity(Number value) {} + @POST + public void setStandardEntity(Integer value) {} + @POST + public void setStandardEntity(int value) {} + } + + private static class SubResource1 { + @GET + public List getFs() { + return null; + } + } + + private static class A {} + private static class B {} + private static class C {} + private static class D {} + private static class E {} + private static class F {} + private static class G {} + private static class H {} + private static class I {} + private static class J {} + + @Test + public void basicInterfaceTest() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(TestResource1.class)); + final String errorMessage = "Unexpected output: " + output; + Assert.assertTrue(errorMessage, output.contains("interface RestApplication")); + Assert.assertTrue(errorMessage, output.contains("getA(): RestResponse;")); + Assert.assertTrue(errorMessage, output.contains("type RestResponse = Promise;")); + Assert.assertTrue(errorMessage, !output.contains("function uriEncoding")); + Assert.assertTrue(errorMessage, output.contains("setAsync(): RestResponse")); + } + + @Test + public void complexInterfaceTest() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); + final String errorMessage = "Unexpected output: " + output; + Assert.assertTrue(errorMessage, output.contains("type RestResponse = Promise;")); + Assert.assertTrue(errorMessage, output.contains("interface Organization")); + Assert.assertTrue(errorMessage, output.contains("interface OrganizationApplication")); + Assert.assertTrue(errorMessage, output.contains("HTTP GET /api/organizations/{ organizationCode : [a-z]+ }/{organizationId}")); + Assert.assertTrue(errorMessage, output.contains("getOrganization(organizationCode: string, organizationId: number): RestResponse;")); + Assert.assertTrue(errorMessage, output.contains("searchOrganizations(queryParams?: { name?: string; \"search-limit\"?: number; }): RestResponse;")); + Assert.assertTrue(errorMessage, output.replace("arg1", "organization").contains("setOrganization(organizationCode: string, organizationId: number, organization: Organization): RestResponse;")); + Assert.assertTrue(errorMessage, output.contains("HTTP GET /api/people/{personId}/address/{address-id}")); + Assert.assertTrue(errorMessage, output.contains("getAddress(personId: number, addressId: number): RestResponse
;")); + Assert.assertTrue(errorMessage, output.contains("HTTP GET /api/people/{personId}")); + Assert.assertTrue(errorMessage, output.contains("getPerson(personId: number): RestResponse;")); + } + + @Test + public void methodNameConflictTest() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(NameConflictResource.class)); + final String errorMessage = "Unexpected output: " + output; + Assert.assertTrue(errorMessage, output.contains("interface RestApplication")); + Assert.assertTrue(errorMessage, output.replace("arg0", "person").contains("person$POST$conflict(person: Person): RestResponse;")); + Assert.assertTrue(errorMessage, output.contains("person$GET$conflict(): RestResponse;")); + Assert.assertTrue(errorMessage, output.contains("person$GET$conflict_search(queryParams?: { search?: string; }): RestResponse;")); + Assert.assertTrue(errorMessage, output.contains("person$GET$conflict_personId(personId: number): RestResponse;")); + } + + @Test + public void customizationTest() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + settings.restResponseType = "AxiosPromise"; + settings.restOptionsType = "AxiosRequestConfig"; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); + final String errorMessage = "Unexpected output: " + output; + Assert.assertTrue(errorMessage, output.contains("type RestResponse = AxiosPromise;")); + Assert.assertTrue(errorMessage, output.contains("searchOrganizations(queryParams?: { name?: string; \"search-limit\"?: number; }, options?: AxiosRequestConfig): RestResponse;")); + } + + @Test + public void basicClientTest() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateJakartaRsApplicationClient = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); + final String errorMessage = "Unexpected output: " + output; + // HttpClient + Assert.assertTrue(errorMessage, output.contains("interface HttpClient")); + Assert.assertTrue(errorMessage, output.contains("request(requestConfig: { method: string; url: string; queryParams?: any; data?: any; copyFn?: (data: R) => R; }): RestResponse;")); + // application client + Assert.assertTrue(errorMessage, output.contains("class OrganizationApplicationClient")); + Assert.assertTrue(errorMessage, output.contains("getPerson(personId: number): RestResponse")); + Assert.assertTrue(errorMessage, output.contains("return this.httpClient.request({ method: \"GET\", url: uriEncoding`api/people/${personId}` });")); + Assert.assertTrue(errorMessage, output.contains("getAddress(personId: number, addressId: number): RestResponse
")); + Assert.assertTrue(errorMessage, output.contains("return this.httpClient.request({ method: \"GET\", url: uriEncoding`api/people/${personId}/address/${addressId}` });")); + Assert.assertTrue(errorMessage, output.contains("type RestResponse = Promise;")); + // helper + Assert.assertTrue(errorMessage, output.contains("function uriEncoding")); + } + + @Test + public void clientCustomizationTest() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateJakartaRsApplicationClient = true; + settings.restResponseType = "AxiosPromise"; + settings.restOptionsType = "AxiosRequestConfig"; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); + final String errorMessage = "Unexpected output: " + output; + // HttpClient + Assert.assertTrue(errorMessage, output.contains("request(requestConfig: { method: string; url: string; queryParams?: any; data?: any; copyFn?: (data: R) => R; options?: AxiosRequestConfig; }): RestResponse;")); + // application client + Assert.assertTrue(errorMessage, output.contains("class OrganizationApplicationClient")); + Assert.assertTrue(errorMessage, output.contains("getPerson(personId: number, options?: AxiosRequestConfig): RestResponse")); + Assert.assertTrue(errorMessage, output.contains("return this.httpClient.request({ method: \"GET\", url: uriEncoding`api/people/${personId}`, options: options });")); + Assert.assertTrue(errorMessage, output.contains("type RestResponse = AxiosPromise;")); + } + + @Test + public void testNamespacingPerResource() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateJakartaRsApplicationInterface = true; + settings.generateJakartaRsApplicationClient = true; + settings.restNamespacing = RestNamespacing.perResource; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); + final String errorMessage = "Unexpected output: " + output; + Assert.assertTrue(errorMessage, !output.contains("class OrganizationApplicationClient")); + Assert.assertTrue(errorMessage, output.contains("class OrganizationsResourceClient implements OrganizationsResource ")); + Assert.assertTrue(errorMessage, !output.contains("class OrganizationResourceClient")); + Assert.assertTrue(errorMessage, output.contains("class PersonResourceClient implements PersonResource ")); + } + + @Test + public void testNamespacingByAnnotation() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateJakartaRsApplicationInterface = true; + settings.generateJakartaRsApplicationClient = true; + settings.restNamespacing = RestNamespacing.byAnnotation; + settings.restNamespacingAnnotation = Api.class; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); + final String errorMessage = "Unexpected output: " + output; + Assert.assertTrue(errorMessage, output.contains("class OrgApiClient implements OrgApi ")); + Assert.assertTrue(errorMessage, output.contains("class OrganizationApplicationClient implements OrganizationApplication ")); + Assert.assertTrue(errorMessage, !output.contains("class OrganizationsResourceClient")); + Assert.assertTrue(errorMessage, !output.contains("class OrganizationResourceClient")); + Assert.assertTrue(errorMessage, !output.contains("class PersonResourceClient")); + } + + @Test + public void testJavadoc() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateJakartaRsApplicationInterface = true; + settings.javadocXmlFiles = Arrays.asList(new File("src/test/javadoc/test-javadoc.xml")); + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); + Assert.assertTrue(output.contains("Returns person with specified ID.")); + } + + @Test + public void testSwaggerComments() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateJakartaRsApplicationInterface = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); + Assert.assertTrue(output.contains("Comment in swagger annotation")); + Assert.assertTrue(output.contains("Response code 200 - ok")); + Assert.assertTrue(output.contains("Response code 400 - not ok")); + } + + @Test + public void testDeprecatedAnnotationComment() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateJakartaRsApplicationInterface = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); + Assert.assertTrue(output.contains("@deprecated")); + } + + @ApplicationPath("api") + public static class OrganizationApplication extends Application { + @Override + public Set> getClasses() { + return new LinkedHashSet<>(Arrays.asList( + OrganizationsResource.class, + PersonResource.class + )); + } + } + + @Api("OrgApi") + @Path("organizations") + public static class OrganizationsResource { + @PathParam("organizationId") + protected long organizationId; + @GET + public List searchOrganizations(@QueryParam("name") String oranizationName, @QueryParam("search-limit") int searchLimit) { + return null; + } + @Path("{ organizationCode : [a-z]+ }/{organizationId}") + public OrganizationResource getOrganizationResource() { + return null; + } + } + + public static class OrganizationResource { + @GET + public Organization getOrganization() { + return null; + } + @PUT + public void setOrganization(@PathParam("organizationCode") String organizationCode, Organization organization) { + } + } + + public static class Organization { + public String name; + } + + @Path("people/{personId}") + public static class PersonResource { + + @PathParam("personId") + protected long personId; + + @ApiOperation(value = "Comment in swagger annotation") + @ApiResponses({ + @ApiResponse(code = 200, message = "ok"), + @ApiResponse(code = 400, message = "not ok"), + }) + @GET + public Person getPerson() { + return null; + } + + @GET + @Path("address/{address-id}") + @Deprecated + public Address getAddress(@PathParam("address-id") long addressId) { + return null; + } + } + + public static class Person { + public String name; + + public Person(String name) { + this.name = name; + } + } + + public static class Address { + public String name; + } + + @Path("conflict") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public static class NameConflictResource { + @POST + public Person person(Person person) { + return new Person("POST"); + } + @GET + public Person person() { + return new Person("A"); + } + @GET + @Path("search") + public Person person(@QueryParam("search") String search) { + return new Person("B"); + } + @GET + @Path("{person-id:.+}") + public Person person(@PathParam("person-id") long personId) { + return new Person("C"); + } + } + + @Test + public void testGettingValidIdentifierName() { + Assert.assertEquals("foo", ModelCompiler.getValidIdentifierName("foo")); + Assert.assertEquals("personId", ModelCompiler.getValidIdentifierName("person-id")); + Assert.assertEquals("veryLongParameterName", ModelCompiler.getValidIdentifierName("very-long-parameter-name")); + Assert.assertEquals("$nameWithDollar", ModelCompiler.getValidIdentifierName("$nameWithDollar")); + Assert.assertEquals("NameWithManyDashes", ModelCompiler.getValidIdentifierName("-name--with-many---dashes-")); + Assert.assertEquals("a2b3c4", ModelCompiler.getValidIdentifierName("1a2b3c4")); + Assert.assertEquals("a2b3c4", ModelCompiler.getValidIdentifierName("111a2b3c4")); + } + + @Test + public void testEnumQueryParam() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(EnumQueryParamResource.class)); + Assert.assertTrue(output.contains("queryParams?: { target?: TargetEnum; }")); + Assert.assertTrue(output.contains("type TargetEnum = \"Target1\" | \"Target2\"")); + } + + @Path("enum-query-param") + public static class EnumQueryParamResource { + @GET + @Path("somePath") + public List getFoo(@QueryParam("target") TargetEnum target) { + return Collections.emptyList(); + } + } + + public enum TargetEnum { + Target1, Target2 + } + + @Test + public void testBeanParam() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + settings.generateJakartaRsApplicationClient = true; + settings.outputFileType = TypeScriptFileType.implementationFile; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(BeanParamResource.class)); + Assert.assertTrue(output.contains("interface SearchParams1QueryParams")); + Assert.assertTrue(output.contains("interface SearchParams2QueryParams")); + Assert.assertTrue(output.contains("queryParams?: SearchParams1QueryParams & SearchParams2QueryParams & { message?: string; }")); + } + + public static class SearchParams1 { + @QueryParam("id") + private Integer id; + + @QueryParam("name") + private String name; + } + + public static class SearchParams2 { + private String description; + @QueryParam("description") + public void setDescription(String description) { + this.description = description; + } + } + + @Test + public void testPathParameterWithReservedWord() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationInterface = true; + settings.generateJakartaRsApplicationClient = true; + settings.outputFileType = TypeScriptFileType.implementationFile; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(ResourceWithReservedWord.class)); + Assert.assertTrue(output.contains("getLogs(_class: string): RestResponse;")); + Assert.assertTrue(output.contains("getLogs(_class: string): RestResponse {")); + Assert.assertTrue(output.contains("uriEncoding`logs/${_class}`")); + } + + @Path("") + public static class ResourceWithReservedWord { + + @GET + @Path("/logs/{class}") + public Collection getLogs(@PathParam("class") String clazz) { + return null; + } + } + +// http://localhost:9998/bean-param?id=1&name=vh&description=desc&message=hello + + @Path("bean-param") + @Produces(MediaType.APPLICATION_JSON) + public static class BeanParamResource { + + @GET + public List getItems( + @BeanParam SearchParams1 params1, + @BeanParam SearchParams2 params2, + @QueryParam("message") String message + ) { + return Collections.emptyList(); + } + } + + @Test + public void testRegExpInPath() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationClient = true; + settings.outputFileType = TypeScriptFileType.implementationFile; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(RegExpResource.class)); + Assert.assertTrue(output.contains("getWithId(id: number)")); + Assert.assertTrue(output.contains("url: uriEncoding`objects/${id}`")); + } + + @Path("objects") + public static class RegExpResource { + @GET + @Path("{id: [0-9]{1,99}}") +// @Path("{id: [0-9]+}") + public String getWithId(@PathParam("id") long id) { + return null; + } + } + + @Test + public void testGenericResources() { + final Settings settings = TestUtils.settings(); + settings.generateJakartaRsApplicationClient = true; + settings.outputFileType = TypeScriptFileType.implementationFile; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(AccountResource.class)); + Assert.assertTrue(!output.contains("get(id: ID): RestResponse")); + Assert.assertTrue(output.contains("get(id: number): RestResponse")); + Assert.assertTrue(output.contains("interface AccountDto")); + } + + public static class AccountDto { + public Integer id; + public String name; + } + + public static interface AbstractCrudResource { + @GET + @Path("{id}") + public ENTITY get(@PathParam("id") ID id); + } + + @Path("/account") + public static interface AccountResource extends AbstractCrudResource { + @GET + @Path("/test") + void test(); + } + + public static void main(String[] args) { + final ResourceConfig config = new ResourceConfig(BeanParamResource.class, JacksonFeature.class); + JdkHttpServerFactory.createHttpServer(URI.create("http://localhost:9998/"), config); + System.out.println("Jersey started."); + } + +} diff --git a/typescript-generator-core/src/test/javadoc/test-javadoc.xml b/typescript-generator-core/src/test/javadoc/test-javadoc.xml index 760e4c352..8ac60d139 100644 --- a/typescript-generator-core/src/test/javadoc/test-javadoc.xml +++ b/typescript-generator-core/src/test/javadoc/test-javadoc.xml @@ -2767,6 +2767,49 @@ + + + + + Returns person with specified ID. + + + + + + + + + + address-id + + + + + + + + + address/{address-id} + + + + + + + people/{personId} + + + + + + + + personId + + + + diff --git a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java index a8fb841a1..12d43310b 100644 --- a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java +++ b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java @@ -90,6 +90,8 @@ public class GenerateTask extends DefaultTask { public boolean disableTaggedUnions; public boolean generateReadonlyAndWriteonlyJSDocTags; public boolean ignoreSwaggerAnnotations; + public boolean generateJakartaRsApplicationInterface; + public boolean generateJakartaRsApplicationClient; public boolean generateJaxrsApplicationInterface; public boolean generateJaxrsApplicationClient; public boolean generateSpringApplicationInterface; @@ -179,6 +181,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.disableTaggedUnions = disableTaggedUnions; settings.generateReadonlyAndWriteonlyJSDocTags = generateReadonlyAndWriteonlyJSDocTags; settings.ignoreSwaggerAnnotations = ignoreSwaggerAnnotations; + settings.generateJakartaRsApplicationInterface = generateJakartaRsApplicationInterface; + settings.generateJakartaRsApplicationClient = generateJakartaRsApplicationClient; settings.generateJaxrsApplicationInterface = generateJaxrsApplicationInterface; settings.generateJaxrsApplicationClient = generateJaxrsApplicationClient; settings.generateSpringApplicationInterface = generateSpringApplicationInterface; diff --git a/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java b/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java index 9bd014eba..9f1613782 100644 --- a/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java +++ b/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java @@ -526,6 +526,18 @@ public class GenerateMojo extends AbstractMojo { */ @Parameter private boolean ignoreSwaggerAnnotations; + + /** + * If true interface for Jakarta-RS REST application will be generated. + */ + @Parameter + private boolean generateJakartaRsApplicationInterface; + + /** + * If true client for Jakarta-RS REST application will be generated. + */ + @Parameter + private boolean generateJakartaRsApplicationClient; /** * If true interface for JAX-RS REST application will be generated. @@ -937,6 +949,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.disableTaggedUnions = disableTaggedUnions; settings.generateReadonlyAndWriteonlyJSDocTags = generateReadonlyAndWriteonlyJSDocTags; settings.ignoreSwaggerAnnotations = ignoreSwaggerAnnotations; + settings.generateJakartaRsApplicationInterface = generateJakartaRsApplicationInterface; + settings.generateJakartaRsApplicationClient = generateJakartaRsApplicationClient; settings.generateJaxrsApplicationInterface = generateJaxrsApplicationInterface; settings.generateJaxrsApplicationClient = generateJaxrsApplicationClient; settings.generateSpringApplicationInterface = generateSpringApplicationInterface;