[FEAT] model generation is ready

This commit is contained in:
Edouard DUPIN 2024-05-21 00:09:25 +02:00
parent dc022abd2d
commit 9ac3a95060
6 changed files with 257 additions and 55 deletions

View File

@ -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<ClassModel> 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<ClassModel> 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,15 +288,46 @@ 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<String> omitField = model.getReadOnlyField();
out.append("\n});\n");
out.append("\nexport type ");
out.append(this.tsTypeName);
out.append(" = zod.infer<typeof ");
out.append(generateZodInfer(this.tsTypeName, this.zodName));
out.append(generateExportCheckFunctionWrite(""));
// Generate the Write Type associated.
out.append("\nexport const ");
out.append(this.zodName);
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<typeof ");
out.append(zodName);
out.append(">;\n");
out.append(generateExportCheckFunction());
return out.toString();
}
@ -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");

View File

@ -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,6 +16,8 @@ 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 {
@ -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<TsClassElement> generateApiModel(final AnalyzeApi api) {
@ -45,72 +76,87 @@ public class TsGenerateApi {
final List<TsClassElement> tsModels = new ArrayList<>();
List<ClassModel> 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();
}
}

View File

@ -68,4 +68,8 @@ public abstract class ClassModel {
public abstract Set<ClassModel> getAlls();
public List<String> getReadOnlyField() {
return List.of();
}
}

View File

@ -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<Class<?>> 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<String> alreadyAdded = new ArrayList<>();
@ -158,4 +169,18 @@ public class ClassObjectModel extends ClassModel {
return Set.of(this);
}
@Override
public List<String> getReadOnlyField() {
final List<String> 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;
}
}

View File

@ -22,6 +22,9 @@ public class ModelGroup {
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)) {

View File

@ -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<T extends ZodTypeAny>(schema: T): T {
if (schema instanceof ZodObject) {
const shape: Record<string, ZodTypeAny> = {};
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;
}