package cz.habarta.typescript.generator.ext; import cz.habarta.typescript.generator.Settings; import cz.habarta.typescript.generator.TsType; import cz.habarta.typescript.generator.emitter.EmitterExtension; import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures; import cz.habarta.typescript.generator.emitter.TsBeanModel; import cz.habarta.typescript.generator.emitter.TsModel; import cz.habarta.typescript.generator.emitter.TsPropertyModel; import java.util.*; /** * Emitter which generates type-safe property path getters. * * Many javascript frameworks require that you specify "property paths" * to extract data from objects. For instance, if you have a data model * which is an array of items, and you want to display them in a grid, * you can give column specifications as strings, like "field1.field2". * With this emitter you can specify such paths like so: * {@code ClassName.field1.field2.get()} * Once you call {@code get()}, you get a string * (in this case "field1.field2") */ public class BeanPropertyPathExtension extends EmitterExtension { @Override public EmitterExtensionFeatures getFeatures() { final EmitterExtensionFeatures features = new EmitterExtensionFeatures(); features.generatesRuntimeCode = true; return features; } @Override public void emitElements(Writer writer, Settings settings, boolean exportKeyword, TsModel model) { emitFieldsClass(writer, settings); Set<TsBeanModel> emittedBeans = new HashSet<>(); for (TsBeanModel bean : model.getBeans()) { emittedBeans.addAll( writeBeanAndParentsFieldSpecs(writer, settings, model, emittedBeans, bean)); } for (TsBeanModel bean : model.getBeans()) { createBeanFieldConstant(writer, exportKeyword, bean); } } private static void emitFieldsClass(Writer writer, Settings settings) { List<String> fieldsClassLines = Arrays.asList( "export class Fields {", " protected $$parent: Fields | undefined;", " protected $$name: string;", " constructor(parent?: Fields, name?: string) {", " this.$$parent = parent;", " this.$$name = name || '';", " };", " get(): string {", " if (this.$$parent && this.$$parent.get().length > 0) {", " return this.$$parent.get() + \".\" + this.$$name;", " } else {", " return this.$$name;", " }", " }", "}"); writer.writeIndentedLine(""); for (String fieldsClassLine : fieldsClassLines) { writer.writeIndentedLine(fieldsClassLine.replace(" ", settings.indentString)); } } /** * Emits a bean and its parent beans before if needed. * Returns the list of beans that were emitted. */ private static Set<TsBeanModel> writeBeanAndParentsFieldSpecs( Writer writer, Settings settings, TsModel model, Set<TsBeanModel> emittedSoFar, TsBeanModel bean) { if (emittedSoFar.contains(bean)) { return new HashSet<>(); } final TsBeanModel parentBean = getBeanModelByType(model, bean.getParent()); final Set<TsBeanModel> emittedBeans = parentBean != null ? writeBeanAndParentsFieldSpecs(writer, settings, model, emittedSoFar, parentBean) : new HashSet<TsBeanModel>(); final String parentClassName = parentBean != null ? getBeanModelClassName(parentBean) + "Fields" : "Fields"; writer.writeIndentedLine(""); writer.writeIndentedLine( "class " + getBeanModelClassName(bean) + "Fields extends " + parentClassName + " {"); writer.writeIndentedLine( settings.indentString + "constructor(parent?: Fields, name?: string) { super(parent, name); }"); for (TsPropertyModel property : bean.getProperties()) { writeBeanProperty(writer, settings, model, bean, property); } writer.writeIndentedLine("}"); emittedBeans.add(bean); return emittedBeans; } /** * is this type an 'original' TS type, or a contextual information? * null, undefined and optional info are not original types, everything * else is original */ private static boolean isOriginalTsType(TsType type) { if (type instanceof TsType.BasicType) { TsType.BasicType basicType = (TsType.BasicType)type; return !(basicType.name.equals("null") || basicType.name.equals("undefined")); } return true; } /** * If the type is optional of number|null|undefined, or list of * of integer, we want to be able to recognize it as number * to link the member to another class. * => extract the original type while ignoring the |null|undefined * and optional informations. */ private static TsType extractOriginalTsType(TsType type) { if (type instanceof TsType.OptionalType) { return extractOriginalTsType(((TsType.OptionalType)type).type); } if (type instanceof TsType.UnionType) { TsType.UnionType union = (TsType.UnionType)type; List<TsType> originalTypes = new ArrayList<>(); for (TsType curType : union.types) { if (isOriginalTsType(curType)) { originalTypes.add(curType); } } return originalTypes.size() == 1 ? extractOriginalTsType(originalTypes.get(0)) : type; } if (type instanceof TsType.BasicArrayType) { return extractOriginalTsType(((TsType.BasicArrayType)type).elementType); } return type; } private static TsBeanModel getBeanModelByType(TsModel model, TsType type) { TsType originalType = extractOriginalTsType(type); if (!(originalType instanceof TsType.ReferenceType)) { return null; } TsType.ReferenceType originalTypeBean = (TsType.ReferenceType)originalType; for (TsBeanModel curBean : model.getBeans()) { if (curBean.getName().equals(originalTypeBean.symbol)) { return curBean; } } return null; } private static String getBeanModelClassName(TsBeanModel bean) { return bean.getName().getSimpleName(); } private static void writeBeanProperty( Writer writer, Settings settings, TsModel model, TsBeanModel bean, TsPropertyModel property) { TsBeanModel fieldBeanModel = getBeanModelByType(model, property.getTsType()); String fieldClassName = fieldBeanModel != null ? getBeanModelClassName(fieldBeanModel) : ""; // if a class has a field of its own type, we get stackoverflow exception if (fieldClassName.equals(bean.getName().getSimpleName())) { fieldClassName = ""; } writer.writeIndentedLine( settings.indentString + property.getName() + " = new " + fieldClassName + "Fields(this, \"" + property.getName() + "\");"); } private static void createBeanFieldConstant(Writer writer, boolean exportKeyword, TsBeanModel bean) { writer.writeIndentedLine((exportKeyword ? "export " : "") + "const " + getBeanModelClassName(bean) + " = new " + getBeanModelClassName(bean) + "Fields();"); } }