From a7220e0f766ace6290ecc3239b9c1ac5fccc461e Mon Sep 17 00:00:00 2001 From: Edouard DUPIN Date: Tue, 21 May 2024 00:41:53 +0200 Subject: [PATCH] [DEV] first step to generation of API --- .../externalRestApi/TsApiGeneration.java | 248 ++++++++++++++++++ .../externalRestApi/TsClassElement.java | 10 +- .../externalRestApi/TsGenerateApi.java | 35 ++- .../externalRestApi/model/ApiModel.java | 28 +- 4 files changed, 298 insertions(+), 23 deletions(-) create mode 100644 src/org/kar/archidata/externalRestApi/TsApiGeneration.java diff --git a/src/org/kar/archidata/externalRestApi/TsApiGeneration.java b/src/org/kar/archidata/externalRestApi/TsApiGeneration.java new file mode 100644 index 0000000..0852b58 --- /dev/null +++ b/src/org/kar/archidata/externalRestApi/TsApiGeneration.java @@ -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 consumes = interfaceElement.consumes; + final List produces = interfaceElement.produces; + final boolean needGenerateProgress = interfaceElement.needGenerateProgress; + final List 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> 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> 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> 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(); + } + +} \ No newline at end of file diff --git a/src/org/kar/archidata/externalRestApi/TsClassElement.java b/src/org/kar/archidata/externalRestApi/TsClassElement.java index e191d49..cbdae4e 100644 --- a/src/org/kar/archidata/externalRestApi/TsClassElement.java +++ b/src/org/kar/archidata/externalRestApi/TsClassElement.java @@ -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 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) { diff --git a/src/org/kar/archidata/externalRestApi/TsGenerateApi.java b/src/org/kar/archidata/externalRestApi/TsGenerateApi.java index 82f83aa..fcd7ec6 100644 --- a/src/org/kar/archidata/externalRestApi/TsGenerateApi.java +++ b/src/org/kar/archidata/externalRestApi/TsGenerateApi.java @@ -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 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 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 generateApiModel(final AnalyzeApi api) { diff --git a/src/org/kar/archidata/externalRestApi/model/ApiModel.java b/src/org/kar/archidata/externalRestApi/model/ApiModel.java index 2c201ee..97858f1 100644 --- a/src/org/kar/archidata/externalRestApi/model/ApiModel.java +++ b/src/org/kar/archidata/externalRestApi/model/ApiModel.java @@ -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 returnTypes = new ArrayList<>();; // Name of the API (function name) @@ -39,12 +39,12 @@ public class ApiModel { public final Map> multiPartParameters = new HashMap<>(); // model of data available public final List unnamedElement = new ArrayList<>(); - + // Possible input type of the REST API public List consumes = new ArrayList<>(); // Possible output type of the REST API public List 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 consume, final List 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); } - + } }