Client api generator
This commit is contained in:
47
.idea/compiler.xml
generated
47
.idea/compiler.xml
generated
@@ -7,6 +7,7 @@
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="peakup-maven" />
|
||||
<module name="peakup-tools" />
|
||||
</profile>
|
||||
<profile name="Annotation profile for peakup-parent" enabled="true">
|
||||
@@ -60,52 +61,6 @@
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/antlr/antlr4-runtime/4.13.2/antlr4-runtime-4.13.2.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/net/bytebuddy/byte-buddy/1.17.8/byte-buddy-1.17.8.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/jboss/logging/jboss-logging/3.6.1.Final/jboss-logging-3.6.1.Final.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/mapstruct/mapstruct-processor/1.6.3/mapstruct-processor-1.6.3.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/errorprone/error_prone_core/2.37.0/error_prone_core-2.37.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/errorprone/error_prone_annotation/2.37.0/error_prone_annotation-2.37.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/errorprone/error_prone_check_api/2.37.0/error_prone_check_api-2.37.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/io/github/java-diff-utils/java-diff-utils/4.12/java-diff-utils-4.12.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/github/kevinstern/software-and-algorithms/1.0/software-and-algorithms-1.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/github/ben-manes/caffeine/caffeine/3.0.5/caffeine-3.0.5.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/googlejavaformat/google-java-format/1.24.0/google-java-format-1.24.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/pcollections/pcollections/4.0.1/pcollections-4.0.1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/guava/guava/32.1.1-jre/guava-32.1.1-jre.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/checkerframework/checker-qual/3.33.0/checker-qual-3.33.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/j2objc/j2objc-annotations/2.8/j2objc-annotations-2.8.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/auto/auto-common/1.2.2/auto-common-1.2.2.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/io/github/eisop/dataflow-errorprone/3.41.0-eisop1/dataflow-errorprone-3.41.0-eisop1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/auto/value/auto-value-annotations/1.9/auto-value-annotations-1.9.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/errorprone/error_prone_annotations/2.37.0/error_prone_annotations-2.37.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/protobuf/protobuf-java/3.25.5/protobuf-java-3.25.5.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/google/auto/service/auto-service-annotations/1.0.1/auto-service-annotations-1.0.1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/javax/inject/javax.inject/1/javax.inject-1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/uber/nullaway/nullaway/0.13.1/nullaway-0.13.1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/checkerframework/dataflow-nullaway/3.53.0/dataflow-nullaway-3.53.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/hibernate/orm/hibernate-processor/7.2.4.Final/hibernate-processor-7.2.4.Final.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/hibernate/orm/hibernate-core/7.2.4.Final/hibernate-core-7.2.4.Final.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/persistence/jakarta.persistence-api/3.2.0/jakarta.persistence-api-3.2.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/fasterxml/classmate/1.7.1/classmate-1.7.1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/hibernate/models/hibernate-models/1.0.1/hibernate-models-1.0.1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/io/smallrye/jandex/3.3.2/jandex-3.3.2.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/xml/bind/jakarta.xml.bind-api/4.0.4/jakarta.xml.bind-api-4.0.4.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/activation/jakarta.activation-api/2.1.4/jakarta.activation-api-2.1.4.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/glassfish/jaxb/jaxb-runtime/4.0.6/jaxb-runtime-4.0.6.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/glassfish/jaxb/jaxb-core/4.0.6/jaxb-core-4.0.6.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/eclipse/angus/angus-activation/2.0.3/angus-activation-2.0.3.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/glassfish/jaxb/txw2/4.0.6/txw2-4.0.6.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/com/sun/istack/istack-commons-runtime/4.1.2/istack-commons-runtime-4.1.2.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/validation/jakarta.validation-api/3.1.1/jakarta.validation-api-3.1.1.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/jakarta/annotation/jakarta.annotation-api/3.0.0/jakarta.annotation-api-3.0.0.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/antlr/antlr4-runtime/4.13.2/antlr4-runtime-4.13.2.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/net/bytebuddy/byte-buddy/1.17.8/byte-buddy-1.17.8.jar" />
|
||||
<entry name="$USER_HOME$/.m2/peak-repo/org/jboss/logging/jboss-logging/3.6.1.Final/jboss-logging-3.6.1.Final.jar" />
|
||||
</processorPath>
|
||||
<module name="peakup-backend" />
|
||||
</profile>
|
||||
|
||||
2
.idea/encodings.xml
generated
2
.idea/encodings.xml
generated
@@ -5,6 +5,8 @@
|
||||
<file url="file://$PROJECT_DIR$/peakup-backend/peakup-dev/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/peakup-backend/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/peakup-backend/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/peakup-maven/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/peakup-maven/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/peakup-tools/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/peakup-tools/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
|
||||
@@ -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`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -160,6 +160,25 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.peakup</groupId>
|
||||
<artifactId>peakup-maven</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>generate-package-info</id>
|
||||
<goals>
|
||||
<goal>generate-package-info</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>generate-typescript</id>
|
||||
<goals>
|
||||
<goal>generate-typescript</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
|
||||
6
peakup-maven/peakup-maven.iml
Normal file
6
peakup-maven/peakup-maven.iml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module version="4">
|
||||
<component name="CheckStyle-IDEA-Module" serialisationVersion="2">
|
||||
<option name="activeLocationsIds" />
|
||||
</component>
|
||||
</module>
|
||||
69
peakup-maven/pom.xml
Normal file
69
peakup-maven/pom.xml
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.peakup</groupId>
|
||||
<artifactId>peakup-maven</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>maven-plugin</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.version>3.9.9</maven.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-plugin-api</artifactId>
|
||||
<version>${maven.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugin-tools</groupId>
|
||||
<artifactId>maven-plugin-annotations</artifactId>
|
||||
<version>3.15.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-core</artifactId>
|
||||
<version>${maven.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.ws.rs</groupId>
|
||||
<artifactId>jakarta.ws.rs-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<version>2.18.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-plugin-plugin</artifactId>
|
||||
<version>3.15.1</version>
|
||||
<configuration>
|
||||
<goalPrefix>peakup</goalPrefix>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-descriptor</id>
|
||||
<phase>process-classes</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -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<Path> 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<Path> 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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Scans all classes extending {@code AbstractResource}, reads JAX-RS annotations and
|
||||
* {@code RestResponse<T>} return types via reflection, then emits into
|
||||
* {@code peakup-frontend/src/lib/api/}:
|
||||
* <p>Scans all compiled classes extending {@code AbstractResource}, reads JAX-RS annotations
|
||||
* and {@code RestResponse<T>} return types via reflection, then writes into the frontend:
|
||||
* <ul>
|
||||
* <li>{@code httpClient.ts} — shared fetch helpers</li>
|
||||
* <li>{@code {Entity}Do.ts} — one interface per domain entity, mirroring the Java class hierarchy</li>
|
||||
* <li>{@code {Entity}Do.ts} — one interface per domain entity</li>
|
||||
* <li>{@code {Name}.ts} — union type per enum</li>
|
||||
* <li>{@code {Resource}Client.ts} — one client file per JAX-RS resource</li>
|
||||
* <li>{@code hooks.ts} — TanStack Query hooks</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Run from the project root (backend must be compiled first):
|
||||
* <pre>
|
||||
* $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
|
||||
* </pre>
|
||||
*/
|
||||
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<Class<?>> 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<ResourceInfo> 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<Class<?>> 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<Class<?>> resourceClasses = findResourceClasses(classLoader);
|
||||
getLog().info("Found: " + resourceClasses.stream()
|
||||
.map(Class::getSimpleName).collect(Collectors.joining(", ")));
|
||||
|
||||
final List<ResourceInfo> 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<Class<?>> 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<Class<?>> findResourceClasses() throws Exception {
|
||||
final Class<?> abstractResource = Class.forName("com.peakup.common.AbstractResource");
|
||||
@SuppressWarnings("unchecked")
|
||||
private URLClassLoader buildClassLoader() throws MojoExecutionException {
|
||||
try {
|
||||
final List<String> elements = (List<String>) 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<Class<?>> findResourceClasses(final URLClassLoader classLoader) throws Exception {
|
||||
final Class<?> abstractResource = classLoader.loadClass("com.peakup.common.AbstractResource");
|
||||
final List<Class<?>> 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<String> 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<String> 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<Class<?>> result)
|
||||
throws IOException {
|
||||
private void scanDirectory(final Path root, final Class<?> abstractResource,
|
||||
final URLClassLoader classLoader, final List<Class<?>> result) throws IOException {
|
||||
try (final Stream<Path> 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<Class<?>> result)
|
||||
throws IOException {
|
||||
private void scanJar(final Path jarPath, final Class<?> abstractResource,
|
||||
final URLClassLoader classLoader, final List<Class<?>> 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<Class<?>> result) {
|
||||
private void tryLoadResource(final String className, final Class<?> abstractResource,
|
||||
final URLClassLoader classLoader, final List<Class<?>> 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<EndpointInfo> 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<ParamInfo> 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<Class<?>> queue, final Set<Class<?>> 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, string | number | boolean | undefined>): 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<T>(path: string): Promise<T> {\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<T>;\n");
|
||||
sb.append("}\n\n");
|
||||
|
||||
sb.append("export async function post<T>(path: string, body?: unknown): Promise<T> {\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<T>;\n");
|
||||
sb.append("}\n\n");
|
||||
|
||||
sb.append("export async function put<T>(path: string, body?: unknown): Promise<T> {\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<T>;\n");
|
||||
sb.append("}\n\n");
|
||||
|
||||
sb.append("export async function del<T>(path: string): Promise<T> {\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<T>;\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, string | number | boolean | undefined>): 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<T>(path: string): Promise<T> {\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<T>;\n"
|
||||
+ "}\n\n"
|
||||
+ "export async function post<T>(path: string, body?: unknown): Promise<T> {\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<T>;\n"
|
||||
+ "}\n\n"
|
||||
+ "export async function put<T>(path: string, body?: unknown): Promise<T> {\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<T>;\n"
|
||||
+ "}\n\n"
|
||||
+ "export async function del<T>(path: string): Promise<T> {\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<T>;\n"
|
||||
+ "}\n";
|
||||
}
|
||||
|
||||
// ── {Entity}Do.ts / {Enum}.ts ─────────────────────────────────────────────
|
||||
|
||||
private static String generateEntityDoFile(final Class<?> cls, final Set<Class<?>> 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<String> 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<Field> 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<Class<?>> 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<String> 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<String> resolveHttpHelpers(final List<EndpointInfo> endpoints) {
|
||||
@@ -580,26 +591,24 @@ public class TypeScriptClientGenerator {
|
||||
|
||||
private static String generateClientFunction(final EndpointInfo endpoint, final String basePath) {
|
||||
final List<String> 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<ParamInfo> 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<ResourceInfo> resources, final Set<Class<?>> 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<String> 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<String> 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<EndpointInfo> 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<EndpointInfo> 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<ParamInfo> 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 extends Annotation> A getAnnotation(final Annotation[] annotations, final Class<A> 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<String> 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 ───────────────────────────────────────────────────────────────
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- Generated by maven-plugin-tools 3.15 (for help mojo with limited elements)-->
|
||||
|
||||
<plugin>
|
||||
<name>peakup-maven</name>
|
||||
<description></description>
|
||||
<groupId>com.peakup</groupId>
|
||||
<artifactId>peakup-maven</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<goalPrefix>peakup</goalPrefix>
|
||||
<mojos>
|
||||
<mojo>
|
||||
<goal>generate-package-info</goal>
|
||||
<description>Generates package-info.java files with the @NonNull annotation for all packages that contain Java source files.</description>
|
||||
<requiresDirectInvocation>false</requiresDirectInvocation>
|
||||
<requiresProject>true</requiresProject>
|
||||
<requiresReports>false</requiresReports>
|
||||
<aggregator>false</aggregator>
|
||||
<requiresOnline>false</requiresOnline>
|
||||
<inheritedByDefault>true</inheritedByDefault>
|
||||
<phase>generate-sources</phase>
|
||||
<implementation>com.peakup.maven.PackageInfoGeneratorMojo</implementation>
|
||||
<language>java</language>
|
||||
<instantiationStrategy>per-lookup</instantiationStrategy>
|
||||
<executionStrategy>once-per-session</executionStrategy>
|
||||
<threadSafe>true</threadSafe>
|
||||
<parameters>
|
||||
<parameter>
|
||||
<name>sourceDirectory</name>
|
||||
<type>java.io.File</type>
|
||||
<required>true</required>
|
||||
<editable>true</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<configuration>
|
||||
<sourceDirectory implementation="java.io.File" default-value="${project.build.sourceDirectory}"/>
|
||||
</configuration>
|
||||
</mojo>
|
||||
<mojo>
|
||||
<goal>generate-typescript</goal>
|
||||
<description>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 </description>
|
||||
<requiresDependencyResolution>compile</requiresDependencyResolution>
|
||||
<requiresDirectInvocation>false</requiresDirectInvocation>
|
||||
<requiresProject>true</requiresProject>
|
||||
<requiresReports>false</requiresReports>
|
||||
<aggregator>false</aggregator>
|
||||
<requiresOnline>false</requiresOnline>
|
||||
<inheritedByDefault>true</inheritedByDefault>
|
||||
<phase>process-classes</phase>
|
||||
<implementation>com.peakup.maven.TypeScriptClientGeneratorMojo</implementation>
|
||||
<language>java</language>
|
||||
<instantiationStrategy>per-lookup</instantiationStrategy>
|
||||
<executionStrategy>once-per-session</executionStrategy>
|
||||
<threadSafe>true</threadSafe>
|
||||
<parameters>
|
||||
<parameter>
|
||||
<name>outputDirApi</name>
|
||||
<type>java.io.File</type>
|
||||
<required>true</required>
|
||||
<editable>true</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
<parameter>
|
||||
<name>outputDirTypes</name>
|
||||
<type>java.io.File</type>
|
||||
<required>true</required>
|
||||
<editable>true</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<configuration>
|
||||
<outputDirApi implementation="java.io.File" default-value="${project.basedir}/../peakup-frontend/src/api"/>
|
||||
<outputDirTypes implementation="java.io.File" default-value="${project.basedir}/../peakup-frontend/src/types"/>
|
||||
</configuration>
|
||||
</mojo>
|
||||
</mojos>
|
||||
</plugin>
|
||||
112
peakup-maven/target/classes/META-INF/maven/plugin.xml
Normal file
112
peakup-maven/target/classes/META-INF/maven/plugin.xml
Normal file
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- Generated by maven-plugin-tools 3.15-->
|
||||
|
||||
<plugin>
|
||||
<name>peakup-maven</name>
|
||||
<description></description>
|
||||
<groupId>com.peakup</groupId>
|
||||
<artifactId>peakup-maven</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<goalPrefix>peakup</goalPrefix>
|
||||
<isolatedRealm>false</isolatedRealm>
|
||||
<inheritedByDefault>true</inheritedByDefault>
|
||||
<requiredJavaVersion>21</requiredJavaVersion>
|
||||
<requiredMavenVersion>3.9.9</requiredMavenVersion>
|
||||
<mojos>
|
||||
<mojo>
|
||||
<goal>generate-package-info</goal>
|
||||
<description>Generates package-info.java files with the @NonNull annotation for all packages that contain Java source files.</description>
|
||||
<requiresDirectInvocation>false</requiresDirectInvocation>
|
||||
<requiresProject>true</requiresProject>
|
||||
<requiresReports>false</requiresReports>
|
||||
<aggregator>false</aggregator>
|
||||
<requiresOnline>false</requiresOnline>
|
||||
<inheritedByDefault>true</inheritedByDefault>
|
||||
<phase>generate-sources</phase>
|
||||
<implementation>com.peakup.maven.PackageInfoGeneratorMojo</implementation>
|
||||
<language>java</language>
|
||||
<instantiationStrategy>per-lookup</instantiationStrategy>
|
||||
<executionStrategy>once-per-session</executionStrategy>
|
||||
<threadSafe>true</threadSafe>
|
||||
<parameters>
|
||||
<parameter>
|
||||
<name>sourceDirectory</name>
|
||||
<type>java.io.File</type>
|
||||
<required>true</required>
|
||||
<editable>true</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<configuration>
|
||||
<sourceDirectory implementation="java.io.File" default-value="${project.build.sourceDirectory}"/>
|
||||
</configuration>
|
||||
</mojo>
|
||||
<mojo>
|
||||
<goal>generate-typescript</goal>
|
||||
<description>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 </description>
|
||||
<requiresDependencyResolution>compile</requiresDependencyResolution>
|
||||
<requiresDirectInvocation>false</requiresDirectInvocation>
|
||||
<requiresProject>true</requiresProject>
|
||||
<requiresReports>false</requiresReports>
|
||||
<aggregator>false</aggregator>
|
||||
<requiresOnline>false</requiresOnline>
|
||||
<inheritedByDefault>true</inheritedByDefault>
|
||||
<phase>process-classes</phase>
|
||||
<implementation>com.peakup.maven.TypeScriptClientGeneratorMojo</implementation>
|
||||
<language>java</language>
|
||||
<instantiationStrategy>per-lookup</instantiationStrategy>
|
||||
<executionStrategy>once-per-session</executionStrategy>
|
||||
<threadSafe>true</threadSafe>
|
||||
<parameters>
|
||||
<parameter>
|
||||
<name>outputDirApi</name>
|
||||
<type>java.io.File</type>
|
||||
<required>true</required>
|
||||
<editable>true</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
<parameter>
|
||||
<name>outputDirTypes</name>
|
||||
<type>java.io.File</type>
|
||||
<required>true</required>
|
||||
<editable>true</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
<parameter>
|
||||
<name>project</name>
|
||||
<type>org.apache.maven.project.MavenProject</type>
|
||||
<required>true</required>
|
||||
<editable>false</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<configuration>
|
||||
<outputDirApi implementation="java.io.File" default-value="${project.basedir}/../peakup-frontend/src/api"/>
|
||||
<outputDirTypes implementation="java.io.File" default-value="${project.basedir}/../peakup-frontend/src/types"/>
|
||||
<project implementation="org.apache.maven.project.MavenProject" default-value="${project}"/>
|
||||
</configuration>
|
||||
</mojo>
|
||||
</mojos>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.ws.rs</groupId>
|
||||
<artifactId>jakarta.ws.rs-api</artifactId>
|
||||
<type>jar</type>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<type>jar</type>
|
||||
<version>2.18.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
3
peakup-maven/target/maven-archiver/pom.properties
Normal file
3
peakup-maven/target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
artifactId=peakup-maven
|
||||
groupId=com.peakup
|
||||
version=1.0-SNAPSHOT
|
||||
@@ -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
|
||||
@@ -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
|
||||
113
peakup-maven/target/plugin-enhanced.xml
Normal file
113
peakup-maven/target/plugin-enhanced.xml
Normal file
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- Generated by maven-plugin-tools 3.15 (enhanced XHTML version (used for plugin:report))-->
|
||||
|
||||
<plugin>
|
||||
<name>peakup-maven</name>
|
||||
<description></description>
|
||||
<groupId>com.peakup</groupId>
|
||||
<artifactId>peakup-maven</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<goalPrefix>peakup</goalPrefix>
|
||||
<isolatedRealm>false</isolatedRealm>
|
||||
<inheritedByDefault>true</inheritedByDefault>
|
||||
<requiredJavaVersion>21</requiredJavaVersion>
|
||||
<requiredMavenVersion>3.9.9</requiredMavenVersion>
|
||||
<mojos>
|
||||
<mojo>
|
||||
<goal>generate-package-info</goal>
|
||||
<description>Generates package-info.java files with the @NonNull annotation for all packages that contain Java source files.</description>
|
||||
<requiresDirectInvocation>false</requiresDirectInvocation>
|
||||
<requiresProject>true</requiresProject>
|
||||
<requiresReports>false</requiresReports>
|
||||
<aggregator>false</aggregator>
|
||||
<requiresOnline>false</requiresOnline>
|
||||
<inheritedByDefault>true</inheritedByDefault>
|
||||
<phase>generate-sources</phase>
|
||||
<implementation>com.peakup.maven.PackageInfoGeneratorMojo</implementation>
|
||||
<language>java</language>
|
||||
<instantiationStrategy>per-lookup</instantiationStrategy>
|
||||
<executionStrategy>once-per-session</executionStrategy>
|
||||
<threadSafe>true</threadSafe>
|
||||
<parameters>
|
||||
<parameter>
|
||||
<name>sourceDirectory</name>
|
||||
<type>java.io.File</type>
|
||||
<required>true</required>
|
||||
<editable>true</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<configuration>
|
||||
<sourceDirectory implementation="java.io.File" default-value="${project.build.sourceDirectory}"/>
|
||||
</configuration>
|
||||
</mojo>
|
||||
<mojo>
|
||||
<goal>generate-typescript</goal>
|
||||
<description>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></description>
|
||||
<requiresDependencyResolution>compile</requiresDependencyResolution>
|
||||
<requiresDirectInvocation>false</requiresDirectInvocation>
|
||||
<requiresProject>true</requiresProject>
|
||||
<requiresReports>false</requiresReports>
|
||||
<aggregator>false</aggregator>
|
||||
<requiresOnline>false</requiresOnline>
|
||||
<inheritedByDefault>true</inheritedByDefault>
|
||||
<phase>process-classes</phase>
|
||||
<implementation>com.peakup.maven.TypeScriptClientGeneratorMojo</implementation>
|
||||
<language>java</language>
|
||||
<instantiationStrategy>per-lookup</instantiationStrategy>
|
||||
<executionStrategy>once-per-session</executionStrategy>
|
||||
<threadSafe>true</threadSafe>
|
||||
<parameters>
|
||||
<parameter>
|
||||
<name>outputDirApi</name>
|
||||
<type>java.io.File</type>
|
||||
<required>true</required>
|
||||
<editable>true</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
<parameter>
|
||||
<name>outputDirTypes</name>
|
||||
<type>java.io.File</type>
|
||||
<required>true</required>
|
||||
<editable>true</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
<parameter>
|
||||
<name>project</name>
|
||||
<type>org.apache.maven.project.MavenProject</type>
|
||||
<required>true</required>
|
||||
<editable>false</editable>
|
||||
<description></description>
|
||||
</parameter>
|
||||
</parameters>
|
||||
<configuration>
|
||||
<outputDirApi implementation="java.io.File" default-value="${project.basedir}/../peakup-frontend/src/api"/>
|
||||
<outputDirTypes implementation="java.io.File" default-value="${project.basedir}/../peakup-frontend/src/types"/>
|
||||
<project implementation="org.apache.maven.project.MavenProject" default-value="${project}"/>
|
||||
</configuration>
|
||||
</mojo>
|
||||
</mojos>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.ws.rs</groupId>
|
||||
<artifactId>jakarta.ws.rs-api</artifactId>
|
||||
<type>jar</type>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<type>jar</type>
|
||||
<version>2.18.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
@@ -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<Path> 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<Path> 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user