[DEV] first step to generation of API

This commit is contained in:
Edouard DUPIN 2024-05-21 00:41:53 +02:00
parent d28c31290f
commit a7220e0f76
4 changed files with 298 additions and 23 deletions

View File

@ -0,0 +1,248 @@
package org.kar.archidata.externalRestApi;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import java.util.Map.Entry;
import org.kar.archidata.dataAccess.DataExport;
import org.kar.archidata.externalRestApi.model.ApiGroupModel;
import org.kar.archidata.externalRestApi.model.ApiModel;
import org.kar.archidata.externalRestApi.model.ClassModel;
import org.kar.archidata.externalRestApi.model.RestTypeRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.ws.rs.core.MediaType;
public class TsApiGeneration {
static final Logger LOGGER = LoggerFactory.getLogger(TsApiGeneration.class);
public static String getBaseHeader() {
return """
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
ModelResponseHttp,
RESTCallbacks,
RESTConfig,
RESTRequestJson,
RESTRequestJsonArray,
RESTRequestVoid
} from "../rest-tools"
""";
}
public static void generateApiFile(
final ApiGroupModel element,
final String pathPackage,
final TsClassElementGroup tsGroup) throws IOException {
final StringBuilder data = new StringBuilder();
data.append(getBaseHeader());
data.append("export namespace ");
data.append(element.name);
data.append(" {\n");
for (final ApiModel interfaceElement : element.interfaces) {
final String methodName = interfaceElement.name;
final String methodPath = interfaceElement.restEndPoint;
final RestTypeRequest methodType = interfaceElement.restTypeRequest;
final List<String> consumes = interfaceElement.consumes;
final List<String> produces = interfaceElement.produces;
final boolean needGenerateProgress = interfaceElement.needGenerateProgress;
final List<ClassModel> returnTypeModel = interfaceElement.returnTypes;
if (interfaceElement.description != null) {
data.append("\n\t/**\n\t * ");
data.append(interfaceElement.description);
data.append("\n\t */");
}
data.append("\n\texport function ");
data.append(methodName);
data.append("({\n\t\t\trestConfig,");
if (!interfaceElement.queries.isEmpty()) {
data.append("\n\t\t\tqueries,");
}
if (!interfaceElement.parameters.isEmpty()) {
data.append("\n\t\t\tparams,");
}
if (produces != null && produces.size() > 1) {
data.append("\n\t\t\tproduce,");
}
if (interfaceElement.unnamedElement.size() == 1 || interfaceElement.multiPartParameters.size() != 0) {
data.append("\n\t\t\tdata,");
}
if (needGenerateProgress) {
data.append("\n\t\t\tcallback,");
}
data.append("\n\t\t}: {");
data.append("\n\t\trestConfig: RESTConfig,");
if (!interfaceElement.queries.isEmpty()) {
data.append("\n\t\tqueries: {");
for (final Entry<String, List<ClassModel>> queryEntry : interfaceElement.queries.entrySet()) {
data.append("\n\t\t\t");
data.append(queryEntry.getKey());
data.append("?: ");
data.append(queryEntry.getValue());
data.append(",");
}
data.append("\n\t\t},");
}
if (!interfaceElement.parameters.isEmpty()) {
data.append("\n\t\tparams: {");
for (final Entry<String, List<ClassModel>> pathEntry : interfaceElement.parameters.entrySet()) {
data.append("\n\t\t\t");
data.append(pathEntry.getKey());
data.append(": ");
data.append(pathEntry.getValue());
data.append(",");
}
data.append("\n\t\t},");
}
if (interfaceElement.unnamedElement.size() == 1) {
data.append("\n\t\tdata: ");
data.append(interfaceElement.unnamedElement.get(0));
data.append(",");
} else if (interfaceElement.multiPartParameters.size() != 0) {
data.append("\n\t\tdata: {");
for (final Entry<String, List<ClassModel>> pathEntry : interfaceElement.multiPartParameters
.entrySet()) {
data.append("\n\t\t\t");
data.append(pathEntry.getKey());
data.append(": ");
data.append(pathEntry.getValue());
data.append(",");
}
data.append("\n\t\t},");
}
if (produces != null && produces.size() > 1) {
data.append("\n\t\tproduce: ");
String isFist = null;
for (final String elem : produces) {
String lastElement = null;
if (MediaType.APPLICATION_JSON.equals(elem)) {
lastElement = "HTTPMimeType.JSON";
}
if (MediaType.MULTIPART_FORM_DATA.equals(elem)) {
lastElement = "HTTPMimeType.MULTIPART";
}
if (DataExport.CSV_TYPE.equals(elem)) {
lastElement = "HTTPMimeType.CSV";
}
if (lastElement != null) {
if (isFist == null) {
isFist = lastElement;
} else {
data.append(" | ");
}
data.append(lastElement);
} else {
LOGGER.error("Unmanaged model type: {}", elem);
}
}
data.append(",");
}
if (needGenerateProgress) {
data.append("\n\t\tcallback?: RESTCallbacks,");
}
data.append("\n\t}): Promise<");
/**
if (interfaceElement.returnTypes.size() == 0 //
|| tmpReturn.get(0).tsTypeName == null //
|| tmpReturn.get(0).tsTypeName.equals("void")) {
data.append("void");
} else {
data.append(ApiTool.convertInTypeScriptType(tmpReturn, returnModelIsArray));
}
*/
data.append("> {");
/**
if (tmpReturn.size() == 0 //
|| tmpReturn.get(0).tsTypeName == null //
|| tmpReturn.get(0).tsTypeName.equals("void")) {
data.append("\n\t\treturn RESTRequestVoid({");
} else if (returnModelIsArray) {
data.append("\n\t\treturn RESTRequestJsonArray({");
} else {
data.append("\n\t\treturn RESTRequestJson({");
}
*/
data.append("\n\t\t\trestModel: {");
data.append("\n\t\t\t\tendPoint: \"");
data.append(interfaceElement.restEndPoint);
data.append("\",");
data.append("\n\t\t\t\trequestType: HTTPRequestModel.");
data.append(methodType);
data.append(",");
if (consumes != null) {
for (final String elem : consumes) {
if (MediaType.APPLICATION_JSON.equals(elem)) {
data.append("\n\t\t\t\tcontentType: HTTPMimeType.JSON,");
break;
} else if (MediaType.MULTIPART_FORM_DATA.equals(elem)) {
data.append("\n\t\t\t\tcontentType: HTTPMimeType.MULTIPART,");
break;
} else if (MediaType.TEXT_PLAIN.equals(elem)) {
data.append("\n\t\t\t\tcontentType: HTTPMimeType.TEXT_PLAIN,");
break;
}
}
} else if ("DELETE".equals(methodType)) {
data.append("\n\t\t\t\tcontentType: HTTPMimeType.TEXT_PLAIN,");
}
if (produces != null) {
if (produces.size() > 1) {
data.append("\n\t\t\t\taccept: produce,");
} else {
for (final String elem : produces) {
if (MediaType.APPLICATION_JSON.equals(elem)) {
data.append("\n\t\t\t\taccept: HTTPMimeType.JSON,");
break;
}
}
}
}
data.append("\n\t\t\t},");
data.append("\n\t\t\trestConfig,");
if (!interfaceElement.parameters.isEmpty()) {
data.append("\n\t\t\tparams,");
}
if (!interfaceElement.queries.isEmpty()) {
data.append("\n\t\t\tqueries,");
}
if (interfaceElement.unnamedElement.size() == 1) {
data.append("\n\t\t\tdata,");
} else if (interfaceElement.multiPartParameters.size() != 0) {
data.append("\n\t\t\tdata,");
}
if (needGenerateProgress) {
data.append("\n\t\t\tcallback,");
}
data.append("\n\t\t}");
/**
if (tmpReturn.size() != 0 && tmpReturn.get(0).tsTypeName != null
&& !tmpReturn.get(0).tsTypeName.equals("void")) {
data.append(", ");
// TODO: correct this it is really bad ...
data.append(ApiTool.convertInTypeScriptCheckType(tmpReturn));
}
**/
data.append(");");
data.append("\n\t};");
}
data.append("\n}\n");
final String fileName = TsClassElement.determineFileName(element.name);
final FileWriter myWriter = new FileWriter(
pathPackage + File.separator + "api" + File.separator + fileName + ".ts");
myWriter.write(data.toString());
myWriter.close();
}
}

