[DEV] update Typescript API generation

This commit is contained in:
Edouard DUPIN 2024-03-10 11:09:52 +01:00
parent 526f902eae
commit 7c217716c9
3 changed files with 424 additions and 346 deletions

View File

@ -3,28 +3,22 @@ package org.kar.archidata.dataAccess;
import java.io.File; import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter; import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import org.kar.archidata.annotation.AnnotationTools;
import org.kar.archidata.annotation.AsyncType; import org.kar.archidata.annotation.AsyncType;
import org.kar.archidata.exception.DataAccessException; import org.kar.archidata.catcher.RestErrorResponse;
import org.kar.archidata.dataAccess.DataFactoryZod.ClassElement;
import org.kar.archidata.dataAccess.DataFactoryZod.GeneratedTypes;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -37,6 +31,7 @@ import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT; import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam; import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
@ -44,143 +39,46 @@ import jakarta.ws.rs.core.MediaType;
public class DataFactoryTsApi { public class DataFactoryTsApi {
static final Logger LOGGER = LoggerFactory.getLogger(DataFactoryTsApi.class); static final Logger LOGGER = LoggerFactory.getLogger(DataFactoryTsApi.class);
public static String convertTypeZodSimpleType(final Class<?> type, final Map<String, String> previousClassesGenerated, final List<String> order) throws Exception { record APIModel(String data, String className) {
if (type == UUID.class) {
return "string";
}
if (type == Long.class) {
return "Bigint";
}
if (type == long.class) {
return "Bigint";
}
if (type == Integer.class || type == int.class) {
return "number";
}
if (type == Boolean.class || type == boolean.class) {
return "boolean";
}
if (type == double.class || type == float.class || type == Double.class || type == Float.class) {
return "number";
}
if (type == Instant.class) {
return "string";
}
if (type == Date.class || type == Timestamp.class) {
return "string";
}
if (type == LocalDate.class) {
return "string";
}
if (type == LocalTime.class) {
return "string";
}
if (type == String.class) {
return "string";
}
if (type.isEnum()) {
final Object[] arr = type.getEnumConstants();
final StringBuilder out = new StringBuilder();
boolean first = true;
out.append("zod.enum([");
for (final Object elem : arr) {
if (!first) {
out.append(", ");
}
first = false;
out.append("\"");
out.append(elem.toString());
out.append("\"");
}
out.append("])");
return out.toString();
}
if (type == List.class) {
return null;
}
// createTable(type, previousClassesGenerated, order);
return "Zod" + type.getSimpleName();
}
public static String convertTypeZod(final Field field, final Map<String, String> previousClassesGenerated, final List<String> order) throws Exception {
final Class<?> type = field.getType();
final String simpleType = convertTypeZodSimpleType(type, previousClassesGenerated, order);
if (simpleType != null) {
return simpleType;
}
if (type == List.class) {
final ParameterizedType listType = (ParameterizedType) field.getGenericType();
final Class<?> listClass = (Class<?>) listType.getActualTypeArguments()[0];
final String simpleSubType = convertTypeZodSimpleType(listClass, previousClassesGenerated, order);
return "zod.array(" + simpleSubType + ")";
}
throw new DataAccessException("Imcompatible type of element in object for: " + type.getCanonicalName());
}
public static String optionalTypeZod(final Class<?> type) throws Exception {
if (type.isPrimitive()) {
return "";
}
return ".optional()";
}
public static void createTablesSpecificType(final Field elem, final int fieldId, final StringBuilder builder, final Map<String, String> previousClassesGenerated, final List<String> order)
throws Exception {
final String name = elem.getName();
final Class<?> classModel = elem.getType();
final int limitSize = AnnotationTools.getLimitSize(elem);
final String comment = AnnotationTools.getComment(elem);
if (fieldId != 0) {
builder.append(",");
}
if (comment != null) {
builder.append("\n\t// ");
builder.append(comment);
}
builder.append("\n\t");
builder.append(name);
builder.append(": ");
builder.append(convertTypeZod(elem, previousClassesGenerated, order));
if (limitSize > 0 && classModel == String.class) {
builder.append(".max(");
builder.append(limitSize);
builder.append(")");
}
if (AnnotationTools.getSchemaReadOnly(elem)) {
builder.append(".readonly()");
}
builder.append(optionalTypeZod(classModel));
} }
/** Request the generation of the TypeScript file for the "Zod" export model /** Request the generation of the TypeScript file for the "Zod" export model
* @param classs List of class used in the model * @param classs List of class used in the model
* @return A string representing the Server models
* @throws Exception */ * @throws Exception */
public static String createApi(final List<Class<?>> classs, final Set<Class<?>> classNeeded) throws Exception { public static List<String> createApi(final List<Class<?>> classs, final GeneratedTypes previous, final String pathPackage) throws Exception {
final List<String> apis = new ArrayList<>(); final List<String> apis = new ArrayList<>();
for (final Class<?> clazz : classs) { final String globalheader = """
final String api = createSingleApi(clazz, classNeeded);
apis.add(api);
}
final StringBuilder generatedDataElems = new StringBuilder();
for (final String elem : apis) {
generatedDataElems.append(elem);
generatedDataElems.append("\n\n");
}
final StringBuilder generatedData = new StringBuilder();
generatedData.append("""
/** /**
* API of the server (auto-generated code) * API of the server (auto-generated code)
*/ */
import {"""); import { HTTPMimeType, HTTPRequestModel, ModelResponseHttp, RESTConfig, RESTRequest, isArrayOf } from "./rest-tools"
for (final Class<?> elem : classNeeded) { import {""";
generatedData.append(elem.getSimpleName());
for (final Class<?> clazz : classs) {
final Set<String> includeModel = new HashSet<>();
includeModel.add("RestErrorResponse");
final APIModel api = createSingleApi(clazz, includeModel, previous);
final StringBuilder generatedData = new StringBuilder();
generatedData.append(globalheader);
for (final String elem : includeModel) {
if (elem == null || elem.equals("string") || elem.equals("String")) {
continue;
}
generatedData.append(elem);
generatedData.append(", "); generatedData.append(", ");
} }
generatedData.append("} from \"./model.ts\"\n\n"); generatedData.append("} from \"./model\"\n");
return generatedData.toString() + generatedDataElems.toString(); generatedData.append(api.data());
String fileName = api.className();
fileName = fileName.replaceAll("([A-Z])", "-$1").toLowerCase();
fileName = fileName.replaceAll("^\\-*", "");
apis.add(fileName);
final FileWriter myWriter = new FileWriter(pathPackage + File.separator + fileName + ".ts");
myWriter.write(generatedData.toString());
myWriter.close();
}
return apis;
} }
public static String apiAnnotationGetPath(final Class<?> element) throws Exception { public static String apiAnnotationGetPath(final Class<?> element) throws Exception {
@ -191,6 +89,30 @@ public class DataFactoryTsApi {
return ((Path) annotation[0]).value(); return ((Path) annotation[0]).value();
} }
public static List<String> apiAnnotationProduces(final Class<?> element) throws Exception {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(Produces.class);
if (annotation.length == 0) {
return null;
}
return Arrays.asList(((Produces) annotation[0]).value());
}
public static List<String> apiAnnotationProduces(final Method element) throws Exception {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(Produces.class);
if (annotation.length == 0) {
return null;
}
return Arrays.asList(((Produces) annotation[0]).value());
}
public static List<String> apiAnnotationProduces(final Class<?> clazz, final Method method) throws Exception {
final List<String> data = apiAnnotationProduces(method);
if (data != null) {
return data;
}
return apiAnnotationProduces(clazz);
}
public static String apiAnnotationGetOperationDescription(final Method element) throws Exception { public static String apiAnnotationGetOperationDescription(final Method element) throws Exception {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(Operation.class); final Annotation[] annotation = element.getDeclaredAnnotationsByType(Operation.class);
if (annotation.length == 0) { if (annotation.length == 0) {
@ -258,11 +180,27 @@ public class DataFactoryTsApi {
return Arrays.asList(((Consumes) annotation[0]).value()); return Arrays.asList(((Consumes) annotation[0]).value());
} }
public static List<String> apiAnnotationGetConsumes(final Class<?> element) throws Exception {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(Consumes.class);
if (annotation.length == 0) {
return null;
}
return Arrays.asList(((Consumes) annotation[0]).value());
}
public static List<String> apiAnnotationGetConsumes(final Class<?> clazz, final Method method) throws Exception {
final List<String> data = apiAnnotationGetConsumes(method);
if (data != null) {
return data;
}
return apiAnnotationGetConsumes(clazz);
}
public static boolean apiAnnotationIsContext(final Parameter element) throws Exception { public static boolean apiAnnotationIsContext(final Parameter element) throws Exception {
return element.getDeclaredAnnotationsByType(Context.class).length != 0; return element.getDeclaredAnnotationsByType(Context.class).length != 0;
} }
public static String createSingleApi(final Class<?> clazz, final Set<Class<?>> classNeeded) throws Exception { public static APIModel createSingleApi(final Class<?> clazz, final Set<String> includeModel, final GeneratedTypes previous) throws Exception {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
// the basic path has no specific elements... // the basic path has no specific elements...
final String basicPath = apiAnnotationGetPath(clazz); final String basicPath = apiAnnotationGetPath(clazz);
@ -270,59 +208,93 @@ public class DataFactoryTsApi {
builder.append("export namespace "); builder.append("export namespace ");
builder.append(classSimpleName); builder.append(classSimpleName);
builder.append("API {\n"); builder.append(" {\n");
LOGGER.info("Parse Class for path: {} => {}", classSimpleName, basicPath); LOGGER.info("Parse Class for path: {} => {}", classSimpleName, basicPath);
for (final Method method : clazz.getDeclaredMethods()) { for (final Method method : clazz.getDeclaredMethods()) {
final String methodName = method.getName(); final String methodName = method.getName();
final String methodPath = apiAnnotationGetPath(method); final String methodPath = apiAnnotationGetPath(method);
final String methodType = apiAnnotationGetTypeRequest(method); final String methodType = apiAnnotationGetTypeRequest(method);
final String methodDescription = apiAnnotationGetOperationDescription(method); final String methodDescription = apiAnnotationGetOperationDescription(method);
final List<String> consumes = apiAnnotationGetConsumes(method); final List<String> consumes = apiAnnotationGetConsumes(clazz, method);
final List<String> produces = apiAnnotationProduces(clazz, method);
if (consumes != null && consumes.contains(MediaType.MULTIPART_FORM_DATA)) { if (consumes != null && consumes.contains(MediaType.MULTIPART_FORM_DATA)) {
LOGGER.error(" [{}] {} => {}/{} ==> Multipart is not managed ...", methodType, methodName, basicPath, methodPath); LOGGER.error(" [{}] {} => {}/{} ==> Multipart is not managed ...", methodType, methodName, basicPath, methodPath);
continue; continue;
} }
LOGGER.trace(" [{}] {} => {}/{}", methodType, methodName, basicPath, methodPath); LOGGER.trace(" [{}] {} => {}/{}", methodType, methodName, basicPath, methodPath);
final Class<?> returnType = method.getReturnType();
if (methodDescription != null) { if (methodDescription != null) {
LOGGER.trace(" description: {}", methodDescription); LOGGER.trace(" description: {}", methodDescription);
} }
LOGGER.trace(" return: {}", returnType.getSimpleName()); Class<?> returnTypeModel = method.getReturnType();
final Map<String, Class<?>> queryParams = new HashMap<>(); boolean returnModelIsArray = false;
final Map<String, Class<?>> pathParams = new HashMap<>(); ClassElement tmpReturn;
final List<Class<?>> emptyElement = new ArrayList<>(); if (returnTypeModel == List.class) {
final ParameterizedType listType = (ParameterizedType) method.getGenericReturnType();
returnTypeModel = (Class<?>) listType.getActualTypeArguments()[0];
tmpReturn = DataFactoryZod.createTable(returnTypeModel, previous);
returnModelIsArray = true;
includeModel.add(tmpReturn.tsTypeName);
} else {
tmpReturn = DataFactoryZod.createTable(returnTypeModel, previous);
}
includeModel.add(tmpReturn.tsTypeName);
includeModel.add(tmpReturn.tsCheckType);
LOGGER.trace(" return: {}", tmpReturn.tsTypeName);
final Map<String, String> queryParams = new HashMap<>();
final Map<String, String> pathParams = new HashMap<>();
final List<String> emptyElement = new ArrayList<>();
// LOGGER.info(" Parameters:"); // LOGGER.info(" Parameters:");
for (final Parameter parameter : method.getParameters()) { for (final Parameter parameter : method.getParameters()) {
// Security context are internal parameter (not available from API)
if (apiAnnotationIsContext(parameter)) { if (apiAnnotationIsContext(parameter)) {
continue; continue;
} }
final Class<?> parameterType = parameter.getType(); final Class<?> parameterType = parameter.getType();
String parameterTypeString;
if (parameterType == List.class) {
parameterTypeString = "any[]";
} else {
final ClassElement tmpClassElement = DataFactoryZod.createTable(parameterType, previous);
includeModel.add(tmpClassElement.tsTypeName);
final ClassElement tmp = new ClassElement(parameterType);
parameterTypeString = tmp.tsTypeName;
}
final String pathParam = apiAnnotationGetPathParam(parameter); final String pathParam = apiAnnotationGetPathParam(parameter);
final String queryParam = apiAnnotationGetQueryParam(parameter); final String queryParam = apiAnnotationGetQueryParam(parameter);
if (queryParam != null) { if (queryParam != null) {
queryParams.put(queryParam, parameterType); queryParams.put(queryParam, parameterTypeString);
} else if (pathParam != null) { } else if (pathParam != null) {
pathParams.put(pathParam, parameterType); pathParams.put(pathParam, parameterTypeString);
} else { } else {
final Class<?> asyncType = apiAnnotationGetAsyncType(parameter); final Class<?> asyncType = apiAnnotationGetAsyncType(parameter);
if (asyncType != null) { if (asyncType != null) {
emptyElement.add(asyncType); final ClassElement tmpClassElement = DataFactoryZod.createTable(asyncType, previous);
includeModel.add(tmpClassElement.tsTypeName);
final ClassElement tmp = new ClassElement(asyncType);
emptyElement.add(tmp.tsTypeName);
} else if (parameterType == List.class) {
parameterTypeString = "any[]";
final Class<?> plop = parameterType.arrayType();
LOGGER.info("ArrayType = {}", plop);
} else { } else {
emptyElement.add(parameterType); final ClassElement tmpClassElement = DataFactoryZod.createTable(parameterType, previous);
includeModel.add(tmpClassElement.tsTypeName);
final ClassElement tmp = new ClassElement(parameterType);
emptyElement.add(tmp.tsTypeName);
} }
// LOGGER.info(" - {} ", parameterType.getSimpleName());
} }
} }
if (!queryParams.isEmpty()) { if (!queryParams.isEmpty()) {
LOGGER.trace(" Query parameter:"); LOGGER.trace(" Query parameter:");
for (final Entry<String, Class<?>> queryEntry : queryParams.entrySet()) { for (final Entry<String, String> queryEntry : queryParams.entrySet()) {
LOGGER.trace(" - {}: {}", queryEntry.getKey(), queryEntry.getValue().getSimpleName()); LOGGER.trace(" - {}: {}", queryEntry.getKey(), queryEntry.getValue());
} }
} }
if (!pathParams.isEmpty()) { if (!pathParams.isEmpty()) {
LOGGER.trace(" Path parameter:"); LOGGER.trace(" Path parameter:");
for (final Entry<String, Class<?>> pathEntry : pathParams.entrySet()) { for (final Entry<String, String> pathEntry : pathParams.entrySet()) {
LOGGER.trace(" - {}: {}", pathEntry.getKey(), pathEntry.getValue().getSimpleName()); LOGGER.trace(" - {}: {}", pathEntry.getKey(), pathEntry.getValue());
} }
} }
@ -330,7 +302,7 @@ public class DataFactoryTsApi {
LOGGER.error(" Fail to parse: Too much element in the model for the data ..."); LOGGER.error(" Fail to parse: Too much element in the model for the data ...");
continue; continue;
} else if (emptyElement.size() == 1) { } else if (emptyElement.size() == 1) {
LOGGER.trace(" data type: {}", emptyElement.get(0).getSimpleName()); LOGGER.trace(" data type: {}", emptyElement.get(0));
} }
// ALL is good can generate the Elements // ALL is good can generate the Elements
@ -342,8 +314,7 @@ public class DataFactoryTsApi {
builder.append("\n\texport function "); builder.append("\n\texport function ");
builder.append(methodName); builder.append(methodName);
builder.append("({"); builder.append("({");
builder.append("options,"); builder.append("restConfig,");
builder.append(" serverUrl,");
if (!queryParams.isEmpty()) { if (!queryParams.isEmpty()) {
builder.append(" queries,"); builder.append(" queries,");
} }
@ -354,78 +325,131 @@ public class DataFactoryTsApi {
builder.append(" data,"); builder.append(" data,");
} }
builder.append(" } : {"); builder.append(" } : {");
builder.append("\n\t\t\toptions: any,"); builder.append("\n\t\t\trestConfig: RESTConfig,");
builder.append("\n\t\t\tserverUrl: string,");
if (!queryParams.isEmpty()) { if (!queryParams.isEmpty()) {
builder.append("\n\t\t\tqueries: {"); builder.append("\n\t\t\tqueries: {");
for (final Entry<String, Class<?>> queryEntry : queryParams.entrySet()) { for (final Entry<String, String> queryEntry : queryParams.entrySet()) {
classNeeded.add(queryEntry.getValue());
builder.append("\n\t\t\t\t"); builder.append("\n\t\t\t\t");
builder.append(queryEntry.getKey()); builder.append(queryEntry.getKey());
builder.append(": "); builder.append(": ");
builder.append(queryEntry.getValue().getSimpleName()); builder.append(queryEntry.getValue());
builder.append(","); builder.append(",");
} }
builder.append("\n\t\t\t},"); builder.append("\n\t\t\t},");
} }
if (!pathParams.isEmpty()) { if (!pathParams.isEmpty()) {
builder.append("\n\t\t\tparams: {"); builder.append("\n\t\t\tparams: {");
for (final Entry<String, Class<?>> pathEntry : pathParams.entrySet()) { for (final Entry<String, String> pathEntry : pathParams.entrySet()) {
classNeeded.add(pathEntry.getValue());
builder.append("\n\t\t\t\t"); builder.append("\n\t\t\t\t");
builder.append(pathEntry.getKey()); builder.append(pathEntry.getKey());
builder.append(": "); builder.append(": ");
builder.append(pathEntry.getValue().getSimpleName()); builder.append(pathEntry.getValue());
builder.append(","); builder.append(",");
} }
builder.append("\n\t\t\t},"); builder.append("\n\t\t\t},");
} }
if (emptyElement.size() == 1) { if (emptyElement.size() == 1) {
builder.append("\n\t\t\tdata: "); builder.append("\n\t\t\tdata: ");
classNeeded.add(emptyElement.get(0)); builder.append(emptyElement.get(0));
builder.append(emptyElement.get(0).getSimpleName());
builder.append(","); builder.append(",");
} }
builder.append("\n\t\t}) : Promise<"); builder.append("\n\t\t}) : Promise<");
if (returnType == Void.class) { builder.append(tmpReturn.tsTypeName);
builder.append("void"); if (returnModelIsArray) {
} else { builder.append("[]");
classNeeded.add(returnType);
builder.append(returnType.getSimpleName());
} }
builder.append("> {"); builder.append("> {");
builder.append("\n\t\treturn new Promise((resolve, reject) => {"); builder.append("\n\t\treturn new Promise((resolve, reject) => {");
/* fetch('https://example.com?' + new URLSearchParams({ foo: 'value', bar: 2, })) */ builder.append("\n\t\t\tRESTRequest({");
builder.append("\n\t\t\t\trestModel: {");
builder.append("\n\t\t\t\t\tendPoint: \"");
builder.append(basicPath);
if (methodPath != null) {
builder.append("/");
builder.append(methodPath);
}
builder.append("\",");
builder.append("\n\t\t\t\t\trequestType: HTTPRequestModel.");
builder.append(methodType);
builder.append(",");
if (consumes != null) {
for (final String elem : consumes) {
if (MediaType.APPLICATION_JSON.equals(elem)) {
builder.append("\n\t\t\t\t\tcontentType: HTTPMimeType.JSON,");
break;
}
}
}
if (produces != null) {
for (final String elem : produces) {
if (MediaType.APPLICATION_JSON.equals(elem)) {
builder.append("\n\t\t\t\t\taccept: HTTPMimeType.JSON,");
break;
}
}
}
builder.append("\n\t\t\t\t},");
builder.append("\n\t\t\t\trestConfig,");
if (!pathParams.isEmpty()) {
builder.append("\n\t\t\t\tparams,");
}
if (!queryParams.isEmpty()) {
builder.append("\n\t\t\t\tqueries,");
}
if (emptyElement.size() == 1) {
builder.append("\n\t\t\t\tdata,");
}
builder.append("\n\t\t\t}).then((value: ModelResponseHttp) => {");
if (returnModelIsArray) {
builder.append("\n\t\t\t\tif (isArrayOf(value.data, is");
builder.append(tmpReturn.tsTypeName);
builder.append(")) {");
} else {
builder.append("\n\t\t\t\tif (is");
builder.append(tmpReturn.tsTypeName);
builder.append("(value.data)) {");
}
builder.append("\n\t\t\t\t\tresolve(value.data);");
builder.append("\n\t\t\t\t} else {");
builder.append("\n\t\t\t\t\treject({");
builder.append("\n\t\t\t\t\t\ttime: Date().toString(),");
builder.append("\n\t\t\t\t\t\tstatus: 950,");
builder.append("\n\t\t\t\t\t\terror: \"REST Fail to verify the data\",");
builder.append("\n\t\t\t\t\t\tstatusMessage: \"API cast ERROR\",");
builder.append("\n\t\t\t\t\t\tmessage: \"api.ts Check type as fail\"");
builder.append("\n\t\t\t\t\t} as RestErrorResponse);");
builder.append("\n\t\t\t\t}");
builder.append("\n\t\t\t}).catch((reason: RestErrorResponse) => {");
builder.append("\n\t\t\t\treject(reason);");
builder.append("\n\t\t\t});");
builder.append("\n\t\t});"); builder.append("\n\t\t});");
builder.append("\n\t};"); builder.append("\n\t};");
} }
builder.append("\n}\n"); builder.append("\n}\n");
return builder.toString(); return new APIModel(builder.toString(), classSimpleName);
} }
public static void generatePackage(final List<Class<?>> classApi, final List<Class<?>> classModel, final String pathPackage) throws Exception { public static void generatePackage(final List<Class<?>> classApi, final List<Class<?>> classModel, final String pathPackage) throws Exception {
final Set<Class<?>> classNeeded = new HashSet<>(classModel); final GeneratedTypes previous = DataFactoryZod.createBasicType();
final String data = createApi(classApi, classNeeded); DataFactoryZod.createTable(RestErrorResponse.class, previous);
FileWriter myWriter = new FileWriter(pathPackage + File.separator + "api.ts"); final List<String> listApi = createApi(classApi, previous, pathPackage);
myWriter.write(data); final String packageApi = DataFactoryZod.createTables(new ArrayList<>(classModel), previous);
myWriter.close(); FileWriter myWriter = new FileWriter(pathPackage + File.separator + "model.ts");
final String packageApi = DataFactoryZod.createTables(new ArrayList<>(classNeeded));
myWriter = new FileWriter(pathPackage + File.separator + "model.ts");
myWriter.write(packageApi.toString()); myWriter.write(packageApi.toString());
myWriter.close(); myWriter.close();
final String index = """
final StringBuilder index = new StringBuilder("""
/** /**
* Global import of the package * Global import of the package
*/ */
export * from "./model.ts"; export * from "./model";
export * from "./api.ts"; """);
for (final String api : listApi) {
"""; index.append("export * from \"./").append(api).append("\";\n");
}
myWriter = new FileWriter(pathPackage + File.separator + "index.ts"); myWriter = new FileWriter(pathPackage + File.separator + "index.ts");
myWriter.write(index); myWriter.write(index.toString());
myWriter.close(); myWriter.close();
return; return;
} }

View File

@ -10,9 +10,7 @@ import java.time.LocalDate;
import java.time.LocalTime; import java.time.LocalTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.kar.archidata.annotation.AnnotationTools; import org.kar.archidata.annotation.AnnotationTools;
@ -23,79 +21,131 @@ import org.slf4j.LoggerFactory;
public class DataFactoryZod { public class DataFactoryZod {
static final Logger LOGGER = LoggerFactory.getLogger(DataFactoryZod.class); static final Logger LOGGER = LoggerFactory.getLogger(DataFactoryZod.class);
public static String convertTypeZodSimpleType(final Class<?> type, final Map<String, String> previousClassesGenerated, final List<String> order) throws Exception { static public class ClassElement {
if (type == UUID.class) { public Class<?>[] model;
return "ZodUUID"; public String zodName;
public String tsTypeName;
public String tsCheckType;
public String declaration;
public String comment = null;
public boolean nativeType;
public ClassElement(final Class<?> model[], final String zodName, final String tsTypeName, final String tsCheckType, final String declaration, final boolean nativeType) {
this.model = model;
this.zodName = zodName;
this.tsTypeName = tsTypeName;
this.tsCheckType = tsCheckType;
this.declaration = declaration;
this.nativeType = nativeType;
} }
if (type == Long.class) {
return "ZodLong"; public ClassElement(final Class<?> model) {
this(new Class<?>[] { model });
} }
if (type == long.class) {
return "ZodLong"; public ClassElement(final Class<?> model[]) {
this.model = model;
this.zodName = "Zod" + model[0].getSimpleName();
this.tsTypeName = model[0].getSimpleName();
this.tsCheckType = "is" + model[0].getSimpleName();
this.declaration = null;
this.nativeType = false;
} }
if (type == Integer.class || type == int.class) {
return "ZodInteger";
} }
if (type == Boolean.class || type == boolean.class) {
return "zod.boolean()"; public static class GeneratedTypes {
final List<ClassElement> previousGeneration = new ArrayList<>();
final List<Class<?>> order = new ArrayList<>();
public ClassElement find(final Class<?> clazz) {
for (final ClassElement elem : this.previousGeneration) {
for (final Class<?> elemClass : elem.model) {
if (elemClass == clazz) {
return elem;
} }
if (type == double.class || type == Double.class) {
return "ZodDouble";
} }
if (type == float.class || type == Float.class) {
return "ZodFloat";
} }
if (type == Instant.class) { return null;
return "ZodInstant";
} }
if (type == Date.class || type == Timestamp.class) {
return "ZodDate"; public void add(final ClassElement elem) {
this.previousGeneration.add(elem);
} }
if (type == LocalDate.class) {
return "ZodLocalDate"; public void add(final ClassElement elem, final boolean addOrder) {
this.previousGeneration.add(elem);
if (addOrder) {
this.order.add(elem.model[0]);
} }
if (type == LocalTime.class) {
return "ZodLocalTime";
} }
if (type == String.class) {
return "zod.string()"; public void addOrder(final ClassElement elem) {
this.order.add(elem.model[0]);
} }
if (type.isEnum()) { }
final Object[] arr = type.getEnumConstants();
public static ClassElement convertTypeZodEnum(final Class<?> clazz, final GeneratedTypes previous) throws Exception {
final ClassElement element = new ClassElement(clazz);
previous.add(element);
final Object[] arr = clazz.getEnumConstants();
final StringBuilder out = new StringBuilder(); final StringBuilder out = new StringBuilder();
boolean first = true; boolean first = true;
out.append("zod.enum(["); out.append("zod.enum([");
for (final Object elem : arr) { for (final Object elem : arr) {
if (!first) { if (!first) {
out.append(", "); out.append(", \n\t");
} else {
out.append("\n\t");
} }
first = false; first = false;
out.append("\""); out.append("'");
out.append(elem.toString()); out.append(elem.toString());
out.append("\""); out.append("'");
} }
out.append("])"); out.append("\n\t]);");
return out.toString(); element.declaration = out.toString();
} previous.addOrder(element);
if (type == List.class) { return element;
return null;
}
createTable(type, previousClassesGenerated, order);
return "Zod" + type.getSimpleName();
} }
public static String convertTypeZod(final Field field, final Map<String, String> previousClassesGenerated, final List<String> order) throws Exception { public static String convertTypeZod(final Class<?> type, final GeneratedTypes previous) throws Exception {
final ClassElement previousType = previous.find(type);
if (previousType != null) {
return previousType.zodName;
}
if (type.isEnum()) {
return convertTypeZodEnum(type, previous).zodName;
}
if (type == List.class) {
throw new DataAccessException("Imcompatible type of element in object for: " + type.getCanonicalName() + " Unmanaged List of List ... ");
}
final ClassElement elemCreated = createTable(type, previous);
if (elemCreated != null) {
return elemCreated.zodName;
}
throw new DataAccessException("Imcompatible type of element in object for: " + type.getCanonicalName());
}
public static String convertTypeZod(final Field field, final GeneratedTypes previous) throws Exception {
final Class<?> type = field.getType(); final Class<?> type = field.getType();
final String simpleType = convertTypeZodSimpleType(type, previousClassesGenerated, order); final ClassElement previousType = previous.find(type);
if (simpleType != null) { if (previousType != null) {
return simpleType; return previousType.zodName;
}
if (type.isEnum()) {
return convertTypeZodEnum(type, previous).zodName;
} }
if (type == List.class) { if (type == List.class) {
final ParameterizedType listType = (ParameterizedType) field.getGenericType(); final ParameterizedType listType = (ParameterizedType) field.getGenericType();
final Class<?> listClass = (Class<?>) listType.getActualTypeArguments()[0]; final Class<?> listClass = (Class<?>) listType.getActualTypeArguments()[0];
final String simpleSubType = convertTypeZodSimpleType(listClass, previousClassesGenerated, order); final String simpleSubType = convertTypeZod(listClass, previous);
return "zod.array(" + simpleSubType + ")"; return "zod.array(" + simpleSubType + ")";
} }
final ClassElement elemCreated = createTable(type, previous);
if (elemCreated != null) {
return elemCreated.zodName;
}
throw new DataAccessException("Imcompatible type of element in object for: " + type.getCanonicalName()); throw new DataAccessException("Imcompatible type of element in object for: " + type.getCanonicalName());
} }
@ -106,8 +156,7 @@ public class DataFactoryZod {
return ".optional()"; return ".optional()";
} }
public static void createTablesSpecificType(final Field elem, final int fieldId, final StringBuilder builder, final Map<String, String> previousClassesGenerated, final List<String> order) public static void createTablesSpecificType(final Field elem, final int fieldId, final StringBuilder builder, final GeneratedTypes previous) throws Exception {
throws Exception {
final String name = elem.getName(); final String name = elem.getName();
final Class<?> classModel = elem.getType(); final Class<?> classModel = elem.getType();
final int limitSize = AnnotationTools.getLimitSize(elem); final int limitSize = AnnotationTools.getLimitSize(elem);
@ -124,7 +173,7 @@ public class DataFactoryZod {
builder.append("\n\t"); builder.append("\n\t");
builder.append(name); builder.append(name);
builder.append(": "); builder.append(": ");
builder.append(convertTypeZod(elem, previousClassesGenerated, order)); builder.append(convertTypeZod(elem, previous));
if (limitSize > 0 && classModel == String.class) { if (limitSize > 0 && classModel == String.class) {
builder.append(".max("); builder.append(".max(");
builder.append(limitSize); builder.append(limitSize);
@ -156,16 +205,39 @@ public class DataFactoryZod {
return false; return false;
} }
public static GeneratedTypes createBasicType() throws Exception {
final GeneratedTypes previous = new GeneratedTypes();
previous.add(new ClassElement(new Class<?>[] { Void.class, void.class }, "void", "void", null, null, true));
previous.add(new ClassElement(new Class<?>[] { String.class }, "zod.string()", "string", null, "zod.string()", true));
previous.add(new ClassElement(new Class<?>[] { Boolean.class, boolean.class }, "zod.boolean()", "boolean", null, "zod.boolean()", true));
previous.add(new ClassElement(new Class<?>[] { UUID.class }, "ZodUUID", "UUID", "isUUID", "zod.string().uuid()", false), true);
previous.add(new ClassElement(new Class<?>[] { Long.class, long.class }, "ZodLong", "Long", "isLong",
// "zod.bigint()",
"zod.number()", false), true);
previous.add(new ClassElement(new Class<?>[] { Integer.class, int.class }, "ZodInteger", "Integer", "isInteger", "zod.number().safe()", false), true);
previous.add(new ClassElement(new Class<?>[] { Double.class, double.class }, "ZodDouble", "Double", "isDouble", "zod.number()", true), true);
previous.add(new ClassElement(new Class<?>[] { Float.class, float.class }, "ZodFloat", "Float", "isFloat", "zod.number()", false), true);
previous.add(new ClassElement(new Class<?>[] { Instant.class }, "ZodInstant", "Instant", "isInstant", "zod.string()", false), true);
previous.add(new ClassElement(new Class<?>[] { Date.class }, "ZodDate", "Date", "isDate", "zod.date()", false), true);
previous.add(new ClassElement(new Class<?>[] { Timestamp.class }, "ZodTimestamp", "Timestamp", "isTimestamp", "zod.date()", false), true);
previous.add(new ClassElement(new Class<?>[] { LocalDate.class }, "ZodLocalDate", "LocalDate", "isLocalDate", "zod.date()", false), true);
previous.add(new ClassElement(new Class<?>[] { LocalTime.class }, "ZodLocalTime", "LocalTime", "isLocalTime", "zod.date()", false), true);
return previous;
}
/** Request the generation of the TypeScript file for the "Zod" export model /** Request the generation of the TypeScript file for the "Zod" export model
* @param classs List of class used in the model * @param classs List of class used in the model
* @return A string representing the Server models * @return A string representing the Server models
* @throws Exception */ * @throws Exception */
public static String createTables(final List<Class<?>> classs) throws Exception { public static String createTables(final List<Class<?>> classs) throws Exception {
final Map<String, String> previousClassesGenerated = new LinkedHashMap<>(); return createTables(classs, createBasicType());
final List<String> order = new ArrayList<>();
for (final Class<?> clazz : classs) {
createTable(clazz, previousClassesGenerated, order);
} }
public static String createTables(final List<Class<?>> classs, final GeneratedTypes previous) throws Exception {
for (final Class<?> clazz : classs) {
createTable(clazz, previous);
}
final StringBuilder generatedData = new StringBuilder(); final StringBuilder generatedData = new StringBuilder();
generatedData.append(""" generatedData.append("""
/** /**
@ -173,62 +245,40 @@ public class DataFactoryZod {
*/ */
import { z as zod } from \"zod\"; import { z as zod } from \"zod\";
export const ZodUUID = zod.string().uuid();
export type UUID = zod.infer<typeof ZodUUID>;
export const ZodLong = zod.bigint();
export type Long = zod.infer<typeof ZodLong>;
export const ZodInteger = zod.number().safe();
export type Integer = zod.infer<typeof ZodInteger>;
export const ZodDouble = zod.number();
export type Double = zod.infer<typeof ZodDouble>;
export const ZodFloat = zod.number();
export type Float = zod.infer<typeof ZodFloat>;
export const ZodInstant = zod.string();
export type Instant = zod.infer<typeof ZodInstant>;
export const ZodDate = zod.date();
export type Date = zod.infer<typeof ZodDate>;
export const ZodTimestamp = zod.date();
export type Timestamp = zod.infer<typeof ZodTimestamp>;
export const ZodLocalDate = zod.date();
export type LocalDate = zod.infer<typeof ZodLocalDate>;
export const ZodLocalTime = zod.date();
export type LocalTime = zod.infer<typeof ZodLocalTime>;
"""); """);
for (final String elem : order) { for (final Class<?> elem : previous.order) {
final String data = previousClassesGenerated.get(elem); final ClassElement data = previous.find(elem);
generatedData.append(data); if (!data.nativeType) {
if (data.comment != null) {
generatedData.append(data.comment);
}
generatedData.append("export const ");
generatedData.append(data.zodName);
generatedData.append(" = ");
generatedData.append(data.declaration);
generatedData.append(";");
generatedData.append(createDeclaration(data));
generatedData.append("\n\n"); generatedData.append("\n\n");
} }
}
LOGGER.info("generated: {}", generatedData.toString()); LOGGER.info("generated: {}", generatedData.toString());
return generatedData.toString(); return generatedData.toString();
} }
public static void createTable(final Class<?> clazz, final Map<String, String> previousClassesGenerated, final List<String> order) throws Exception { public static ClassElement createTable(final Class<?> clazz, final GeneratedTypes previous) throws Exception {
if (clazz == null) { if (clazz == null) {
return; return null;
}
final ClassElement alreadyExist = previous.find(clazz);
if (previous.find(clazz) != null) {
return alreadyExist;
} }
if (clazz.isPrimitive()) { if (clazz.isPrimitive()) {
return; return null;
}
if (clazz == Double.class || clazz == Float.class || clazz == Integer.class || clazz == Long.class || clazz == UUID.class || clazz == Instant.class || clazz == Date.class
|| clazz == Timestamp.class || clazz == LocalDate.class || clazz == LocalTime.class || clazz == String.class) {
return;
}
if (previousClassesGenerated.get(clazz.getCanonicalName()) != null) {
return;
} }
// add the current class to prevent multiple creation // add the current class to prevent multiple creation
previousClassesGenerated.put(clazz.getCanonicalName(), "In Generation"); final ClassElement curentElementClass = new ClassElement(clazz);
previous.add(curentElementClass);
// Local generation of class: // Local generation of class:
final StringBuilder internalBuilder = new StringBuilder(); final StringBuilder internalBuilder = new StringBuilder();
final List<String> alreadyAdded = new ArrayList<>(); final List<String> alreadyAdded = new ArrayList<>();
@ -258,62 +308,67 @@ public class DataFactoryZod {
* tableName + " field name=" + AnnotationTools.getFieldName(elem) + " type=" + elem.getType()); } fieldId++; */ * tableName + " field name=" + AnnotationTools.getFieldName(elem) + " type=" + elem.getType()); } fieldId++; */
} else { } else {
LOGGER.trace("Create type for: {} ==> {}", AnnotationTools.getFieldName(elem), elem.getType()); LOGGER.trace("Create type for: {} ==> {}", AnnotationTools.getFieldName(elem), elem.getType());
DataFactoryZod.createTablesSpecificType(elem, fieldId, internalBuilder, previousClassesGenerated, order); DataFactoryZod.createTablesSpecificType(elem, fieldId, internalBuilder, previous);
fieldId++; fieldId++;
} }
} }
final String description = AnnotationTools.getSchemaDescription(clazz); final String description = AnnotationTools.getSchemaDescription(clazz);
final String example = AnnotationTools.getSchemaExample(clazz); final String example = AnnotationTools.getSchemaExample(clazz);
final StringBuilder generatedData = new StringBuilder(); final StringBuilder generatedCommentedData = new StringBuilder();
if (description != null || example != null) { if (description != null || example != null) {
generatedData.append("/**\n"); generatedCommentedData.append("/**\n");
if (description != null) { if (description != null) {
for (final String elem : description.split("\n")) { for (final String elem : description.split("\n")) {
generatedData.append(" * "); generatedCommentedData.append(" * ");
generatedData.append(elem); generatedCommentedData.append(elem);
generatedData.append("\n"); generatedCommentedData.append("\n");
} }
} }
if (example != null) { if (example != null) {
generatedData.append(" * Example:\n"); generatedCommentedData.append(" * Example:\n");
generatedData.append(" * ```\n"); generatedCommentedData.append(" * ```\n");
for (final String elem : example.split("\n")) { for (final String elem : example.split("\n")) {
generatedData.append(" * "); generatedCommentedData.append(" * ");
generatedData.append(elem); generatedCommentedData.append(elem);
generatedData.append("\n"); generatedCommentedData.append("\n");
} }
generatedData.append(" * ```\n"); generatedCommentedData.append(" * ```\n");
} }
generatedData.append(" */\n"); generatedCommentedData.append(" */\n");
} }
generatedData.append("export const Zod"); curentElementClass.comment = generatedCommentedData.toString();
generatedData.append(clazz.getSimpleName()); final StringBuilder generatedData = new StringBuilder();
generatedData.append(" = ");
final Class<?> parentClass = clazz.getSuperclass(); final Class<?> parentClass = clazz.getSuperclass();
if (parentClass != null && parentClass != Object.class) { if (parentClass != null && parentClass != Object.class && parentClass != Record.class) {
createTable(parentClass, previousClassesGenerated, order); final ClassElement parentDeclaration = createTable(parentClass, previous);
generatedData.append("Zod"); generatedData.append(parentDeclaration.zodName);
generatedData.append(parentClass.getSimpleName());
generatedData.append(".extend({"); generatedData.append(".extend({");
} else { } else {
generatedData.append("zod.object({"); generatedData.append("zod.object({");
} }
generatedData.append(internalBuilder.toString()); generatedData.append(internalBuilder.toString());
generatedData.append("\n});"); generatedData.append("\n})");
// declare generic type: // Remove the previous to reorder the map ==> parent must be inserted before us.
curentElementClass.declaration = generatedData.toString();
previous.addOrder(curentElementClass);
return curentElementClass;
}
public static String createDeclaration(final ClassElement elem) {
final StringBuilder generatedData = new StringBuilder();
generatedData.append("\nexport type "); generatedData.append("\nexport type ");
generatedData.append(clazz.getSimpleName()); generatedData.append(elem.tsTypeName);
generatedData.append(" = zod.infer<typeof Zod"); generatedData.append(" = zod.infer<typeof ");
generatedData.append(clazz.getSimpleName()); generatedData.append(elem.zodName);
generatedData.append(">;"); generatedData.append(">;");
// declare generic isXXX: // declare generic isXXX:
generatedData.append("\nexport function is"); generatedData.append("\nexport function ");
generatedData.append(clazz.getSimpleName()); generatedData.append(elem.tsCheckType);
generatedData.append("(data: any): data is "); generatedData.append("(data: any): data is ");
generatedData.append(clazz.getSimpleName()); generatedData.append(elem.tsTypeName);
generatedData.append(" {\n\ttry {\n\t\tZod"); generatedData.append(" {\n\ttry {\n\t\t");
generatedData.append(clazz.getSimpleName()); generatedData.append(elem.zodName);
generatedData.append(""" generatedData.append("""
.parse(data); .parse(data);
return true; return true;
@ -323,9 +378,7 @@ public class DataFactoryZod {
} }
} }
"""); """);
// Remove the previous to reorder the map ==> parent must be inserted before us. return generatedData.toString();
previousClassesGenerated.put(clazz.getCanonicalName(), generatedData.toString());
order.add(clazz.getCanonicalName());
} }
public static void generatePackage(final List<Class<?>> classs, final String pathPackage) throws Exception { public static void generatePackage(final List<Class<?>> classs, final String pathPackage) throws Exception {

View File

@ -11,6 +11,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.UUID;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.kar.archidata.dataAccess.DataAccess; import org.kar.archidata.dataAccess.DataAccess;
@ -103,7 +104,7 @@ public class DataTools {
return null; return null;
} }
public static void undelete(final Long id) { public static void undelete(final UUID id) {
try { try {
DataAccess.unsetDelete(Data.class, id); DataAccess.unsetDelete(Data.class, id);
} catch (final Exception e) { } catch (final Exception e) {