From 9ac3a95060430051ddc35436b4b2291ed285f8bc Mon Sep 17 00:00:00 2001 From: Edouard DUPIN Date: Tue, 21 May 2024 00:09:25 +0200 Subject: [PATCH] [FEAT] model generation is ready --- .../externalRestApi/TsClassElement.java | 134 ++++++++++++++---- .../externalRestApi/TsGenerateApi.java | 101 ++++++++++--- .../externalRestApi/model/ClassModel.java | 20 +-- .../model/ClassObjectModel.java | 27 +++- .../externalRestApi/model/ModelGroup.java | 9 +- src/resources/zod-tools.ts | 21 +++ 6 files changed, 257 insertions(+), 55 deletions(-) create mode 100644 src/resources/zod-tools.ts diff --git a/src/org/kar/archidata/externalRestApi/TsClassElement.java b/src/org/kar/archidata/externalRestApi/TsClassElement.java index f35a999..e191d49 100644 --- a/src/org/kar/archidata/externalRestApi/TsClassElement.java +++ b/src/org/kar/archidata/externalRestApi/TsClassElement.java @@ -14,8 +14,18 @@ import org.kar.archidata.externalRestApi.model.ClassMapModel; import org.kar.archidata.externalRestApi.model.ClassModel; import org.kar.archidata.externalRestApi.model.ClassObjectModel; import org.kar.archidata.externalRestApi.model.ClassObjectModel.FieldProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class TsClassElement { + static final Logger LOGGER = LoggerFactory.getLogger(TsClassElement.class); + + public enum DefinedPosition { + NATIVE, // Native element of TS language. + BASIC, // basic wrapping for JAVA type. + NORMAL // Normal Object to interpret. + } + public List models; public String zodName; public String tsTypeName; @@ -23,11 +33,10 @@ public class TsClassElement { public String declaration; public String fileName = null; public String comment = null; - public boolean isEnum = false; - public boolean nativeType; + public DefinedPosition nativeType = DefinedPosition.NORMAL; public TsClassElement(final List model, final String zodName, final String tsTypeName, - final String tsCheckType, final String declaration, final boolean nativeType) { + final String tsCheckType, final String declaration, final DefinedPosition nativeType) { this.models = model; this.zodName = zodName; this.tsTypeName = tsTypeName; @@ -44,7 +53,6 @@ public class TsClassElement { this.tsTypeName = model.getOriginClasses().getSimpleName(); this.tsCheckType = "is" + model.getOriginClasses().getSimpleName(); this.declaration = null; - this.nativeType = false; this.fileName = this.tsTypeName.replaceAll("([a-z])([A-Z])", "$1-$2").replaceAll("([A-Z])([A-Z][a-z])", "$1-$2") .toLowerCase(); } @@ -58,7 +66,7 @@ public class TsClassElement { /** * Interface of the server (auto-generated code) */ - import { z as zod } from \"zod\"; + import { z as zod } from "zod"; """; } @@ -127,23 +135,30 @@ public class TsClassElement { out.append(this.tsTypeName); out.append(");\n"); } - out.append(generateExportCheckFunction()); + out.append(generateExportCheckFunctionWrite("")); return out.toString(); } - private Object generateExportCheckFunction() { + private String generateExportCheckFunctionWrite(final String writeString) { final StringBuilder out = new StringBuilder(); out.append("\nexport function "); out.append(this.tsCheckType); + out.append(writeString); out.append("(data: any): data is "); out.append(this.tsTypeName); + out.append(writeString); out.append(" {\n\ttry {\n\t\t"); out.append(this.zodName); + out.append(writeString); out.append(""" .parse(data); return true; } catch (e: any) { - console.log(`Fail to parse data ${e}`); + console.log(`Fail to parse data type='"""); + out.append(this.zodName); + out.append(writeString); + out.append(""" + ' error=${e}`); return false; } } @@ -156,10 +171,10 @@ public class TsClassElement { final StringBuilder out = new StringBuilder(); for (final ClassModel depModel : depModels) { final TsClassElement tsModel = tsGroup.find(depModel); - if (!tsModel.nativeType) { + if (tsModel.nativeType != DefinedPosition.NATIVE) { out.append("import {"); out.append(tsModel.zodName); - out.append("} from \""); + out.append("} from \"./"); out.append(tsModel.fileName); out.append("\";\n"); } @@ -193,6 +208,45 @@ public class TsClassElement { return out.toString(); } + public String optionalTypeZod(final FieldProperty field) { + if (field.model().getOriginClasses() == null || field.model().getOriginClasses().isPrimitive()) { + return ""; + } + return ".optional()"; + } + + public String maxSizeZod(final FieldProperty field) { + final StringBuilder builder = new StringBuilder(); + final Class clazz = field.model().getOriginClasses(); + if (field.limitSize() > 0 && clazz == String.class) { + builder.append(".max("); + builder.append(field.limitSize()); + builder.append(")"); + } + return builder.toString(); + } + + public String readOnlyZod(final FieldProperty field) { + if (field.readOnly()) { + return ".readonly()"; + } + return ""; + } + + public String generateBaseObject() { + final StringBuilder out = new StringBuilder(); + out.append(getBaseHeader()); + out.append("\n"); + + out.append("export const "); + out.append(this.zodName); + out.append(" = "); + out.append(this.declaration); + out.append(";"); + generateZodInfer(this.tsTypeName, this.zodName); + return out.toString(); + } + public String generateObject(final ClassObjectModel model, final TsClassElementGroup tsGroup) throws IOException { final StringBuilder out = new StringBuilder(); out.append(getBaseHeader()); @@ -201,7 +255,7 @@ public class TsClassElement { out.append(generateComment(model)); out.append("export const "); - out.append(this.tsTypeName); + out.append(this.zodName); out.append(" = "); if (model.getExtendsClass() != null) { @@ -216,10 +270,10 @@ public class TsClassElement { for (final FieldProperty field : model.getFields()) { final ClassModel fieldModel = field.model(); if (field.comment() != null) { - out.append("\t/*\n"); + out.append("\t/**\n"); out.append("\t * "); out.append(field.comment()); - out.append("\n\t*/\n"); + out.append("\n\t */\n"); } out.append("\t"); out.append(field.name()); @@ -234,18 +288,49 @@ public class TsClassElement { final String data = generateTsMap(fieldMapModel, tsGroup); out.append(data); } - out.append(";\n"); + out.append(maxSizeZod(field)); + out.append(readOnlyZod(field)); + out.append(optionalTypeZod(field)); + out.append(",\n"); } + final List omitField = model.getReadOnlyField(); out.append("\n});\n"); - out.append("\nexport type "); - out.append(this.tsTypeName); - out.append(" = zod.infer;\n"); - out.append(generateExportCheckFunction()); + out.append("Write = "); + out.append(this.zodName); + if (omitField.size() != 0) { + out.append(".omit({\n"); + for (final String elem : omitField) { + out.append("\t"); + out.append(elem); + out.append(": true,\n"); + } + out.append("\n})"); + } + out.append(";\n"); + out.append(generateZodInfer(this.tsTypeName + "Write", this.zodName + "Write")); + + // Check only the input value ==> no need of the output + out.append(generateExportCheckFunctionWrite("Write")); + return out.toString(); } - + + private String generateZodInfer(final String tsName, final String zodName) { + final StringBuilder out = new StringBuilder(); + out.append("\nexport type "); + out.append(tsName); + out.append(" = zod.infer;\n"); + return out.toString(); + } + private String generateTsMap(final ClassMapModel model, final TsClassElementGroup tsGroup) { final StringBuilder out = new StringBuilder(); out.append("zod.record("); @@ -308,15 +393,16 @@ public class TsClassElement { } public void generateFile(final String pathPackage, final TsClassElementGroup tsGroup) throws IOException { - if (this.nativeType) { + if (this.nativeType == DefinedPosition.NATIVE) { return; } final ClassModel model = this.models.get(0); String data = ""; - if (model instanceof final ClassEnumModel modelEnum) { + if (this.nativeType == DefinedPosition.BASIC && model instanceof final ClassObjectModel modelObject) { + data = generateBaseObject(); + } else if (model instanceof final ClassEnumModel modelEnum) { data = generateEnum(modelEnum, tsGroup); - } - if (model instanceof final ClassObjectModel modelObject) { + } else if (model instanceof final ClassObjectModel modelObject) { data = generateObject(modelObject, tsGroup); } final Path path = Paths.get(pathPackage + File.separator + "model"); diff --git a/src/org/kar/archidata/externalRestApi/TsGenerateApi.java b/src/org/kar/archidata/externalRestApi/TsGenerateApi.java index e66b930..82f83aa 100644 --- a/src/org/kar/archidata/externalRestApi/TsGenerateApi.java +++ b/src/org/kar/archidata/externalRestApi/TsGenerateApi.java @@ -1,7 +1,11 @@ package org.kar.archidata.externalRestApi; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.sql.Timestamp; import java.time.Instant; import java.time.LocalDate; @@ -12,10 +16,12 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import org.kar.archidata.dataAccess.DataFactoryTsApi; +import org.kar.archidata.externalRestApi.TsClassElement.DefinedPosition; import org.kar.archidata.externalRestApi.model.ClassModel; public class TsGenerateApi { - + public static List getCompatibleModels( final List requestedModel, final List> search) { @@ -30,7 +36,7 @@ public class TsGenerateApi { } return out; } - + public static void generateApi(final AnalyzeApi api, final String pathPackage) throws IOException { final List localModel = generateApiModel(api); final TsClassElementGroup tsGroup = new TsClassElementGroup(localModel); @@ -38,6 +44,31 @@ public class TsGenerateApi { for (final TsClassElement element : localModel) { element.generateFile(pathPackage, tsGroup); } + createIndex(pathPackage, tsGroup); + + 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 { + final StringBuilder out = new StringBuilder(""" + /** + * Interface of the server (auto-generated code) + */ + """); + for (final TsClassElement elem : tsGroup.getTsElements()) { + if (elem.nativeType == DefinedPosition.NATIVE) { + continue; + } + out.append("export * from \"./"); + out.append(elem.fileName); + out.append("\"\n"); + } + final FileWriter myWriter = new FileWriter( + pathPackage + File.separator + "model" + File.separator + "index.ts"); + myWriter.write(out.toString()); + myWriter.close(); + } private static List generateApiModel(final AnalyzeApi api) { @@ -45,72 +76,87 @@ public class TsGenerateApi { final List tsModels = new ArrayList<>(); List models = getCompatibleModels(api.classModels, List.of(Void.class, void.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "void", "void", null, null, true)); + tsModels.add(new TsClassElement(models, "void", "void", null, null, DefinedPosition.NATIVE)); + } + models = getCompatibleModels(api.classModels, List.of(Object.class)); + if (models != null) { + tsModels.add( + new TsClassElement(models, "zod.object()", "object", null, "zod.object()", DefinedPosition.NATIVE)); } // Map is binded to any ==> can not determine this complex model for now models = getCompatibleModels(api.classModels, List.of(Map.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "any", "any", null, null, true)); + tsModels.add(new TsClassElement(models, "zod.any()", "any", null, null, DefinedPosition.NATIVE)); } models = getCompatibleModels(api.classModels, List.of(String.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "zod.string()", "string", null, "zod.string()", true)); + tsModels.add( + new TsClassElement(models, "zod.string()", "string", null, "zod.string()", DefinedPosition.NATIVE)); } models = getCompatibleModels(api.classModels, List.of(InputStream.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "z.instanceof(File)", "File", null, "z.instanceof(File)", true)); + tsModels.add(new TsClassElement(models, "z.instanceof(File)", "File", null, "z.instanceof(File)", + DefinedPosition.NATIVE)); } models = getCompatibleModels(api.classModels, List.of(Boolean.class, boolean.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "zod.boolean()", "boolean", null, "zod.boolean()", true)); + tsModels.add(new TsClassElement(models, "zod.boolean()", "boolean", null, "zod.boolean()", + DefinedPosition.NATIVE)); } models = getCompatibleModels(api.classModels, List.of(UUID.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "ZodUUID", "UUID", "isUUID", "zod.string().uuid()", false)); + tsModels.add(new TsClassElement(models, "ZodUUID", "UUID", "isUUID", "zod.string().uuid()", + DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(Long.class, long.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "ZodLong", "Long", "isLong", "zod.number()", false)); + tsModels.add( + new TsClassElement(models, "ZodLong", "Long", "isLong", "zod.number()", DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(Short.class, short.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "ZodShort", "Short", "isShort", "zod.number().safe()", true)); + tsModels.add(new TsClassElement(models, "ZodShort", "Short", "isShort", "zod.number().safe()", + DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(Integer.class, int.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "ZodInteger", "Integer", "isInteger", "zod.number().safe()", true)); + tsModels.add(new TsClassElement(models, "ZodInteger", "Integer", "isInteger", "zod.number().safe()", + DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(Double.class, double.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "ZodDouble", "Double", "isDouble", "zod.number()", true)); + tsModels.add(new TsClassElement(models, "ZodDouble", "Double", "isDouble", "zod.number()", + DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(Float.class, float.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "ZodFloat", "Float", "isFloat", "zod.number()", false)); + tsModels.add( + new TsClassElement(models, "ZodFloat", "Float", "isFloat", "zod.number()", DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(Instant.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "ZodInstant", "Instant", "isInstant", "zod.string()", false)); + tsModels.add(new TsClassElement(models, "ZodInstant", "Instant", "isInstant", "zod.string()", + DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(Date.class)); if (models != null) { - tsModels.add(new TsClassElement(models, "ZodDate", "Date", "isDate", - "zod.string().datetime({ precision: 3 })", false)); + tsModels.add(new TsClassElement(models, "ZodIsoDate", "IsoDate", "isIsoDate", + "zod.string().datetime({ precision: 3 })", DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(Timestamp.class)); if (models != null) { tsModels.add(new TsClassElement(models, "ZodTimestamp", "Timestamp", "isTimestamp", - "zod.string().datetime({ precision: 3 })", false)); + "zod.string().datetime({ precision: 3 })", DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(LocalDate.class)); if (models != null) { tsModels.add(new TsClassElement(models, "ZodLocalDate", "LocalDate", "isLocalDate", "zod.string().date()", - false)); + DefinedPosition.BASIC)); } models = getCompatibleModels(api.classModels, List.of(LocalTime.class)); if (models != null) { tsModels.add(new TsClassElement(models, "ZodLocalTime", "LocalTime", "isLocalTime", "zod.string().time()", - false)); + DefinedPosition.BASIC)); } for (final ClassModel model : api.classModels) { boolean alreadyExist = false; @@ -126,5 +172,22 @@ public class TsGenerateApi { tsModels.add(new TsClassElement(model)); } return tsModels; + + } + + public static void copyResourceFile(final String name, final String destinationPath) throws IOException { + final InputStream ioStream = DataFactoryTsApi.class.getClassLoader().getResourceAsStream(name); + if (ioStream == null) { + throw new IllegalArgumentException("rest-tools.ts is not found"); + } + final BufferedReader buffer = new BufferedReader(new InputStreamReader(ioStream)); + final FileWriter myWriter = new FileWriter(destinationPath); + String line; + while ((line = buffer.readLine()) != null) { + myWriter.write(line); + myWriter.write("\n"); + } + ioStream.close(); + myWriter.close(); } } diff --git a/src/org/kar/archidata/externalRestApi/model/ClassModel.java b/src/org/kar/archidata/externalRestApi/model/ClassModel.java index dbd621b..b2f2620 100644 --- a/src/org/kar/archidata/externalRestApi/model/ClassModel.java +++ b/src/org/kar/archidata/externalRestApi/model/ClassModel.java @@ -11,19 +11,19 @@ import java.util.Set; public abstract class ClassModel { protected Class originClasses = null; protected List dependencyModels = new ArrayList<>(); - + public Class getOriginClasses() { return this.originClasses; } - + protected boolean isCompatible(final Class clazz) { return this.originClasses == clazz; } - + public List getDependencyModels() { return this.dependencyModels; } - + public static ClassModel getModel(final Type type, final ModelGroup previousModel) throws IOException { if (type instanceof final ParameterizedType paramType) { final Type[] typeArguments = paramType.getActualTypeArguments(); @@ -37,7 +37,7 @@ public abstract class ClassModel { } return previousModel.add((Class) type); } - + public static ClassModel getModelBase( final Class clazz, final Type parameterizedType, @@ -53,7 +53,7 @@ public abstract class ClassModel { */ return getModel(parameterizedType, previousModel); } - + public static ClassModel getModel(final Class type, final ModelGroup previousModel) throws IOException { if (type == List.class) { throw new IOException("Fail to manage parametrized type..."); @@ -63,9 +63,13 @@ public abstract class ClassModel { } return previousModel.add(type); } - + public abstract void analyze(final ModelGroup group) throws Exception; - + public abstract Set getAlls(); + public List getReadOnlyField() { + return List.of(); + } + } diff --git a/src/org/kar/archidata/externalRestApi/model/ClassObjectModel.java b/src/org/kar/archidata/externalRestApi/model/ClassObjectModel.java index b28520a..6156c7e 100644 --- a/src/org/kar/archidata/externalRestApi/model/ClassObjectModel.java +++ b/src/org/kar/archidata/externalRestApi/model/ClassObjectModel.java @@ -1,7 +1,11 @@ package org.kar.archidata.externalRestApi.model; import java.lang.reflect.Field; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Set; @@ -109,6 +113,13 @@ public class ClassObjectModel extends ClassModel { if (this.isPrimitive) { return; } + final List> basicClass = List.of(Void.class, void.class, Character.class, char.class, Short.class, + short.class, Integer.class, int.class, Long.class, long.class, Float.class, float.class, Double.class, + double.class, Date.class, Timestamp.class, LocalDate.class, LocalTime.class); + if (basicClass.contains(clazz)) { + return; + } + // Local generation of class: LOGGER.trace("parse class: '{}'", clazz.getCanonicalName()); final List alreadyAdded = new ArrayList<>(); @@ -137,7 +148,7 @@ public class ClassObjectModel extends ClassModel { this.fields.add(new FieldProperty(elem, previous)); } this.name = clazz.getName(); - + final String[] elems = this.name.split("\\$"); if (elems.length == 2) { LOGGER.warn("Can have conflict in generation: {} (Remove class path) ==> {}", this.name, elems[1]); @@ -158,4 +169,18 @@ public class ClassObjectModel extends ClassModel { return Set.of(this); } + @Override + public List getReadOnlyField() { + final List out = new ArrayList<>(); + for (final FieldProperty field : this.fields) { + if (field.readOnly()) { + out.add(field.name); + } + } + if (this.extendsClass != null) { + out.addAll(this.extendsClass.getReadOnlyField()); + } + return out; + } + } diff --git a/src/org/kar/archidata/externalRestApi/model/ModelGroup.java b/src/org/kar/archidata/externalRestApi/model/ModelGroup.java index 25d6c94..ef78118 100644 --- a/src/org/kar/archidata/externalRestApi/model/ModelGroup.java +++ b/src/org/kar/archidata/externalRestApi/model/ModelGroup.java @@ -11,17 +11,20 @@ import jakarta.ws.rs.core.Response; public class ModelGroup { static final Logger LOGGER = LoggerFactory.getLogger(ModelGroup.class); public List previousModel = new ArrayList<>(); - + public ModelGroup() {} - + public ModelGroup(final List models) { this.previousModel = models; } - + public ClassModel add(Class clazz) { if (clazz == Response.class) { clazz = Object.class; } + if (clazz == Number.class) { + return null; + } //LOGGER.trace("Search element {}", clazz.getCanonicalName()); for (final ClassModel value : this.previousModel) { if (value.isCompatible(clazz)) { diff --git a/src/resources/zod-tools.ts b/src/resources/zod-tools.ts new file mode 100644 index 0000000..d92f79c --- /dev/null +++ b/src/resources/zod-tools.ts @@ -0,0 +1,21 @@ +/** @file + * @author Edouard DUPIN + * @copyright 2024, Edouard DUPIN, all right reserved + * @license MPL-2 + */ + +import { z as zod, ZodTypeAny, ZodObject } from 'zod'; + +export function removeReadonly(schema: T): T { + if (schema instanceof ZodObject) { + const shape: Record = {}; + for (const key in schema.shape) { + const field = schema.shape[key]; + shape[key] = field._def.typeName === 'ZodReadonly' + ? field._def.innerType + : removeReadonly(field); + } + return zod.object(shape) as T; + } + return schema; +} \ No newline at end of file