From e36d75e38fec6a7fc8c4bbf273f9b4662c4749cc Mon Sep 17 00:00:00 2001 From: Ingo Schnabel Date: Mon, 6 Apr 2026 10:22:11 +0200 Subject: [PATCH] Client api generator --- .idea/compiler.xml | 47 +- .idea/encodings.xml | 2 + CLAUDE.md | 2 +- peakup-backend/pom.xml | 19 + peakup-maven/peakup-maven.iml | 6 + peakup-maven/pom.xml | 69 +++ .../maven/PackageInfoGeneratorMojo.java | 84 +++ .../maven/TypeScriptClientGeneratorMojo.java | 542 +++++++++--------- .../com.peakup/peakup-maven/plugin-help.xml | 86 +++ .../target/classes/META-INF/maven/plugin.xml | 112 ++++ .../target/maven-archiver/pom.properties | 3 + .../compile/default-compile/createdFiles.lst | 5 + .../compile/default-compile/inputFiles.lst | 2 + peakup-maven/target/plugin-enhanced.xml | 113 ++++ .../tools/generator/PackageInfoGenerator.java | 79 --- pom.xml | 1 + 16 files changed, 776 insertions(+), 396 deletions(-) create mode 100644 peakup-maven/peakup-maven.iml create mode 100644 peakup-maven/pom.xml create mode 100644 peakup-maven/src/main/java/com/peakup/maven/PackageInfoGeneratorMojo.java rename peakup-tools/src/main/java/com/peakup/tools/generator/TypeScriptClientGenerator.java => peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java (61%) create mode 100644 peakup-maven/target/classes/META-INF/maven/com.peakup/peakup-maven/plugin-help.xml create mode 100644 peakup-maven/target/classes/META-INF/maven/plugin.xml create mode 100644 peakup-maven/target/maven-archiver/pom.properties create mode 100644 peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst create mode 100644 peakup-maven/target/plugin-enhanced.xml delete mode 100644 peakup-tools/src/main/java/com/peakup/tools/generator/PackageInfoGenerator.java diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 2ac53cb..b0de460 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,6 +7,7 @@ + @@ -60,52 +61,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.idea/encodings.xml b/.idea/encodings.xml index ee200f5..c859c83 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -5,6 +5,8 @@ + + diff --git a/CLAUDE.md b/CLAUDE.md index 23b7b84..e9e7019 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,7 +65,7 @@ - `$MVN` = `JAVA_HOME=/home/ingo/.jdks/temurin-21.0.7 mvn -s /home/ingo/.m2/settings_peak.xml` - Backend: `$MVN compile`, `quarkus:dev`, `test -Dtest=ClassName` -- Frontend: `npm run dev`, `npm run build` +- Frontend: `npm run dev`, `npm run build`, `npm run lint` --- diff --git a/peakup-backend/pom.xml b/peakup-backend/pom.xml index 46ec4de..25196e3 100644 --- a/peakup-backend/pom.xml +++ b/peakup-backend/pom.xml @@ -160,6 +160,25 @@ + + com.peakup + peakup-maven + 1.0-SNAPSHOT + + + generate-package-info + + generate-package-info + + + + generate-typescript + + generate-typescript + + + + ${quarkus.platform.group-id} quarkus-maven-plugin diff --git a/peakup-maven/peakup-maven.iml b/peakup-maven/peakup-maven.iml new file mode 100644 index 0000000..9e3449c --- /dev/null +++ b/peakup-maven/peakup-maven.iml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/peakup-maven/pom.xml b/peakup-maven/pom.xml new file mode 100644 index 0000000..b013bad --- /dev/null +++ b/peakup-maven/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + com.peakup + peakup-maven + 1.0-SNAPSHOT + maven-plugin + + + 21 + 21 + UTF-8 + 3.9.9 + + + + + org.apache.maven + maven-plugin-api + ${maven.version} + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.15.1 + provided + + + org.apache.maven + maven-core + ${maven.version} + provided + + + jakarta.ws.rs + jakarta.ws.rs-api + 3.1.0 + + + com.fasterxml.jackson.core + jackson-annotations + 2.18.3 + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.15.1 + + peakup + + + + default-descriptor + process-classes + + + + + + + diff --git a/peakup-maven/src/main/java/com/peakup/maven/PackageInfoGeneratorMojo.java b/peakup-maven/src/main/java/com/peakup/maven/PackageInfoGeneratorMojo.java new file mode 100644 index 0000000..02f9dec --- /dev/null +++ b/peakup-maven/src/main/java/com/peakup/maven/PackageInfoGeneratorMojo.java @@ -0,0 +1,84 @@ +package com.peakup.maven; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +/** + * Generates package-info.java files with the @NonNull annotation + * for all packages that contain Java source files. + */ +@Mojo(name = "generate-package-info", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true) +public class PackageInfoGeneratorMojo extends AbstractMojo { + + private static final String ANNOTATION = "@com.peakup.common.annotation.NonNull"; + + @Parameter(defaultValue = "${project.build.sourceDirectory}", required = true) + private File sourceDirectory; + + @Override + public void execute() throws MojoExecutionException { + if (!sourceDirectory.exists()) { + getLog().info("Source directory does not exist, skipping: " + sourceDirectory); + return; + } + + Path sourceRoot = sourceDirectory.toPath().toAbsolutePath().normalize(); + getLog().info("Generating package-info.java files in: " + sourceRoot); + + try (Stream stream = Files.walk(sourceRoot)) { + stream.filter(Files::isDirectory) + .forEach(directory -> processDirectory(sourceRoot, directory)); + } catch (IOException exception) { + throw new MojoExecutionException("Failed to walk source directory: " + sourceRoot, exception); + } + } + + private void processDirectory(Path sourceRoot, Path currentDir) { + try (Stream files = Files.list(currentDir)) { + boolean hasJavaFiles = files.anyMatch(path -> + path.toString().endsWith(".java") && !path.getFileName().toString().equals("package-info.java") + ); + + if (hasJavaFiles) { + String packageName = determinePackageName(sourceRoot, currentDir); + if (packageName != null) { + createPackageInfo(currentDir, packageName); + } + } + } catch (IOException exception) { + getLog().warn("Failed to process directory " + currentDir + ": " + exception.getMessage()); + } + } + + private String determinePackageName(Path sourceRoot, Path currentDir) { + Path relativePath = sourceRoot.relativize(currentDir); + String packageName = StreamSupport.stream(relativePath.spliterator(), false) + .map(Path::toString) + .collect(Collectors.joining(".")); + return packageName.isEmpty() ? null : packageName; + } + + private void createPackageInfo(Path dir, String packageName) throws IOException { + Path file = dir.resolve("package-info.java"); + + if (Files.exists(file)) { + return; + } + + String content = String.format("%s%npackage %s;%n", ANNOTATION, packageName); + Files.writeString(file, content, StandardOpenOption.CREATE_NEW); + getLog().info("Created: " + file); + } +} diff --git a/peakup-tools/src/main/java/com/peakup/tools/generator/TypeScriptClientGenerator.java b/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java similarity index 61% rename from peakup-tools/src/main/java/com/peakup/tools/generator/TypeScriptClientGenerator.java rename to peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java index a4edc3e..19ff552 100644 --- a/peakup-tools/src/main/java/com/peakup/tools/generator/TypeScriptClientGenerator.java +++ b/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java @@ -1,4 +1,4 @@ -package com.peakup.tools.generator; +package com.peakup.maven; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.ws.rs.DELETE; @@ -17,13 +17,13 @@ import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -37,94 +37,131 @@ import java.util.jar.JarFile; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; + /** * Generates TypeScript REST client and entity types from peakup-backend REST resources. * - *

Scans all classes extending {@code AbstractResource}, reads JAX-RS annotations and - * {@code RestResponse} return types via reflection, then emits into - * {@code peakup-frontend/src/lib/api/}: + *

Scans all compiled classes extending {@code AbstractResource}, reads JAX-RS annotations + * and {@code RestResponse} return types via reflection, then writes into the frontend: *

    *
  • {@code httpClient.ts} — shared fetch helpers
  • - *
  • {@code {Entity}Do.ts} — one interface per domain entity, mirroring the Java class hierarchy
  • + *
  • {@code {Entity}Do.ts} — one interface per domain entity
  • *
  • {@code {Name}.ts} — union type per enum
  • *
  • {@code {Resource}Client.ts} — one client file per JAX-RS resource
  • *
  • {@code hooks.ts} — TanStack Query hooks
  • *
- * - *

Run from the project root (backend must be compiled first): - *

- *   $MVN -f peakup-backend/pom.xml install -DskipTests -q
- *   $MVN -f peakup-tools/pom.xml compile exec:java \
- *     -Dexec.mainClass=com.peakup.tools.generator.TypeScriptClientGenerator
- * 
*/ -public class TypeScriptClientGenerator { +@Mojo( + name = "generate-typescript", + defaultPhase = LifecyclePhase.PROCESS_CLASSES, + requiresDependencyResolution = ResolutionScope.COMPILE, + threadSafe = true +) +public class TypeScriptClientGeneratorMojo extends AbstractMojo { private static final String BACKEND_JAVA_PREFIX = "com.peakup"; - private static final String OUTPUT_DIR_API = "peakup-frontend/src/api"; - private static final String OUTPUT_DIR_TYPES = "peakup-frontend/src/types"; - // ── Main ───────────────────────────────────────────────────────────────── + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; - public static void main(final String[] args) throws Exception { - final Path outputDirApi = Paths.get(System.getProperty("user.dir")).resolve(OUTPUT_DIR_API); - final Path outputDirTypes = Paths.get(System.getProperty("user.dir")).resolve(OUTPUT_DIR_TYPES); - Files.createDirectories(outputDirApi); - Files.createDirectories(outputDirTypes); + @Parameter(defaultValue = "${project.basedir}/../peakup-frontend/src/api", required = true) + private File outputDirApi; - System.out.println("Scanning for REST resources..."); - final List> resourceClasses = findResourceClasses(); - System.out.println("Found: " + resourceClasses.stream() - .map(Class::getSimpleName).collect(Collectors.joining(", "))); + @Parameter(defaultValue = "${project.basedir}/../peakup-frontend/src/types", required = true) + private File outputDirTypes; - final List resources = resourceClasses.stream() - .map(TypeScriptClientGenerator::extractResource) - .filter(r -> !r.endpoints().isEmpty()) - .sorted(Comparator.comparing(ResourceInfo::resourceKey)) - .collect(Collectors.toList()); + @Override + public void execute() throws MojoExecutionException { + final Path apiPath = outputDirApi.toPath(); + final Path typesPath = outputDirTypes.toPath(); - System.out.println("Collecting entity types..."); - final Set> entityTypes = collectEntityTypes(resources); - System.out.println("Types: " + entityTypes.stream() - .map(Class::getSimpleName).collect(Collectors.joining(", "))); - - writeFile(outputDirApi.resolve("httpClient.ts"), generateHttpClient()); - - for (final ResourceInfo resource : resources) { - final String clientFileName = resource.resourceClass().getSimpleName().replace("Resource", "") + "Client.ts"; - writeFile(outputDirApi.resolve(clientFileName), generateResourceClient(resource, entityTypes)); + try { + Files.createDirectories(apiPath); + Files.createDirectories(typesPath); + } catch (IOException exception) { + throw new MojoExecutionException("Failed to create output directories", exception); } - for (final Class entityType : topoSort(entityTypes)) { - final String doFileName = entityType.isEnum() - ? entityType.getSimpleName() + ".ts" - : entityType.getSimpleName() + "Do.ts"; - writeFile(outputDirTypes.resolve(doFileName), generateEntityDoFile(entityType, entityTypes)); + final URLClassLoader classLoader = buildClassLoader(); + + try { + getLog().info("Scanning for REST resources..."); + final List> resourceClasses = findResourceClasses(classLoader); + getLog().info("Found: " + resourceClasses.stream() + .map(Class::getSimpleName).collect(Collectors.joining(", "))); + + final List resources = resourceClasses.stream() + .map(this::extractResource) + .filter(resource -> !resource.endpoints().isEmpty()) + .sorted(Comparator.comparing(ResourceInfo::resourceKey)) + .collect(Collectors.toList()); + + getLog().info("Collecting entity types..."); + final Set> entityTypes = collectEntityTypes(resources); + getLog().info("Types: " + entityTypes.stream() + .map(Class::getSimpleName).collect(Collectors.joining(", "))); + + writeFile(apiPath.resolve("httpClient.ts"), generateHttpClient()); + + for (final ResourceInfo resource : resources) { + final String clientFileName = resource.resourceClass().getSimpleName().replace("Resource", "") + "Client.ts"; + writeFile(apiPath.resolve(clientFileName), generateResourceClient(resource, entityTypes)); + } + + for (final Class entityType : topoSort(entityTypes)) { + final String fileName = entityType.isEnum() + ? entityType.getSimpleName() + ".ts" + : entityType.getSimpleName() + "Do.ts"; + writeFile(typesPath.resolve(fileName), generateEntityDoFile(entityType, entityTypes)); + } + + writeFile(apiPath.resolve("hooks.ts"), generateHooks(resources, entityTypes)); + + getLog().info("Done → api: " + apiPath.toAbsolutePath() + ", types: " + typesPath.toAbsolutePath()); + } catch (Exception exception) { + throw new MojoExecutionException("TypeScript generation failed", exception); + } finally { + try { + classLoader.close(); + } catch (IOException exception) { + getLog().warn("Failed to close classloader: " + exception.getMessage()); + } } - - writeFile(outputDirApi.resolve("hooks.ts"), generateHooks(resources, entityTypes)); - - System.out.println("Done → api: " + outputDirApi.toAbsolutePath() + ", types: " + outputDirTypes.toAbsolutePath()); } - // ── Classpath Scanning ──────────────────────────────────────────────────── + // ── Classpath ───────────────────────────────────────────────────────────── - private static List> findResourceClasses() throws Exception { - final Class abstractResource = Class.forName("com.peakup.common.AbstractResource"); + @SuppressWarnings("unchecked") + private URLClassLoader buildClassLoader() throws MojoExecutionException { + try { + final List elements = (List) project.getCompileClasspathElements(); + final URL[] urls = new URL[elements.size()]; + for (int i = 0; i < elements.size(); i++) { + urls[i] = new File(elements.get(i)).toURI().toURL(); + } + return new URLClassLoader(urls, getClass().getClassLoader()); + } catch (Exception exception) { + throw new MojoExecutionException("Failed to build classpath URLClassLoader", exception); + } + } + + private List> findResourceClasses(final URLClassLoader classLoader) throws Exception { + final Class abstractResource = classLoader.loadClass("com.peakup.common.AbstractResource"); final List> result = new ArrayList<>(); - // Collect classpath entries from the system property and the context classloader (exec:java - // uses an isolated URLClassLoader that is not reflected in java.class.path) - final Set classpathEntries = new LinkedHashSet<>(); - classpathEntries.addAll(Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator))); - collectUrlsFromClassLoader(Thread.currentThread().getContextClassLoader(), classpathEntries); - - for (final String entry : classpathEntries) { - final Path entryPath = Paths.get(entry); - if (Files.isDirectory(entryPath)) { - scanDirectory(entryPath, abstractResource, result); - } else if (entry.endsWith(".jar") && entry.contains("peakup-backend")) { - scanJar(entryPath, abstractResource, result); + for (final URL url : classLoader.getURLs()) { + final File file = new File(url.toURI()); + if (file.isDirectory()) { + scanDirectory(file.toPath(), abstractResource, classLoader, result); + } else if (file.getName().endsWith(".jar") && file.getName().contains("peakup-backend")) { + scanJar(file.toPath(), abstractResource, classLoader, result); } } @@ -132,34 +169,19 @@ public class TypeScriptClientGenerator { return result; } - private static void collectUrlsFromClassLoader(final ClassLoader classLoader, final Set entries) { - if (classLoader == null) return; - try { - final java.lang.reflect.Method getURLs = classLoader.getClass().getMethod("getURLs"); - final java.net.URL[] urls = (java.net.URL[]) getURLs.invoke(classLoader); - for (final java.net.URL url : urls) { - if ("file".equals(url.getProtocol())) { - entries.add(new java.io.File(url.toURI()).getAbsolutePath()); - } - } - } catch (final Exception ignored) { - } - collectUrlsFromClassLoader(classLoader.getParent(), entries); - } - - private static void scanDirectory(final Path root, final Class abstractResource, final List> result) - throws IOException { + private void scanDirectory(final Path root, final Class abstractResource, + final URLClassLoader classLoader, final List> result) throws IOException { try (final Stream walk = Files.walk(root)) { - walk.filter(p -> p.toString().endsWith(".class")) - .map(p -> toClassName(root.relativize(p).toString())) + walk.filter(path -> path.toString().endsWith(".class")) + .map(path -> toClassName(root.relativize(path).toString())) .filter(name -> name.startsWith(BACKEND_JAVA_PREFIX) && !name.contains("$") && !name.endsWith("package-info")) - .forEach(name -> tryLoadResource(name, abstractResource, result)); + .forEach(name -> tryLoadResource(name, abstractResource, classLoader, result)); } } - private static void scanJar(final Path jarPath, final Class abstractResource, final List> result) - throws IOException { + private void scanJar(final Path jarPath, final Class abstractResource, + final URLClassLoader classLoader, final List> result) throws IOException { try (final JarFile jar = new JarFile(jarPath.toFile())) { jar.entries().asIterator().forEachRemaining(entry -> { final String name = entry.getName(); @@ -167,15 +189,15 @@ public class TypeScriptClientGenerator { || name.contains("$") || name.contains("package-info")) { return; } - tryLoadResource(toClassName(name), abstractResource, result); + tryLoadResource(toClassName(name), abstractResource, classLoader, result); }); } } - private static void tryLoadResource(final String className, final Class abstractResource, - final List> result) { + private void tryLoadResource(final String className, final Class abstractResource, + final URLClassLoader classLoader, final List> result) { try { - final Class cls = Class.forName(className); + final Class cls = classLoader.loadClass(className); if (abstractResource.isAssignableFrom(cls) && !cls.equals(abstractResource) && !Modifier.isAbstract(cls.getModifiers())) { @@ -191,15 +213,15 @@ public class TypeScriptClientGenerator { // ── Resource Extraction ─────────────────────────────────────────────────── - private static ResourceInfo extractResource(final Class cls) { - final jakarta.ws.rs.Path pathAnn = cls.getAnnotation(jakarta.ws.rs.Path.class); - final String basePath = pathAnn != null ? pathAnn.value() : ""; + private ResourceInfo extractResource(final Class cls) { + final jakarta.ws.rs.Path pathAnnotation = cls.getAnnotation(jakarta.ws.rs.Path.class); + final String basePath = pathAnnotation != null ? pathAnnotation.value() : ""; final String resourceKey = deriveResourceKey(basePath, cls); final List endpoints = Arrays.stream(cls.getDeclaredMethods()) - .filter(TypeScriptClientGenerator::hasHttpAnnotation) + .filter(TypeScriptClientGeneratorMojo::hasHttpAnnotation) .sorted(Comparator.comparing(Method::getName)) - .map(m -> extractEndpoint(m, basePath)) + .map(method -> extractEndpoint(method, basePath)) .collect(Collectors.toList()); return new ResourceInfo(cls, basePath, resourceKey, endpoints); @@ -207,8 +229,8 @@ public class TypeScriptClientGenerator { private static EndpointInfo extractEndpoint(final Method method, final String basePath) { final String httpMethod = getHttpMethod(method); - final jakarta.ws.rs.Path subPathAnn = method.getAnnotation(jakarta.ws.rs.Path.class); - final String subPath = subPathAnn != null ? subPathAnn.value() : ""; + final jakarta.ws.rs.Path subPathAnnotation = method.getAnnotation(jakarta.ws.rs.Path.class); + final String subPath = subPathAnnotation != null ? subPathAnnotation.value() : ""; final Type javaReturnType = unwrapRestResponse(method.getGenericReturnType()); final String tsReturnType = toTsType(javaReturnType); @@ -217,25 +239,25 @@ public class TypeScriptClientGenerator { final List queryParams = new ArrayList<>(); ParamInfo bodyParam = null; - final Parameter[] params = method.getParameters(); + final java.lang.reflect.Parameter[] params = method.getParameters(); final Annotation[][] allAnnotations = method.getParameterAnnotations(); final Type[] genericTypes = method.getGenericParameterTypes(); for (int i = 0; i < params.length; i++) { - final Annotation[] ann = allAnnotations[i]; + final Annotation[] annotations = allAnnotations[i]; final Type javaType = genericTypes[i]; - final boolean nullable = isNullable(ann); - final DefaultValue defVal = getAnnotation(ann, DefaultValue.class); - final PathParam pathParam = getAnnotation(ann, PathParam.class); - final QueryParam queryParam = getAnnotation(ann, QueryParam.class); + final boolean nullable = isNullable(annotations); + final DefaultValue defaultValue = getAnnotation(annotations, DefaultValue.class); + final PathParam pathParam = getAnnotation(annotations, PathParam.class); + final QueryParam queryParam = getAnnotation(annotations, QueryParam.class); if (pathParam != null) { pathParams.add(new ParamInfo(pathParam.value(), javaType, toTsType(javaType), false, null)); } else if (queryParam != null) { - final boolean optional = nullable || defVal != null; + final boolean optional = nullable || defaultValue != null; queryParams.add(new ParamInfo(queryParam.value(), javaType, toTsType(javaType), optional, - defVal != null ? defVal.value() : null)); - } else if (!isJaxRsSpecialParam(ann)) { + defaultValue != null ? defaultValue.value() : null)); + } else if (!isJaxRsSpecialParam(annotations)) { bodyParam = new ParamInfo("body", javaType, toTsType(javaType), false, null); } } @@ -245,11 +267,11 @@ public class TypeScriptClientGenerator { } private static Type unwrapRestResponse(final Type type) { - if (type instanceof final ParameterizedType pt) { - final Type rawType = pt.getRawType(); + if (type instanceof final ParameterizedType parameterizedType) { + final Type rawType = parameterizedType.getRawType(); if (rawType instanceof final Class rawClass && rawClass.getSimpleName().equals("RestResponse")) { - final Type[] args = pt.getActualTypeArguments(); + final Type[] args = parameterizedType.getActualTypeArguments(); if (args.length == 1) return args[0]; } } @@ -299,7 +321,6 @@ public class TypeScriptClientGenerator { } } - // Also include abstract com.peakup.* superclasses for DO hierarchy generation for (final Class cls : new ArrayList<>(visited)) { if (!cls.isEnum()) { addAbstractSuperclasses(cls, visited); @@ -322,11 +343,11 @@ public class TypeScriptClientGenerator { private static void seedFromType(final Type type, final Queue> queue, final Set> visited) { if (type instanceof final Class cls) { if (isDomainType(cls) && !visited.contains(cls)) queue.add(cls); - } else if (type instanceof final ParameterizedType pt) { - seedFromType(pt.getRawType(), queue, visited); - for (final Type arg : pt.getActualTypeArguments()) seedFromType(arg, queue, visited); - } else if (type instanceof final GenericArrayType gat) { - seedFromType(gat.getGenericComponentType(), queue, visited); + } else if (type instanceof final ParameterizedType parameterizedType) { + seedFromType(parameterizedType.getRawType(), queue, visited); + for (final Type arg : parameterizedType.getActualTypeArguments()) seedFromType(arg, queue, visited); + } else if (type instanceof final GenericArrayType genericArrayType) { + seedFromType(genericArrayType.getGenericComponentType(), queue, visited); } } @@ -361,21 +382,21 @@ public class TypeScriptClientGenerator { return "unknown"; } - if (type instanceof final ParameterizedType pt) { - final Type rawType = pt.getRawType(); + if (type instanceof final ParameterizedType parameterizedType) { + final Type rawType = parameterizedType.getRawType(); if (rawType instanceof final Class rawClass) { final String rawName = rawClass.getName(); if (rawName.equals("java.util.List") || rawName.equals("java.util.Collection") || rawName.equals("java.util.Set")) { - final Type[] args = pt.getActualTypeArguments(); + final Type[] args = parameterizedType.getActualTypeArguments(); if (args.length == 1) return toTsType(args[0]) + "[]"; } } return "unknown"; } - if (type instanceof final GenericArrayType gat) { - return toTsType(gat.getGenericComponentType()) + "[]"; + if (type instanceof final GenericArrayType genericArrayType) { + return toTsType(genericArrayType.getGenericComponentType()) + "[]"; } return "unknown"; @@ -384,76 +405,67 @@ public class TypeScriptClientGenerator { // ── httpClient.ts ───────────────────────────────────────────────────────── private static String generateHttpClient() { - final StringBuilder sb = new StringBuilder(); - sb.append("// GENERATED — do not edit manually.\n\n"); - sb.append("const BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8111';\n\n"); - - sb.append("export function buildQuery(params: Record): string {\n"); - sb.append(" const p = new URLSearchParams();\n"); - sb.append(" for (const [key, val] of Object.entries(params)) {\n"); - sb.append(" if (val !== undefined) p.set(key, String(val));\n"); - sb.append(" }\n"); - sb.append(" const query = p.toString();\n"); - sb.append(" return query ? `?${query}` : '';\n"); - sb.append("}\n\n"); - - sb.append("export async function get(path: string): Promise {\n"); - sb.append(" const res = await fetch(`${BASE_URL}${path}`);\n"); - sb.append(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n"); - sb.append(" return res.json() as Promise;\n"); - sb.append("}\n\n"); - - sb.append("export async function post(path: string, body?: unknown): Promise {\n"); - sb.append(" const res = await fetch(`${BASE_URL}${path}`, {\n"); - sb.append(" method: 'POST',\n"); - sb.append(" headers: { 'Content-Type': 'application/json' },\n"); - sb.append(" body: body !== undefined ? JSON.stringify(body) : undefined,\n"); - sb.append(" });\n"); - sb.append(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n"); - sb.append(" if (res.status === 204) return undefined as T;\n"); - sb.append(" return res.json() as Promise;\n"); - sb.append("}\n\n"); - - sb.append("export async function put(path: string, body?: unknown): Promise {\n"); - sb.append(" const res = await fetch(`${BASE_URL}${path}`, {\n"); - sb.append(" method: 'PUT',\n"); - sb.append(" headers: { 'Content-Type': 'application/json' },\n"); - sb.append(" body: body !== undefined ? JSON.stringify(body) : undefined,\n"); - sb.append(" });\n"); - sb.append(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n"); - sb.append(" if (res.status === 204) return undefined as T;\n"); - sb.append(" return res.json() as Promise;\n"); - sb.append("}\n\n"); - - sb.append("export async function del(path: string): Promise {\n"); - sb.append(" const res = await fetch(`${BASE_URL}${path}`, { method: 'DELETE' });\n"); - sb.append(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n"); - sb.append(" if (res.status === 204) return undefined as T;\n"); - sb.append(" return res.json() as Promise;\n"); - sb.append("}\n"); - - return sb.toString(); + return "// GENERATED — do not edit manually.\n\n" + + "const BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8111';\n\n" + + "export function buildQuery(params: Record): string {\n" + + " const p = new URLSearchParams();\n" + + " for (const [key, val] of Object.entries(params)) {\n" + + " if (val !== undefined) p.set(key, String(val));\n" + + " }\n" + + " const query = p.toString();\n" + + " return query ? `?${query}` : '';\n" + + "}\n\n" + + "export async function get(path: string): Promise {\n" + + " const res = await fetch(`${BASE_URL}${path}`);\n" + + " if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n" + + " return res.json() as Promise;\n" + + "}\n\n" + + "export async function post(path: string, body?: unknown): Promise {\n" + + " const res = await fetch(`${BASE_URL}${path}`, {\n" + + " method: 'POST',\n" + + " headers: { 'Content-Type': 'application/json' },\n" + + " body: body !== undefined ? JSON.stringify(body) : undefined,\n" + + " });\n" + + " if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n" + + " if (res.status === 204) return undefined as T;\n" + + " return res.json() as Promise;\n" + + "}\n\n" + + "export async function put(path: string, body?: unknown): Promise {\n" + + " const res = await fetch(`${BASE_URL}${path}`, {\n" + + " method: 'PUT',\n" + + " headers: { 'Content-Type': 'application/json' },\n" + + " body: body !== undefined ? JSON.stringify(body) : undefined,\n" + + " });\n" + + " if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n" + + " if (res.status === 204) return undefined as T;\n" + + " return res.json() as Promise;\n" + + "}\n\n" + + "export async function del(path: string): Promise {\n" + + " const res = await fetch(`${BASE_URL}${path}`, { method: 'DELETE' });\n" + + " if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n" + + " if (res.status === 204) return undefined as T;\n" + + " return res.json() as Promise;\n" + + "}\n"; } // ── {Entity}Do.ts / {Enum}.ts ───────────────────────────────────────────── private static String generateEntityDoFile(final Class cls, final Set> allTypes) { - final StringBuilder sb = new StringBuilder(); - sb.append("// GENERATED — do not edit manually.\n\n"); + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("// GENERATED — do not edit manually.\n\n"); if (cls.isEnum()) { final Object[] constants = cls.getEnumConstants(); - sb.append("export type ").append(cls.getSimpleName()).append(" =\n"); + stringBuilder.append("export type ").append(cls.getSimpleName()).append(" =\n"); for (int i = 0; i < constants.length; i++) { - sb.append(" | '").append(((Enum) constants[i]).name()).append("'"); - sb.append(i < constants.length - 1 ? "\n" : ";\n"); + stringBuilder.append(" | '").append(((Enum) constants[i]).name()).append("'"); + stringBuilder.append(i < constants.length - 1 ? "\n" : ";\n"); } - return sb.toString(); + return stringBuilder.toString(); } final Set importStatements = new LinkedHashSet<>(); - // Resolve parent for extends clause final Class superClass = cls.getSuperclass(); final String extendsClause; if (superClass != null && superClass != Object.class && allTypes.contains(superClass)) { @@ -464,10 +476,9 @@ public class TypeScriptClientGenerator { extendsClause = ""; } - // Own declared fields only — each parent gets its own DO file final List ownFields = Arrays.stream(cls.getDeclaredFields()) - .filter(f -> !Modifier.isStatic(f.getModifiers())) - .filter(f -> !isJsonIgnored(f)) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .filter(field -> !isJsonIgnored(field)) .collect(Collectors.toList()); for (final Field field : ownFields) { @@ -476,22 +487,22 @@ public class TypeScriptClientGenerator { if (!importStatements.isEmpty()) { for (final String importStatement : importStatements) { - sb.append(importStatement).append("\n"); + stringBuilder.append(importStatement).append("\n"); } - sb.append("\n"); + stringBuilder.append("\n"); } - sb.append("export interface ").append(cls.getSimpleName()).append("Do").append(extendsClause).append(" {\n"); + stringBuilder.append("export interface ").append(cls.getSimpleName()).append("Do").append(extendsClause).append(" {\n"); for (final Field field : ownFields) { - final boolean nullable = isNullableField(field); + final boolean nullable = isNullable(field.getAnnotations()); final String tsType = toTsType(field.getGenericType()); - sb.append(" ").append(field.getName()).append(": ").append(tsType); - if (nullable) sb.append(" | null"); - sb.append(";\n"); + stringBuilder.append(" ").append(field.getName()).append(": ").append(tsType); + if (nullable) stringBuilder.append(" | null"); + stringBuilder.append(";\n"); } - sb.append("}\n"); + stringBuilder.append("}\n"); - return sb.toString(); + return stringBuilder.toString(); } private static void collectImportsFromType(final Type type, final Set> allTypes, @@ -501,13 +512,13 @@ public class TypeScriptClientGenerator { final String tsName = cls.isEnum() ? cls.getSimpleName() : cls.getSimpleName() + "Do"; importStatements.add("import type { " + tsName + " } from '" + typeImportPrefix + tsName + "';"); } - } else if (type instanceof final ParameterizedType pt) { - collectImportsFromType(pt.getRawType(), allTypes, importStatements, typeImportPrefix); - for (final Type arg : pt.getActualTypeArguments()) { + } else if (type instanceof final ParameterizedType parameterizedType) { + collectImportsFromType(parameterizedType.getRawType(), allTypes, importStatements, typeImportPrefix); + for (final Type arg : parameterizedType.getActualTypeArguments()) { collectImportsFromType(arg, allTypes, importStatements, typeImportPrefix); } - } else if (type instanceof final GenericArrayType gat) { - collectImportsFromType(gat.getGenericComponentType(), allTypes, importStatements, typeImportPrefix); + } else if (type instanceof final GenericArrayType genericArrayType) { + collectImportsFromType(genericArrayType.getGenericComponentType(), allTypes, importStatements, typeImportPrefix); } } @@ -522,23 +533,23 @@ public class TypeScriptClientGenerator { } } - final StringBuilder sb = new StringBuilder(); - sb.append("// GENERATED — do not edit manually.\n\n"); + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("// GENERATED — do not edit manually.\n\n"); final Set httpHelpers = resolveHttpHelpers(resource.endpoints()); - sb.append("import { ").append(String.join(", ", httpHelpers)).append(" } from './httpClient';\n"); + stringBuilder.append("import { ").append(String.join(", ", httpHelpers)).append(" } from './httpClient';\n"); for (final String typeImport : typeImportStatements) { - sb.append(typeImport).append("\n"); + stringBuilder.append(typeImport).append("\n"); } - sb.append("\n"); + stringBuilder.append("\n"); - sb.append("export const ").append(resource.resourceKey()).append(" = {\n"); + stringBuilder.append("export const ").append(resource.resourceKey()).append(" = {\n"); for (final EndpointInfo endpoint : resource.endpoints()) { - sb.append(generateClientFunction(endpoint, resource.basePath())); + stringBuilder.append(generateClientFunction(endpoint, resource.basePath())); } - sb.append("};\n"); + stringBuilder.append("};\n"); - return sb.toString(); + return stringBuilder.toString(); } private static Set resolveHttpHelpers(final List endpoints) { @@ -580,26 +591,24 @@ public class TypeScriptClientGenerator { private static String generateClientFunction(final EndpointInfo endpoint, final String basePath) { final List paramList = new ArrayList<>(); - for (final ParamInfo p : endpoint.pathParams()) { - paramList.add(p.name() + ": " + p.tsType()); + for (final ParamInfo paramInfo : endpoint.pathParams()) { + paramList.add(paramInfo.name() + ": " + paramInfo.tsType()); } final List queryParams = endpoint.queryParams(); for (int i = 0; i < queryParams.size(); i++) { - final ParamInfo p = queryParams.get(i); - if (p.optional() && p.defaultValue() != null) { - paramList.add(p.name() + " = " + tsDefaultValue(p.defaultValue(), p.tsType())); - } else if (p.optional()) { - // Use `name: type | undefined` instead of `name?: type` when a required param follows, - // because TypeScript forbids required params after optional params (TS1016). + final ParamInfo paramInfo = queryParams.get(i); + if (paramInfo.optional() && paramInfo.defaultValue() != null) { + paramList.add(paramInfo.name() + " = " + tsDefaultValue(paramInfo.defaultValue(), paramInfo.tsType())); + } else if (paramInfo.optional()) { final boolean requiredParamFollows = endpoint.bodyParam() != null || queryParams.subList(i + 1, queryParams.size()).stream().anyMatch(q -> !q.optional()); if (requiredParamFollows) { - paramList.add(p.name() + ": " + p.tsType() + " | undefined"); + paramList.add(paramInfo.name() + ": " + paramInfo.tsType() + " | undefined"); } else { - paramList.add(p.name() + "?: " + p.tsType()); + paramList.add(paramInfo.name() + "?: " + paramInfo.tsType()); } } else { - paramList.add(p.name() + ": " + p.tsType()); + paramList.add(paramInfo.name() + ": " + paramInfo.tsType()); } } if (endpoint.bodyParam() != null) { @@ -616,9 +625,9 @@ public class TypeScriptClientGenerator { final String urlExpr; if (hasQuery) { - final String qpNames = endpoint.queryParams().stream() + final String queryParamNames = endpoint.queryParams().stream() .map(ParamInfo::name).collect(Collectors.joining(", ")); - urlExpr = "`" + tsPath + "${buildQuery({ " + qpNames + " })}`"; + urlExpr = "`" + tsPath + "${buildQuery({ " + queryParamNames + " })}`"; } else if (hasTemplate) { urlExpr = "`" + tsPath + "`"; } else { @@ -642,9 +651,6 @@ public class TypeScriptClientGenerator { // ── hooks.ts ────────────────────────────────────────────────────────────── private static String generateHooks(final List resources, final Set> allTypes) { - // Only collect type names from mutation endpoints: query hooks infer their return type - // from queryFn, so they never reference Do types by name in the generated code. - // Including GET return types causes unused-import warnings (e.g. AthleteDo). final Set neededTsTypeNames = new LinkedHashSet<>(); for (final ResourceInfo resource : resources) { for (final EndpointInfo endpoint : resource.endpoints()) { @@ -657,13 +663,13 @@ public class TypeScriptClientGenerator { } } - final StringBuilder sb = new StringBuilder(); - sb.append("// GENERATED — do not edit manually.\n\n"); - sb.append("import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n"); + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("// GENERATED — do not edit manually.\n\n"); + stringBuilder.append("import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n"); for (final ResourceInfo resource : resources) { final String clientFile = "./" + resource.resourceClass().getSimpleName().replace("Resource", "") + "Client"; - sb.append("import { ").append(resource.resourceKey()).append(" } from '").append(clientFile).append("';\n"); + stringBuilder.append("import { ").append(resource.resourceKey()).append(" } from '").append(clientFile).append("';\n"); } final Set typeImports = new LinkedHashSet<>(); @@ -671,45 +677,45 @@ public class TypeScriptClientGenerator { collectImportsFromTsTypeName(tsTypeName, allTypes, typeImports, "../types/"); } for (final String typeImport : typeImports) { - sb.append(typeImport).append("\n"); + stringBuilder.append(typeImport).append("\n"); } - sb.append("\n"); + stringBuilder.append("\n"); - sb.append("export const queryKeys = {\n"); + stringBuilder.append("export const queryKeys = {\n"); for (final ResourceInfo resource : resources) { - final List gets = resource.endpoints().stream() - .filter(e -> e.httpMethod().equals("GET")).collect(Collectors.toList()); - if (gets.isEmpty()) continue; - sb.append(" ").append(resource.resourceKey()).append(": {\n"); - for (final EndpointInfo endpoint : gets) { + final List getEndpoints = resource.endpoints().stream() + .filter(endpoint -> endpoint.httpMethod().equals("GET")).collect(Collectors.toList()); + if (getEndpoints.isEmpty()) continue; + stringBuilder.append(" ").append(resource.resourceKey()).append(": {\n"); + for (final EndpointInfo endpoint : getEndpoints) { final List allParams = new ArrayList<>(); allParams.addAll(endpoint.pathParams()); allParams.addAll(endpoint.queryParams()); final String paramSig = allParams.stream() - .map(p -> p.name() + (p.optional() ? "?" : "") + ": " + p.tsType()) + .map(paramInfo -> paramInfo.name() + (paramInfo.optional() ? "?" : "") + ": " + paramInfo.tsType()) .collect(Collectors.joining(", ")); final String keyItems = allParams.stream().map(ParamInfo::name).collect(Collectors.joining(", ")); final String keyArray = "'" + resource.resourceKey() + "', '" + endpoint.functionName() + "'" + (keyItems.isEmpty() ? "" : ", " + keyItems); - sb.append(" ").append(endpoint.functionName()).append(": (") + stringBuilder.append(" ").append(endpoint.functionName()).append(": (") .append(paramSig).append(") => [").append(keyArray).append("] as const,\n"); } - sb.append(" },\n"); + stringBuilder.append(" },\n"); } - sb.append("};\n\n"); + stringBuilder.append("};\n\n"); for (final ResourceInfo resource : resources) { - sb.append("// ── ").append(resource.resourceKey()).append(" ────────────────────────────────────\n\n"); + stringBuilder.append("// ── ").append(resource.resourceKey()).append(" ────────────────────────────────────\n\n"); for (final EndpointInfo endpoint : resource.endpoints()) { if (endpoint.httpMethod().equals("GET")) { - sb.append(generateQueryHook(endpoint, resource)); + stringBuilder.append(generateQueryHook(endpoint, resource)); } else { - sb.append(generateMutationHook(endpoint, resource)); + stringBuilder.append(generateMutationHook(endpoint, resource)); } } } - return sb.toString(); + return stringBuilder.toString(); } private static String generateQueryHook(final EndpointInfo endpoint, final ResourceInfo resource) { @@ -718,13 +724,13 @@ public class TypeScriptClientGenerator { allParams.addAll(endpoint.pathParams()); allParams.addAll(endpoint.queryParams()); - final String paramSig = allParams.stream().map(p -> { - if (p.optional() && p.defaultValue() != null) { - return p.name() + " = " + tsDefaultValue(p.defaultValue(), p.tsType()); - } else if (p.optional()) { - return p.name() + "?: " + p.tsType(); + final String paramSig = allParams.stream().map(paramInfo -> { + if (paramInfo.optional() && paramInfo.defaultValue() != null) { + return paramInfo.name() + " = " + tsDefaultValue(paramInfo.defaultValue(), paramInfo.tsType()); + } else if (paramInfo.optional()) { + return paramInfo.name() + "?: " + paramInfo.tsType(); } - return p.name() + ": " + p.tsType(); + return paramInfo.name() + ": " + paramInfo.tsType(); }).collect(Collectors.joining(", ")); final String queryKeyArgs = allParams.stream().map(ParamInfo::name).collect(Collectors.joining(", ")); @@ -764,7 +770,7 @@ public class TypeScriptClientGenerator { destructure = single.name(); } else { variablesType = "{ " + mutationParams.stream() - .map(p -> p.name() + (p.optional() ? "?" : "") + ": " + p.tsType()) + .map(paramInfo -> paramInfo.name() + (paramInfo.optional() ? "?" : "") + ": " + paramInfo.tsType()) .collect(Collectors.joining("; ")) + " }"; destructure = "{ " + mutationParams.stream() .map(ParamInfo::name).collect(Collectors.joining(", ")) + " }"; @@ -818,24 +824,20 @@ public class TypeScriptClientGenerator { } private static boolean isJsonIgnored(final Field field) { - final JsonIgnore ann = field.getAnnotation(JsonIgnore.class); - return ann != null && ann.value(); + final JsonIgnore annotation = field.getAnnotation(JsonIgnore.class); + return annotation != null && annotation.value(); } private static boolean isNullable(final Annotation[] annotations) { - for (final Annotation ann : annotations) { - if (ann.annotationType().getSimpleName().equals("Nullable")) return true; + for (final Annotation annotation : annotations) { + if (annotation.annotationType().getSimpleName().equals("Nullable")) return true; } return false; } - private static boolean isNullableField(final Field field) { - return isNullable(field.getAnnotations()); - } - private static boolean isJaxRsSpecialParam(final Annotation[] annotations) { - for (final Annotation ann : annotations) { - final String name = ann.annotationType().getSimpleName(); + for (final Annotation annotation : annotations) { + final String name = annotation.annotationType().getSimpleName(); if (name.equals("Context") || name.equals("HeaderParam") || name.equals("CookieParam") || name.equals("FormParam") || name.equals("BeanParam") || name.equals("Suspended")) { @@ -847,8 +849,8 @@ public class TypeScriptClientGenerator { @SuppressWarnings("unchecked") private static A getAnnotation(final Annotation[] annotations, final Class type) { - for (final Annotation ann : annotations) { - if (type.isInstance(ann)) return (A) ann; + for (final Annotation annotation : annotations) { + if (type.isInstance(annotation)) return (A) annotation; } return null; } @@ -863,8 +865,8 @@ public class TypeScriptClientGenerator { private static String buildApiCallArgs(final EndpointInfo endpoint) { final List args = new ArrayList<>(); - endpoint.pathParams().forEach(p -> args.add(p.name())); - endpoint.queryParams().forEach(p -> args.add(p.name())); + endpoint.pathParams().forEach(paramInfo -> args.add(paramInfo.name())); + endpoint.queryParams().forEach(paramInfo -> args.add(paramInfo.name())); if (endpoint.bodyParam() != null) args.add(endpoint.bodyParam().name()); return String.join(", ", args); } @@ -881,18 +883,18 @@ public class TypeScriptClientGenerator { private static String kebabToCamel(final String kebab) { final String[] parts = kebab.split("-"); - final StringBuilder sb = new StringBuilder(parts[0]); + final StringBuilder stringBuilder = new StringBuilder(parts[0]); for (int i = 1; i < parts.length; i++) { if (!parts[i].isEmpty()) { - sb.append(Character.toUpperCase(parts[i].charAt(0))).append(parts[i].substring(1)); + stringBuilder.append(Character.toUpperCase(parts[i].charAt(0))).append(parts[i].substring(1)); } } - return sb.toString(); + return stringBuilder.toString(); } - private static String capitalize(final String s) { - if (s == null || s.isEmpty()) return s; - return Character.toUpperCase(s.charAt(0)) + s.substring(1); + private static String capitalize(final String value) { + if (value == null || value.isEmpty()) return value; + return Character.toUpperCase(value.charAt(0)) + value.substring(1); } private static String pathToTsTemplate(final String path) { @@ -919,7 +921,7 @@ public class TypeScriptClientGenerator { try { Double.parseDouble(javaDefault); return javaDefault; - } catch (final NumberFormatException e) { + } catch (final NumberFormatException exception) { return "0"; } } @@ -929,9 +931,9 @@ public class TypeScriptClientGenerator { return "'" + javaDefault + "'"; } - private static void writeFile(final Path path, final String content) throws IOException { + private void writeFile(final Path path, final String content) throws IOException { Files.writeString(path, content, StandardCharsets.UTF_8); - System.out.println("Written: " + path.getFileName()); + getLog().info("Written: " + path.getFileName()); } // ── Records ─────────────────────────────────────────────────────────────── diff --git a/peakup-maven/target/classes/META-INF/maven/com.peakup/peakup-maven/plugin-help.xml b/peakup-maven/target/classes/META-INF/maven/com.peakup/peakup-maven/plugin-help.xml new file mode 100644 index 0000000..6a63858 --- /dev/null +++ b/peakup-maven/target/classes/META-INF/maven/com.peakup/peakup-maven/plugin-help.xml @@ -0,0 +1,86 @@ + + + + + + peakup-maven + + com.peakup + peakup-maven + 1.0-SNAPSHOT + peakup + + + generate-package-info + Generates package-info.java files with the @NonNull annotation for all packages that contain Java source files. + false + true + false + false + false + true + generate-sources + com.peakup.maven.PackageInfoGeneratorMojo + java + per-lookup + once-per-session + true + + + sourceDirectory + java.io.File + true + true + + + + + + + + + generate-typescript + Generates TypeScript REST client and entity types from peakup-backend REST resources. +Scans all compiled classes extending AbstractResource, reads JAX-RS annotations and RestResponse<T> return types via reflection, then writes into the frontend: + +* httpClient.ts — shared fetch helpers +* {EntityDo.ts} — one interface per domain entity +* {Name.ts} — union type per enum +* {ResourceClient.ts} — one client file per JAX-RS resource +* hooks.ts — TanStack Query hooks + compile + false + true + false + false + false + true + process-classes + com.peakup.maven.TypeScriptClientGeneratorMojo + java + per-lookup + once-per-session + true + + + outputDirApi + java.io.File + true + true + + + + outputDirTypes + java.io.File + true + true + + + + + + + + + + \ No newline at end of file diff --git a/peakup-maven/target/classes/META-INF/maven/plugin.xml b/peakup-maven/target/classes/META-INF/maven/plugin.xml new file mode 100644 index 0000000..cb960bf --- /dev/null +++ b/peakup-maven/target/classes/META-INF/maven/plugin.xml @@ -0,0 +1,112 @@ + + + + + + peakup-maven + + com.peakup + peakup-maven + 1.0-SNAPSHOT + peakup + false + true + 21 + 3.9.9 + + + generate-package-info + Generates package-info.java files with the @NonNull annotation for all packages that contain Java source files. + false + true + false + false + false + true + generate-sources + com.peakup.maven.PackageInfoGeneratorMojo + java + per-lookup + once-per-session + true + + + sourceDirectory + java.io.File + true + true + + + + + + + + + generate-typescript + Generates TypeScript REST client and entity types from peakup-backend REST resources. +Scans all compiled classes extending AbstractResource, reads JAX-RS annotations and RestResponse<T> return types via reflection, then writes into the frontend: + +* httpClient.ts — shared fetch helpers +* {EntityDo.ts} — one interface per domain entity +* {Name.ts} — union type per enum +* {ResourceClient.ts} — one client file per JAX-RS resource +* hooks.ts — TanStack Query hooks + compile + false + true + false + false + false + true + process-classes + com.peakup.maven.TypeScriptClientGeneratorMojo + java + per-lookup + once-per-session + true + + + outputDirApi + java.io.File + true + true + + + + outputDirTypes + java.io.File + true + true + + + + project + org.apache.maven.project.MavenProject + true + false + + + + + + + + + + + + + jakarta.ws.rs + jakarta.ws.rs-api + jar + 3.1.0 + + + com.fasterxml.jackson.core + jackson-annotations + jar + 2.18.3 + + + \ No newline at end of file diff --git a/peakup-maven/target/maven-archiver/pom.properties b/peakup-maven/target/maven-archiver/pom.properties new file mode 100644 index 0000000..f2798b9 --- /dev/null +++ b/peakup-maven/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=peakup-maven +groupId=com.peakup +version=1.0-SNAPSHOT diff --git a/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..4cb4284 --- /dev/null +++ b/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,5 @@ +com/peakup/maven/TypeScriptClientGeneratorMojo.class +com/peakup/maven/TypeScriptClientGeneratorMojo$ResourceInfo.class +com/peakup/maven/PackageInfoGeneratorMojo.class +com/peakup/maven/TypeScriptClientGeneratorMojo$ParamInfo.class +com/peakup/maven/TypeScriptClientGeneratorMojo$EndpointInfo.class diff --git a/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..4242a08 --- /dev/null +++ b/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,2 @@ +/home/ingo/deve/peakUp/peakup-maven/src/main/java/com/peakup/maven/PackageInfoGeneratorMojo.java +/home/ingo/deve/peakUp/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java diff --git a/peakup-maven/target/plugin-enhanced.xml b/peakup-maven/target/plugin-enhanced.xml new file mode 100644 index 0000000..7525b40 --- /dev/null +++ b/peakup-maven/target/plugin-enhanced.xml @@ -0,0 +1,113 @@ + + + + + + peakup-maven + + com.peakup + peakup-maven + 1.0-SNAPSHOT + peakup + false + true + 21 + 3.9.9 + + + generate-package-info + Generates package-info.java files with the @NonNull annotation for all packages that contain Java source files. + false + true + false + false + false + true + generate-sources + com.peakup.maven.PackageInfoGeneratorMojo + java + per-lookup + once-per-session + true + + + sourceDirectory + java.io.File + true + true + + + + + + + + + generate-typescript + Generates TypeScript REST client and entity types from peakup-backend REST resources. +<p>Scans all compiled classes extending <code>AbstractResource</code>, reads JAX-RS annotations and <code>RestResponse&lt;T&gt;</code> return types via reflection, then writes into the frontend:</p> +<ul> + <li><code>httpClient.ts</code> — shared fetch helpers</li> + <li><code>{Entity</code>Do.ts} — one interface per domain entity</li> + <li><code>{Name</code>.ts} — union type per enum</li> + <li><code>{Resource</code>Client.ts} — one client file per JAX-RS resource</li> + <li><code>hooks.ts</code> — TanStack Query hooks</li> +</ul> + compile + false + true + false + false + false + true + process-classes + com.peakup.maven.TypeScriptClientGeneratorMojo + java + per-lookup + once-per-session + true + + + outputDirApi + java.io.File + true + true + + + + outputDirTypes + java.io.File + true + true + + + + project + org.apache.maven.project.MavenProject + true + false + + + + + + + + + + + + + jakarta.ws.rs + jakarta.ws.rs-api + jar + 3.1.0 + + + com.fasterxml.jackson.core + jackson-annotations + jar + 2.18.3 + + + \ No newline at end of file diff --git a/peakup-tools/src/main/java/com/peakup/tools/generator/PackageInfoGenerator.java b/peakup-tools/src/main/java/com/peakup/tools/generator/PackageInfoGenerator.java deleted file mode 100644 index d50446d..0000000 --- a/peakup-tools/src/main/java/com/peakup/tools/generator/PackageInfoGenerator.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.peakup.tools.generator; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -/** - * Utility zur automatischen Generierung von package-info.java Dateien - * in bestehenden Package-Strukturen. - */ -public class PackageInfoGenerator { - - private static final String SRC_BASE = "peakup-backend/src/main/java"; // Pfad anpassen falls nötig - private static final String PACKAGE_PREFIX = "com.peakup"; - private static final String ANNOTATION = "@com.peakup.common.annotation.NonNull"; - - public static void main(String[] args) { - Path projectRoot = Paths.get(System.getProperty("user.dir")); - Path sourceRoot = projectRoot.resolve(SRC_BASE).toAbsolutePath().normalize(); - - if (!Files.exists(sourceRoot)) { - System.err.println("Source Root nicht gefunden: " + sourceRoot); - return; - } - - try (Stream stream = Files.walk(sourceRoot)) { - stream.filter(Files::isDirectory) - .forEach(dir -> processDirectory(sourceRoot, dir)); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void processDirectory(Path sourceRoot, Path currentDir) { - try (Stream files = Files.list(currentDir)) { - // Nur Verzeichnisse mit .java Dateien (außer package-info selbst) - boolean hasJavaFiles = files.anyMatch(p -> - p.toString().endsWith(".java") && !p.getFileName().toString().equals("package-info.java") - ); - - if (hasJavaFiles) { - String packageName = determinePackageName(sourceRoot, currentDir); - createPackageInfo(currentDir, packageName); - } - } catch (IOException e) { - System.err.println("Fehler in " + currentDir + ": " + e.getMessage()); - } - } - - private static String determinePackageName(Path sourceRoot, Path currentDir) { - // Erzeugt den relativen Pfad von src/main/java zum aktuellen Ordner - Path relativePath = sourceRoot.relativize(currentDir); - - // Verbindet die Pfad-Elemente mit Punkten (plattformunabhängig) - String pkg = StreamSupport.stream(relativePath.spliterator(), false) - .map(Path::toString) - .collect(Collectors.joining(".")); - - return pkg.isEmpty() ? null : pkg; - } - - private static void createPackageInfo(Path dir, String packageName) throws IOException { - Path file = dir.resolve("package-info.java"); - - if (Files.exists(file)) { - // System.out.println("Überspringe (existiert bereits): " + file); - return; - } - - String content = String.format("%s\npackage %s;", ANNOTATION, packageName); - Files.writeString(file, content, StandardOpenOption.CREATE_NEW); - System.out.println("Erstellt: " + file.toAbsolutePath()); - } -} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9507b3b..bf33e66 100644 --- a/pom.xml +++ b/pom.xml @@ -10,6 +10,7 @@ pom + peakup-maven peakup-backend peakup-tools