[FEAT] model generation is ready

This commit is contained in:
Edouard DUPIN 2024-05-21 00:09:25 +02:00
parent be1e189d60
commit d28c31290f
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,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<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(">;\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<typeof ");
out.append(zodName);
out.append(">;\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");

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,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<ClassModel> getCompatibleModels(
final List<ClassModel> requestedModel,
final List<Class<?>> search) {
@ -30,7 +36,7 @@ public class TsGenerateApi {
}
return out;
}
public static void generateApi(final AnalyzeApi api, final String pathPackage) throws IOException {
final List<TsClassElement> 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<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

@ -11,19 +11,19 @@ import java.util.Set;
public abstract class ClassModel {
protected Class<?> originClasses = null;
protected List<ClassModel> dependencyModels = new ArrayList<>();
public Class<?> getOriginClasses() {
return this.originClasses;
}
protected boolean isCompatible(final Class<?> clazz) {
return this.originClasses == clazz;
}
public List<ClassModel> 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<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<>();
@ -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<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

@ -11,17 +11,20 @@ import jakarta.ws.rs.core.Response;
public class ModelGroup {
static final Logger LOGGER = LoggerFactory.getLogger(ModelGroup.class);
public List<ClassModel> previousModel = new ArrayList<>();
public ModelGroup() {}
public ModelGroup(final List<ClassModel> 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)) {

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;
}