View File

@ -35,6 +35,10 @@ public class TsClassElement {
public String comment = null;
public DefinedPosition nativeType = DefinedPosition.NORMAL;
public static String determineFileName(final String className) {
return className.replaceAll("([a-z])([A-Z])", "$1-$2").replaceAll("([A-Z])([A-Z][a-z])", "$1-$2").toLowerCase();
}
public TsClassElement(final List<ClassModel> model, final String zodName, final String tsTypeName,
final String tsCheckType, final String declaration, final DefinedPosition nativeType) {
this.models = model;
@ -43,8 +47,7 @@ public class TsClassElement {
this.tsCheckType = tsCheckType;
this.declaration = declaration;
this.nativeType = nativeType;
this.fileName = tsTypeName.replaceAll("([a-z])([A-Z])", "$1-$2").replaceAll("([A-Z])([A-Z][a-z])", "$1-$2")
.toLowerCase();
this.fileName = determineFileName(tsTypeName);
}
public TsClassElement(final ClassModel model) {
@ -53,8 +56,7 @@ public class TsClassElement {
this.tsTypeName = model.getOriginClasses().getSimpleName();
this.tsCheckType = "is" + model.getOriginClasses().getSimpleName();
this.declaration = null;
this.fileName = this.tsTypeName.replaceAll("([a-z])([A-Z])", "$1-$2").replaceAll("([A-Z])([A-Z][a-z])", "$1-$2")
.toLowerCase();
this.fileName = determineFileName(this.tsTypeName);
}
public boolean isCompatible(final ClassModel model) {

View File

@ -18,6 +18,7 @@ import java.util.UUID;
import org.kar.archidata.dataAccess.DataFactoryTsApi;
import org.kar.archidata.externalRestApi.TsClassElement.DefinedPosition;
import org.kar.archidata.externalRestApi.model.ApiGroupModel;
import org.kar.archidata.externalRestApi.model.ClassModel;
public class TsGenerateApi {
@ -40,17 +41,42 @@ public class TsGenerateApi {
public static void generateApi(final AnalyzeApi api, final String pathPackage) throws IOException {
final List<TsClassElement> localModel = generateApiModel(api);
final TsClassElementGroup tsGroup = new TsClassElementGroup(localModel);
// Generates all files
// Generates all MODEL files
for (final TsClassElement element : localModel) {
element.generateFile(pathPackage, tsGroup);
}
createIndex(pathPackage, tsGroup);
// Generate index of model files
createModelIndex(pathPackage, tsGroup);
for (final ApiGroupModel element : api.apiModels) {
TsApiGeneration.generateApiFile(element, pathPackage, tsGroup);
}
// Generate index of model files
createResourceIndex(pathPackage, api.apiModels);
copyResourceFile("rest-tools.ts", pathPackage + File.separator + "rest-tools.ts");
//copyResourceFile("zod-tools.ts", pathPackage + File.separator + "zod-tools.ts");
}
private static void createIndex(final String pathPackage, final TsClassElementGroup tsGroup) throws IOException {
private static void createResourceIndex(final String pathPackage, final List<ApiGroupModel> apiModels)
throws IOException {
final StringBuilder out = new StringBuilder("""
/**
* Interface of the server (auto-generated code)
*/
""");
for (final ApiGroupModel elem : apiModels) {
final String fileName = TsClassElement.determineFileName(elem.name);
out.append("export * from \"./");
out.append(fileName);
out.append("\"\n");
}
final FileWriter myWriter = new FileWriter(pathPackage + File.separator + "api" + File.separator + "index.ts");
myWriter.write(out.toString());
myWriter.close();
}
private static void createModelIndex(final String pathPackage, final TsClassElementGroup tsGroup)
throws IOException {
final StringBuilder out = new StringBuilder("""
/**
* Interface of the server (auto-generated code)
@ -68,7 +94,6 @@ public class TsGenerateApi {
pathPackage + File.separator + "model" + File.separator + "index.ts");
myWriter.write(out.toString());
myWriter.close();
}
private static List<TsClassElement> generateApiModel(final AnalyzeApi api) {

View File

@ -14,10 +14,10 @@ import org.slf4j.LoggerFactory;
public class ApiModel {
static final Logger LOGGER = LoggerFactory.getLogger(ApiModel.class);
Class<?> originClass;
Method orignMethod;
// Name of the REST end-point name
public String restEndPoint;
// Type of the request:
@ -25,8 +25,8 @@ public class ApiModel {
// Description of the API
public String description;
// need to generate the progression of stream (if possible)
boolean needGenerateProgress;
public boolean needGenerateProgress;
// List of types returned by the API
public List<ClassModel> returnTypes = new ArrayList<>();;
// Name of the API (function name)
@ -39,12 +39,12 @@ public class ApiModel {
public final Map<String, List<ClassModel>> multiPartParameters = new HashMap<>();
// model of data available
public final List<ClassModel> unnamedElement = new ArrayList<>();
// Possible input type of the REST API
public List<String> consumes = new ArrayList<>();
// Possible output type of the REST API
public List<String> produces = new ArrayList<>();
private void updateReturnTypes(final Method method, final ModelGroup previousModel) throws Exception {
// get return type from the user specification:
final Class<?>[] returnTypeModel = ApiTool.apiAnnotationGetAsyncType(method);
@ -66,7 +66,7 @@ public class ApiModel {
}
return;
}
final Class<?> returnTypeModelRaw = method.getReturnType();
LOGGER.info("Get return Type RAW = {}", returnTypeModelRaw.getCanonicalName());
if (returnTypeModelRaw == Map.class) {
@ -92,12 +92,12 @@ public class ApiModel {
LOGGER.warn(" - {}", elem);
}
}
public ApiModel(final Class<?> clazz, final Method method, final String baseRestEndPoint,
final List<String> consume, final List<String> produce, final ModelGroup previousModel) throws Exception {
this.originClass = clazz;
this.orignMethod = method;
String tmpPath = ApiTool.apiAnnotationGetPath(method);
if (tmpPath == null) {
tmpPath = "";
@ -105,19 +105,19 @@ public class ApiModel {
this.restEndPoint = baseRestEndPoint + "/" + tmpPath;
this.restTypeRequest = ApiTool.apiAnnotationGetTypeRequest2(method);
this.name = method.getName();
this.description = ApiTool.apiAnnotationGetOperationDescription(method);
this.consumes = ApiTool.apiAnnotationGetConsumes2(consume, method);
this.produces = ApiTool.apiAnnotationProduces2(produce, method);
LOGGER.trace(" [{}] {} => {}/{}", baseRestEndPoint, this.name, this.restEndPoint);
this.needGenerateProgress = ApiTool.apiAnnotationTypeScriptProgress(method);
updateReturnTypes(method, previousModel);
LOGGER.trace(" return: {}", this.returnTypes.size());
for (final ClassModel elem : this.returnTypes) {
LOGGER.trace(" - {}", elem);
}
// LOGGER.info(" Parameters:");
for (final Parameter parameter : method.getParameters()) {
// Security context are internal parameter (not available from API)
@ -142,7 +142,7 @@ public class ApiModel {
} else {
parameterModel.add(previousModel.add(parameterType));
}
final String pathParam = ApiTool.apiAnnotationGetPathParam(parameter);
final String queryParam = ApiTool.apiAnnotationGetQueryParam(parameter);
final String formDataParam = ApiTool.apiAnnotationGetFormDataParam(parameter);
@ -159,6 +159,6 @@ public class ApiModel {
if (this.unnamedElement.size() > 1) {
throw new IOException("Can not parse the API, enmpty element is more than 1 in " + this.name);
}
}
}