From 8be0aec28779305055e6350def8f3cc91e82ae70 Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Fri, 13 Nov 2026 20:06:26 +0100 Subject: [PATCH] Generate typed Java ViewModels and discover declarative registry defaults --- README.md | 2 +- .../fxdesigner/kit/ProjectionHostChecks.java | 20 ++ designer-model-processor/pom.xml | 2 + .../processor/ModelMetadataProcessor.java | 78 ++++- .../processor/ViewModelGenerator.java | 318 ++++++++++++++++++ .../processor/ViewModelGenerationTest.java | 197 +++++++++++ .../fxdesigner/target/ControllerMetadata.java | 8 + docs/generated-model-metadata.md | 2 +- docs/generated-viewmodels.md | 60 ++++ docs/repository-bootstrap-plan.md | 6 + docs/runtime/neitzellib-baseline.json | 13 +- docs/runtime/neitzellib-baseline.md | 6 + .../preview/classpath/app/pom.xml | 2 +- .../components/CustomGeneratedPerson.java | 11 + .../components/GeneratedController.java | 5 + .../java/fixture/model/GeneratedAddress.java | 22 ++ .../java/fixture/model/GeneratedEmployee.java | 13 + .../java/fixture/model/GeneratedPerson.java | 35 ++ .../fixture/components/Generated.fxml | 3 + .../fixture/components/GeneratedAddress.fxml | 2 + .../fixture/components/GeneratedExplicit.fxml | 3 + .../checks/GeneratedViewModelChecks.java | 109 ++++++ .../java/fixture/checks/PreviewChecks.java | 4 + .../preview/modulepath/app/pom.xml | 2 +- .../components/CustomGeneratedPerson.java | 11 + .../components/GeneratedController.java | 5 + .../java/fixture/model/GeneratedAddress.java | 22 ++ .../java/fixture/model/GeneratedEmployee.java | 13 + .../java/fixture/model/GeneratedPerson.java | 35 ++ .../app/src/main/java/module-info.java | 8 + .../fixture/components/Generated.fxml | 3 + .../fixture/components/GeneratedAddress.fxml | 2 + .../fixture/components/GeneratedExplicit.fxml | 3 + .../checks/GeneratedViewModelChecks.java | 109 ++++++ .../java/fixture/checks/PreviewChecks.java | 4 + scripts/check-preview-fixtures.py | 2 +- 36 files changed, 1123 insertions(+), 17 deletions(-) create mode 100644 designer-model-processor/src/main/java/de/neitzel/fxdesigner/processor/ViewModelGenerator.java create mode 100644 designer-model-processor/src/test/java/de/neitzel/fxdesigner/processor/ViewModelGenerationTest.java create mode 100644 docs/generated-viewmodels.md create mode 100644 integration-fixtures/preview/classpath/app/src/main/java/fixture/components/CustomGeneratedPerson.java create mode 100644 integration-fixtures/preview/classpath/app/src/main/java/fixture/components/GeneratedController.java create mode 100644 integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedAddress.java create mode 100644 integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedEmployee.java create mode 100644 integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedPerson.java create mode 100644 integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/Generated.fxml create mode 100644 integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/GeneratedAddress.fxml create mode 100644 integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/GeneratedExplicit.fxml create mode 100644 integration-fixtures/preview/classpath/app/src/test/java/fixture/checks/GeneratedViewModelChecks.java create mode 100644 integration-fixtures/preview/modulepath/app/src/main/java/fixture/components/CustomGeneratedPerson.java create mode 100644 integration-fixtures/preview/modulepath/app/src/main/java/fixture/components/GeneratedController.java create mode 100644 integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedAddress.java create mode 100644 integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedEmployee.java create mode 100644 integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedPerson.java create mode 100644 integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/Generated.fxml create mode 100644 integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/GeneratedAddress.fxml create mode 100644 integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/GeneratedExplicit.fxml create mode 100644 integration-fixtures/preview/modulepath/checks/src/main/java/fixture/checks/GeneratedViewModelChecks.java diff --git a/README.md b/README.md index a5652ad..aa3775e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Shared Scene Builder Kit designer for NeitzelLib FX, with minimal standalone and The first workflow is runnable in both hosts: Build and Refresh publishes immutable snapshots; authenticated target workers discover examples and render parent/Component FXML drafts into the shared Kit canvas. Source and supported canvas edits use host-owned documents and undo/redo. See [the connected workflow](docs/first-connected-workflow.md) for architecture, launch commands, evidence and limits. -The repository builds six shared Maven modules, a Maven capture plugin, a separate Model metadata processor and an IntelliJ adapter plugin. Application code runs only in target JVMs. The shared UI now applies [source-preserving edits](docs/source-preserving-projection.md), supports nested/anonymous Components and fx:root bodies, and provides Source/Design/Split modes. Retained [target sessions, target sizing, interactive preview and Component/Row navigation](docs/target-runtime-sessions.md) now extend that workflow. The shared [binding discovery browser](docs/binding-discovery.md) now lists declared Model paths and direction-aware target compatibility. The [DirectBinding inspector](docs/direct-binding-inspector.md) creates, edits and removes declarations through source-preserving host Undo/Redo. Shared [Converter discovery and binding validation](docs/converter-validation.md) now adds capability-based proposals, document diagnostics and preview gating. [Custom ViewModel discovery and Computed-binding validation](docs/viewmodel-computed-validation.md) now follow the explicitly selected Controller or Component in both hosts. The [Computed-binding inspector](docs/computed-binding-inspector.md) now creates, changes and removes Computed declarations with method suggestions and shared source-preserving Undo/Redo. The [Repeat inspector](docs/repeat-binding-inspector.md) adds source-preserving collection declarations with host Undo/Redo and [collection/Component compatibility discovery](docs/repeat-compatibility.md). The shared [Table/Column inspector](docs/table-column-inspector.md) adds source-preserving Table configuration, ordered Column authoring and explicit Row navigation, now with [Table-specific compatibility and declared Row alignment discovery](docs/table-compatibility.md). The shared [reference-safe Structure editor](docs/reference-safe-structure.md) now renames identifiers, copies subtrees with fresh identifiers and deletes associated bindings through source-preserving host Undo/Redo, including destination validation for cross-View paste. The [shared Problems panel and IntelliJ Problems integration](docs/shared-diagnostics.md) now expose current diagnostics with authoritative FXML navigation and editor-scoped cleanup. Shared [source highlighting and diagnostic quick fixes](docs/source-highlighting-fixes.md) now add explicit source-preserving corrections with host Undo/Redo and native IntelliJ Alt+Enter actions. Opt-in [generated Model metadata](docs/generated-model-metadata.md) now feeds the shared discovery and binding authoring workflows from immutable build snapshots. Java ViewModel/form generation and product packaging remain later work. +The repository builds six shared Maven modules, a Maven capture plugin, a separate Model metadata processor and an IntelliJ adapter plugin. Application code runs only in target JVMs. The shared UI now applies [source-preserving edits](docs/source-preserving-projection.md), supports nested/anonymous Components and fx:root bodies, and provides Source/Design/Split modes. Retained [target sessions, target sizing, interactive preview and Component/Row navigation](docs/target-runtime-sessions.md) now extend that workflow. The shared [binding discovery browser](docs/binding-discovery.md) now lists declared Model paths and direction-aware target compatibility. The [DirectBinding inspector](docs/direct-binding-inspector.md) creates, edits and removes declarations through source-preserving host Undo/Redo. Shared [Converter discovery and binding validation](docs/converter-validation.md) now adds capability-based proposals, document diagnostics and preview gating. [Custom ViewModel discovery and Computed-binding validation](docs/viewmodel-computed-validation.md) now follow the explicitly selected Controller or Component in both hosts. The [Computed-binding inspector](docs/computed-binding-inspector.md) now creates, changes and removes Computed declarations with method suggestions and shared source-preserving Undo/Redo. The [Repeat inspector](docs/repeat-binding-inspector.md) adds source-preserving collection declarations with host Undo/Redo and [collection/Component compatibility discovery](docs/repeat-compatibility.md). The shared [Table/Column inspector](docs/table-column-inspector.md) adds source-preserving Table configuration, ordered Column authoring and explicit Row navigation, now with [Table-specific compatibility and declared Row alignment discovery](docs/table-compatibility.md). The shared [reference-safe Structure editor](docs/reference-safe-structure.md) now renames identifiers, copies subtrees with fresh identifiers and deletes associated bindings through source-preserving host Undo/Redo, including destination validation for cross-View paste. The [shared Problems panel and IntelliJ Problems integration](docs/shared-diagnostics.md) now expose current diagnostics with authoritative FXML navigation and editor-scoped cleanup. Shared [source highlighting and diagnostic quick fixes](docs/source-highlighting-fixes.md) now add explicit source-preserving corrections with host Undo/Redo and native IntelliJ Alt+Enter actions. Opt-in [generated Model metadata](docs/generated-model-metadata.md) now feeds the shared discovery and binding authoring workflows from immutable build snapshots. Opt-in [generated Java ViewModels and registry defaults](docs/generated-viewmodels.md) now reuse the runtime storage and identity rules, with shared discovery in both hosts. Reviewed FXML form generation and product packaging remain later work. Configuration 1.0 schemas, context selection, local JDK mappings and draft protocol validation are now executable. See [configuration and protocol](docs/configuration-and-protocol.md) for formats and validation rules. diff --git a/designer-kit-ui/src/test/java/de/neitzel/fxdesigner/kit/ProjectionHostChecks.java b/designer-kit-ui/src/test/java/de/neitzel/fxdesigner/kit/ProjectionHostChecks.java index 34fd415..b374db9 100644 --- a/designer-kit-ui/src/test/java/de/neitzel/fxdesigner/kit/ProjectionHostChecks.java +++ b/designer-kit-ui/src/test/java/de/neitzel/fxdesigner/kit/ProjectionHostChecks.java @@ -1949,6 +1949,26 @@ public final class ProjectionHostChecks { set(document, original); await(() -> fx(() -> source(pane).getText().equals(original) && object(pane, "card") != null)); viewModelBindings(pane, document, original); + String generated = + java.nio.file.Files.readString( + java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()) + .resolveSibling("Generated.fxml")); + set(document, generated); + await(() -> fx(() -> source(pane).getText().equals(generated))); + bindingTab(pane, "Discover targets"); + click(pane, "Discover bindings"); + await( + () -> + fx( + () -> + ((Label) pane.lookup("#binding-status")) + .getText() + .contains("fixture.components.CustomGeneratedPerson"))); + check(documentText(document).equals(generated), "Generated registry discovery changed source"); + set(document, original); + await(() -> fx(() -> source(pane).getText().equals(original))); + System.out.println( + "PASS generated ViewModel registry discovery in shared host without source mutation"); document .save(document.snapshot().toCompletableFuture().get().revision()) .toCompletableFuture() diff --git a/designer-model-processor/pom.xml b/designer-model-processor/pom.xml index defc393..5aa9b3a 100644 --- a/designer-model-processor/pom.xml +++ b/designer-model-processor/pom.xml @@ -6,6 +6,8 @@ de.neitzel.fxdesignerdesigner-contracts${project.version} org.junit.jupiterjunit-jupitertest + de.neitzel.libfx1.0-SNAPSHOTtest + org.projectlomboklombok1.18.42test org.apache.maven.pluginsmaven-compiler-pluginnone diff --git a/designer-model-processor/src/main/java/de/neitzel/fxdesigner/processor/ModelMetadataProcessor.java b/designer-model-processor/src/main/java/de/neitzel/fxdesigner/processor/ModelMetadataProcessor.java index 56f20c5..e9c685a 100644 --- a/designer-model-processor/src/main/java/de/neitzel/fxdesigner/processor/ModelMetadataProcessor.java +++ b/designer-model-processor/src/main/java/de/neitzel/fxdesigner/processor/ModelMetadataProcessor.java @@ -11,10 +11,23 @@ import javax.tools.*; /** Explicitly selected Models only; no application classloading or code execution. */ @SupportedAnnotationTypes("*") -@SupportedOptions("designer.models") +@SupportedOptions({ + "designer.models", + "designer.viewModels", + "designer.viewModelModels", + "designer.generatedPackage", + "designer.viewModelDefaults" +}) public final class ModelMetadataProcessor extends AbstractProcessor { private final Set emitted = new HashSet<>(); - private final Set sources = new HashSet<>(); + private final Map metadata = new TreeMap<>(); + private final Map modelTypes = new TreeMap<>(); + private boolean delayed; + private boolean generated; + + private boolean typed() { + return "true".equals(processingEnv.getOptions().get("designer.viewModels")); + } @Override public SourceVersion getSupportedSourceVersion() { @@ -24,9 +37,43 @@ public final class ModelMetadataProcessor extends AbstractProcessor { @Override public boolean process(Set annotations, RoundEnvironment round) { if (round.processingOver()) return false; - sources.addAll(round.getRootElements()); String selection = processingEnv.getOptions().getOrDefault("designer.models", ""); if (selection == null || selection.isBlank()) return false; + if (typed()) { + var selectedTypes = + Arrays.stream(selection.split(",")) + .map(String::trim) + .map(processingEnv.getElementUtils()::getTypeElement) + .toList(); + if (selectedTypes.stream().allMatch(java.util.Objects::nonNull) + && selectedTypes.stream() + .noneMatch(t -> com.sun.source.util.Trees.instance(processingEnv).getPath(t) != null)) + return false; + } + // Give accessor-generating processors (including Lombok) a completed source round. + if (typed() && !delayed) { + delayed = true; + try (var out = + processingEnv + .getFiler() + .createSourceFile( + processingEnv + .getOptions() + .getOrDefault("designer.generatedPackage", "generated.viewmodels") + + ".ProcessorRound") + .openWriter()) { + out.write( + "package " + + processingEnv + .getOptions() + .getOrDefault("designer.generatedPackage", "generated.viewmodels") + + "; final class ProcessorRound {} "); + } catch (Exception e) { + throw new IllegalStateException(e); + } + return false; + } + if (generated) return false; for (String name : selection.split(",")) { var type = processingEnv.getElementUtils().getTypeElement(name.trim()); if (type == null) { @@ -43,6 +90,16 @@ public final class ModelMetadataProcessor extends AbstractProcessor { Diagnostic.Kind.ERROR, "Cannot generate Model metadata: " + e.getMessage(), type); } } + if (typed()) { + generated = true; + try { + new ViewModelGenerator(processingEnv, metadata, modelTypes).generate(); + } catch (Exception e) { + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Cannot generate ViewModels: " + e.getMessage()); + } + } return false; } @@ -98,8 +155,10 @@ public final class ModelMetadataProcessor extends AbstractProcessor { while (source.getEnclosingElement() instanceof TypeElement) source = source.getEnclosingElement(); // Test compilation and dependent modules must not duplicate metadata for classpath Models. - if (!sources.contains(source) || depth > 4 || name.startsWith("java.") || !emitted.add(name)) - return; + if (com.sun.source.util.Trees.instance(processingEnv).getPath(source) == null + || depth > (typed() ? 256 : 4) + || name.startsWith("java.") + || !emitted.add(name)) return; var readers = new TreeMap(); var setters = new ArrayList(); for (var member : processingEnv.getElementUtils().getAllMembers(owner)) { @@ -148,12 +207,19 @@ public final class ModelMetadataProcessor extends AbstractProcessor { if (value instanceof DeclaredType d && !collection(value)) nested.add((TypeElement) d.asElement()); } + var description = new ModelMetadata(name, properties); + metadata.put(name, description); + modelTypes.put(name, owner); + if (typed()) { + TypeMirror parent = owner.getSuperclass(); + if (parent instanceof DeclaredType d) emit((TypeElement) d.asElement(), depth + 1); + } var resource = processingEnv .getFiler() .createResource(StandardLocation.CLASS_OUTPUT, "", ModelMetadata.resource(name), owner); try (var out = resource.openOutputStream()) { - out.write(new ModelMetadata(name, properties).encode()); + out.write(description.encode()); } for (var n : nested) emit(n, depth + 1); } diff --git a/designer-model-processor/src/main/java/de/neitzel/fxdesigner/processor/ViewModelGenerator.java b/designer-model-processor/src/main/java/de/neitzel/fxdesigner/processor/ViewModelGenerator.java new file mode 100644 index 0000000..92c123e --- /dev/null +++ b/designer-model-processor/src/main/java/de/neitzel/fxdesigner/processor/ViewModelGenerator.java @@ -0,0 +1,318 @@ +package de.neitzel.fxdesigner.processor; + +import de.neitzel.fxdesigner.protocol.ModelMetadata; +import java.util.*; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.*; +import javax.lang.model.type.DeclaredType; +import javax.tools.StandardLocation; + +/** Generates accessors from the same Model description packaged for discovery. */ +final class ViewModelGenerator { + private final ProcessingEnvironment env; + private final Map models; + private final Map types; + private final String pkg; + private final Map names = new TreeMap<>(); + + ViewModelGenerator( + ProcessingEnvironment env, + Map models, + Map types) { + this.env = env; + this.models = models; + this.types = types; + pkg = env.getOptions().getOrDefault("designer.generatedPackage", "generated.viewmodels"); + if (!javax.lang.model.SourceVersion.isName(pkg)) + throw new IllegalArgumentException("Invalid generated package: " + pkg); + } + + private boolean assignable(String name, String parent) { + var a = env.getElementUtils().getTypeElement(name.replace('$', '.')); + var b = env.getElementUtils().getTypeElement(parent); + return a != null + && b != null + && env.getTypeUtils() + .isAssignable( + env.getTypeUtils().erasure(a.asType()), env.getTypeUtils().erasure(b.asType())); + } + + private boolean simple(String name) { + var t = env.getElementUtils().getTypeElement(name.replace('$', '.')); + return t == null + || t.getKind() == ElementKind.ENUM + || name.startsWith("java.") + || assignable(name, "java.lang.Number") + || assignable(name, "java.util.Date") + || assignable(name, "java.util.Calendar") + || assignable(name, "java.time.temporal.TemporalAccessor") + || assignable(name, "java.time.temporal.TemporalAmount"); + } + + private String javaType(String binary) { + if (!binary.startsWith("[")) return binary.replace('$', '.'); + String c = binary.substring(1); + return (c.startsWith("L") + ? c.substring(1, c.length() - 1).replace('$', '.') + : switch (c) { + case "I" -> "int"; + case "J" -> "long"; + case "Z" -> "boolean"; + case "B" -> "byte"; + case "S" -> "short"; + case "C" -> "char"; + case "F" -> "float"; + case "D" -> "double"; + default -> javaType(c); + }) + + "[]"; + } + + private String value(ModelMetadata.Property p) { + if (names.containsKey(p.type())) return names.get(p.type()); + if (assignable(p.type(), "java.util.Collection") && !p.elementType().isEmpty()) + return "javafx.collections.ObservableList<" + javaType(p.elementType()) + ">"; + return javaType(p.type()); + } + + private String specialized(String type) { + return switch (type) { + case "java.lang.String" -> "String"; + case "int", "java.lang.Integer" -> "Integer"; + case "long", "java.lang.Long" -> "Long"; + case "float", "java.lang.Float" -> "Float"; + case "double", "java.lang.Double" -> "Double"; + case "boolean", "java.lang.Boolean" -> "Boolean"; + default -> ""; + }; + } + + private String boxed(String type) { + return switch (type) { + case "byte" -> "java.lang.Byte"; + case "short" -> "java.lang.Short"; + case "char" -> "java.lang.Character"; + default -> type; + }; + } + + private String property(ModelMetadata.Property p) { + String scalar = specialized(p.type()); + return scalar.isEmpty() + ? "javafx.beans.property.Property<" + boxed(value(p)) + ">" + : "javafx.beans.property." + scalar + "Property"; + } + + private String selectedModel(String name) { + var type = env.getElementUtils().getTypeElement(name.replace('$', '.')); + if (type == null) + throw new IllegalArgumentException("Selected ViewModel Model not found: " + name); + return env.getElementUtils().getBinaryName(type).toString(); + } + + void generate() throws Exception { + var required = new HashSet(); + var queue = new ArrayDeque(); + String selected = + env.getOptions() + .getOrDefault("designer.viewModelModels", env.getOptions().get("designer.models")); + for (String name : selected.split(",", -1)) { + String binary = selectedModel(name.trim()); + if (!models.containsKey(binary)) + throw new IllegalArgumentException( + "Selected ViewModel Model has no source metadata: " + name); + queue.add(binary); + } + while (!queue.isEmpty()) { + String name = queue.remove(); + if (!models.containsKey(name) || simple(name) || !required.add(name)) continue; + for (var p : models.get(name).properties()) + if (models.containsKey(p.type()) + && !simple(p.type()) + && !models.get(p.type()).properties().isEmpty()) queue.add(p.type()); + var type = types.get(name); + if (type.getSuperclass() instanceof DeclaredType d) + queue.add(env.getElementUtils().getBinaryName((TypeElement) d.asElement()).toString()); + } + for (var m : models.values()) + if (required.contains(m.model())) { + var type = types.get(m.model()); + var methods = + env.getElementUtils().getAllMembers(type).stream() + .filter( + e -> + e instanceof ExecutableElement + && e.getModifiers().contains(Modifier.PUBLIC)) + .map(e -> e.getSimpleName().toString()) + .collect(java.util.stream.Collectors.toSet()); + if (methods.contains("getLatestMessage") && methods.contains("setLatestMessage")) + throw new IllegalArgumentException( + m.model() + ".latestMessage conflicts with ViewModel.getLatestMessage"); + if (!type.getModifiers().contains(Modifier.PUBLIC)) + throw new IllegalArgumentException("Generated Model must be public: " + m.model()); + if (!type.getTypeParameters().isEmpty()) + throw new IllegalArgumentException( + "Generic Model needs a concrete declaration: " + m.model()); + for (TypeElement c = type; c != null; ) { + if (env.getElementUtils().getTypeElement(c.getQualifiedName() + "BeanInfo") != null) + throw new IllegalArgumentException( + "Custom BeanInfo requires explicit mapping: " + m.model()); + c = c.getSuperclass() instanceof DeclaredType d ? (TypeElement) d.asElement() : null; + } + String name = pkg + "." + type.getSimpleName() + "ViewModel"; + if (names.containsValue(name)) + throw new IllegalArgumentException("Generated class name collision: " + name); + names.put(m.model(), name); + } + var defaults = new TreeMap(); + String settings = env.getOptions().getOrDefault("designer.viewModelDefaults", ""); + if (settings != null && !settings.isBlank()) + for (String item : settings.split(",")) { + var pair = item.split("=", -1); + if (pair.length != 2) + throw new IllegalArgumentException("Invalid ViewModel default: " + item); + pair[0] = selectedModel(pair[0].trim()); + pair[1] = pair[1].trim(); + if (!names.containsKey(pair[0]) + || !javax.lang.model.SourceVersion.isName(pair[1]) + || defaults.put(pair[0], pair[1]) != null) + throw new IllegalArgumentException("Invalid/conflicting ViewModel default: " + item); + } + var runtime = env.getElementUtils().getTypeElement("de.neitzel.fx.view.ViewModel"); + if (runtime == null) + throw new IllegalArgumentException( + "NeitzelLib FX with generated ViewModel support is required"); + var reserved = new HashSet(); + for (var member : env.getElementUtils().getAllMembers(runtime)) + if (member instanceof ExecutableElement m) reserved.add(m.getSimpleName().toString()); + for (var entry : names.entrySet()) { + var model = models.get(entry.getKey()); + var type = types.get(entry.getKey()); + String parentModel = + type.getSuperclass() instanceof DeclaredType d + ? env.getElementUtils().getBinaryName((TypeElement) d.asElement()).toString() + : ""; + String parent = names.getOrDefault(parentModel, "de.neitzel.fx.view.ViewModel"); + var inherited = new HashMap(); + if (names.containsKey(parentModel)) + for (var p : models.get(parentModel).properties()) inherited.put(p.name(), p); + var source = + new StringBuilder( + "package " + + pkg + + ";\n@SuppressWarnings({\"unchecked\",\"rawtypes\"})\npublic class " + + entry.getValue().substring(pkg.length() + 1) + + " extends " + + parent + + " {\n"); + source + .append(" public ") + .append(entry.getValue().substring(pkg.length() + 1)) + .append('(') + .append(javaType(model.model())) + .append(" model) { super(model); }\n"); + for (var p : model.properties()) { + if (!simple(p.type()) && !names.containsKey(p.type())) { + var child = env.getElementUtils().getTypeElement(p.type().replace('$', '.')); + var members = + env.getElementUtils().getAllMembers(child).stream() + .filter( + e -> + e instanceof ExecutableElement + && e.getModifiers().contains(Modifier.PUBLIC)) + .map(e -> e.getSimpleName().toString()) + .collect(java.util.stream.Collectors.toSet()); + if (members.stream() + .anyMatch(n -> n.startsWith("get") && members.contains("set" + n.substring(3)))) + throw new IllegalArgumentException( + "Nested Model requires an available generated mapping: " + + model.model() + + "." + + p.name()); + } + String cap = Character.toUpperCase(p.name().charAt(0)) + p.name().substring(1); + for (String n : List.of(p.name() + "Property", "get" + cap, "set" + cap)) + if (reserved.contains(n)) + throw new IllegalArgumentException( + model.model() + "." + p.name() + " conflicts with ViewModel." + n); + if (inherited.containsKey(p.name())) { + var old = inherited.get(p.name()); + if (!property(old).equals(property(p)) || !value(old).equals(value(p))) + throw new IllegalArgumentException( + "Incompatible writable Attribute narrowing: " + model.model() + "." + p.name()); + continue; + } + source + .append(" public ") + .append(property(p)) + .append(' ') + .append(p.name()) + .append("Property() { return (") + .append(property(p)) + .append(") (javafx.beans.property.Property) storedProperty(\"") + .append(p.name()) + .append("\"); }\n"); + source + .append(" public ") + .append(value(p)) + .append(" get") + .append(cap) + .append("() { return ") + .append(p.name()) + .append("Property().") + .append(specialized(p.type()).isEmpty() ? "getValue()" : "get()") + .append("; }\n"); + source + .append(" public void set") + .append(cap) + .append('(') + .append(value(p)) + .append(" value) { ") + .append(p.name()) + .append("Property().setValue(value); }\n"); + } + source.append("}\n"); + try (var out = env.getFiler().createSourceFile(entry.getValue(), type).openWriter()) { + out.write(source.toString()); + } + } + if (names.isEmpty()) return; + String registry = pkg + ".GeneratedViewModelRegistry"; + var source = new StringBuilder("package " + pkg + ";\n"); + for (var entry : names.entrySet()) + source + .append("@de.neitzel.fx.view.ViewModelRegistration(model=") + .append(javaType(entry.getKey())) + .append(".class,generated=") + .append(entry.getValue()) + .append(".class,defaultType=") + .append(defaults.getOrDefault(entry.getKey(), entry.getValue())) + .append(".class)\n"); + source.append( + "public final class GeneratedViewModelRegistry implements" + + " de.neitzel.fx.view.ViewModelRegistry {\n"); + int index = 0; + for (var e : defaults.entrySet()) + source + .append(" private Class check") + .append(index++) + .append("() { return ") + .append(e.getValue()) + .append(".class; }\n"); + source.append("}\n"); + try (var out = env.getFiler().createSourceFile(registry).openWriter()) { + out.write(source.toString()); + } + try (var out = + env.getFiler() + .createResource( + StandardLocation.CLASS_OUTPUT, + "", + "META-INF/services/de.neitzel.fx.view.ViewModelRegistry") + .openWriter()) { + out.write(registry + "\n"); + } + } +} diff --git a/designer-model-processor/src/test/java/de/neitzel/fxdesigner/processor/ViewModelGenerationTest.java b/designer-model-processor/src/test/java/de/neitzel/fxdesigner/processor/ViewModelGenerationTest.java new file mode 100644 index 0000000..7caea6a --- /dev/null +++ b/designer-model-processor/src/test/java/de/neitzel/fxdesigner/processor/ViewModelGenerationTest.java @@ -0,0 +1,197 @@ +package de.neitzel.fxdesigner.processor; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.*; +import java.util.*; +import javax.annotation.processing.Processor; +import javax.tools.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class ViewModelGenerationTest { + @TempDir Path root; + String diagnostics; + + boolean compile( + String source, String custom, boolean lombok, boolean reverse, String... extraOptions) + throws Exception { + Path src = root.resolve("src"), out = root.resolve("out"), gen = root.resolve("generated"); + Files.createDirectories(src); + Files.createDirectories(out); + Files.createDirectories(gen); + Files.writeString(src.resolve("Person.java"), source); + var inputs = new ArrayList(); + inputs.add(src.resolve("Person.java").toFile()); + if (custom != null) { + Files.writeString(src.resolve("Custom.java"), custom); + inputs.add(src.resolve("Custom.java").toFile()); + } + try (var paths = Files.list(src)) { + inputs.clear(); + inputs.addAll(paths.filter(p -> p.toString().endsWith(".java")).map(Path::toFile).toList()); + } + var compiler = ToolProvider.getSystemJavaCompiler(); + var messages = new DiagnosticCollector(); + try (var files = compiler.getStandardFileManager(null, null, null)) { + var options = + new ArrayList<>( + List.of( + "-classpath", + System.getProperty("java.class.path"), + "-d", + out.toString(), + "-s", + gen.toString(), + "-Adesigner.models=sample.Person", + "-Adesigner.viewModels=true", + "-Adesigner.generatedPackage=sample.generated")); + options.addAll(List.of(extraOptions)); + if (custom != null) options.add("-Adesigner.viewModelDefaults=sample.Person=sample.Custom"); + var task = + compiler.getTask( + null, files, messages, options, null, files.getJavaFileObjectsFromFiles(inputs)); + var processors = new ArrayList(); + processors.add(new ModelMetadataProcessor()); + if (lombok) + processors.add( + (Processor) + Class.forName("lombok.launch.AnnotationProcessorHider$AnnotationProcessor") + .getConstructor() + .newInstance()); + if (reverse) Collections.reverse(processors); + task.setProcessors(processors); + boolean success = task.call(); + diagnostics = messages.getDiagnostics().toString(); + return success; + } + } + + @Test + void nestedSelectionsAreNormalizedAndUnknownSelectionsFail() throws Exception { + String source = + "package sample; public class Person { public static class Child { public String" + + " getName(){return null;} public void setName(String n){} } }"; + assertTrue( + compile( + source, + null, + false, + false, + "-Adesigner.models=sample.Person.Child", + "-Adesigner.viewModelModels=sample.Person.Child"), + diagnostics); + assertTrue(Files.exists(root.resolve("generated/sample/generated/ChildViewModel.java"))); + assertFalse(compile(source, null, false, false, "-Adesigner.viewModelModels=sample.Missing")); + assertTrue(diagnostics.contains("Selected ViewModel Model not found"), diagnostics); + } + + @Test + void cleanCompilationSupportsHandwrittenSubclassAndStoredAccessors() throws Exception { + assertTrue( + compile( + "package sample; public class Person { public String getName(){return null;} public" + + " void setName(String n){} public Person getChild(){return null;} public void" + + " setChild(Person p){} }", + "package sample; public class Custom extends sample.generated.PersonViewModel { public" + + " Custom(Person p){super(p);} public String greeting(){return getName();} }", + false, + false)); + String generated = + Files.readString(root.resolve("generated/sample/generated/PersonViewModel.java")); + assertTrue(generated.contains("storedProperty(\"name\")")); + assertTrue(generated.contains("Property")); + assertTrue( + Files.exists(root.resolve("out/META-INF/services/de.neitzel.fx.view.ViewModelRegistry"))); + } + + @Test + void lombokAccessorsWorkInBothProcessorOrders() throws Exception { + for (boolean reverse : List.of(false, true)) { + Path previous = root; + root = Files.createDirectory(previous.resolve("order" + reverse)); + assertTrue( + compile( + "package sample; @lombok.Getter @lombok.Setter public class Person { private String" + + " name; }", + null, + true, + reverse)); + assertTrue( + Files.readString(root.resolve("generated/sample/generated/PersonViewModel.java")) + .contains("nameProperty")); + root = previous; + } + } + + @Test + void reservedNamesAndInvalidCustomDefaultsFailCompilation() throws Exception { + assertFalse( + compile( + "package sample; public class Person { public String getLatestMessage(){return null;}" + + " public void setLatestMessage(String s){} }", + null, + false, + false)); + assertFalse( + compile( + "package sample; public class Person { public String getName(){return null;} public" + + " void setName(String s){} }", + "package sample; public class Custom extends de.neitzel.fx.view.ViewModel { public" + + " Custom(Person p){super(p);} }", + false, + false)); + } + + @Test + void removedAttributesBreakCustomCodeWithoutRewritingIt() throws Exception { + String custom = + "package sample; public class Custom extends sample.generated.PersonViewModel { public" + + " Custom(Person p){super(p);} public String title(){return getName();} }"; + assertTrue( + compile( + "package sample; public class Person {public String getName(){return null;} public void" + + " setName(String v){} }", + custom, + false, + false)); + for (String dir : List.of("out", "generated")) + try (var paths = Files.walk(root.resolve(dir))) { + for (var p : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(p); + } + assertFalse( + compile( + "package sample; public class Person {public String getTitle(){return null;} public" + + " void setTitle(String v){} }", + custom, + false, + false)); + assertEquals(custom, Files.readString(root.resolve("src/Custom.java"))); + } + + @Test + void writableNarrowingAndCustomBeanInfoAreDiagnosed() throws Exception { + Files.createDirectories(root.resolve("src")); + Files.writeString( + root.resolve("src/Parent.java"), + "package sample; public class Parent {public Object getValue(){return null;} public void" + + " setValue(Object v){} }"); + assertFalse( + compile( + "package sample; public class Person extends Parent {public String getValue(){return" + + " null;} public void setValue(String v){} }", + null, + false, + false)); + assertTrue(diagnostics.contains("Incompatible writable Attribute narrowing"), diagnostics); + assertFalse( + compile( + "package sample; public class Person { public String getName(){return null;} public" + + " void setName(String n){} } class PersonBeanInfo extends" + + " java.beans.SimpleBeanInfo {}", + null, + false, + false)); + assertTrue(diagnostics.contains("Custom BeanInfo"), diagnostics); + } +} diff --git a/designer-target-tools/src/main/java/de/neitzel/fxdesigner/target/ControllerMetadata.java b/designer-target-tools/src/main/java/de/neitzel/fxdesigner/target/ControllerMetadata.java index d660d87..c67a049 100644 --- a/designer-target-tools/src/main/java/de/neitzel/fxdesigner/target/ControllerMetadata.java +++ b/designer-target-tools/src/main/java/de/neitzel/fxdesigner/target/ControllerMetadata.java @@ -28,6 +28,14 @@ final class ControllerMetadata { selected = null; } } + if (model != null) { + try { + selected = ViewModelDefaults.select(model, selected, model.getClassLoader()); + } catch (RuntimeException | java.util.ServiceConfigurationError e) { + problems.add(problem("ERROR", "ViewModel registry selection failed: " + e.getMessage())); + selected = null; + } + } viewModel = selected; if (selected == null || selected == ViewModel.class) return; if (selected.isInterface() || Modifier.isAbstract(selected.getModifiers())) { diff --git a/docs/generated-model-metadata.md b/docs/generated-model-metadata.md index e21184d..60c630d 100644 --- a/docs/generated-model-metadata.md +++ b/docs/generated-model-metadata.md @@ -35,7 +35,7 @@ Build and Refresh already performs `clean package` in an isolated reactor copy. ## Boundaries and validation -This is build-time Model metadata for existing discovery and binding authoring. It does not generate Java ViewModel subclasses, a ViewModel registry, editable FXML forms, labels/control defaults, arbitrary BeanInfo metadata, Java source origins or project-wide Model selection UI. Those retain their later roadmap gates. The processor is a Java 21 build artifact depending only on the contracts; it is not packaged in either host or required on the application runtime path. Wire DTOs and protocol versions do not change. +This is build-time Model metadata for existing discovery and binding authoring. The subsequent [ViewModel generation slice](generated-viewmodels.md) now generates Java subclasses and registry defaults. Editable FXML forms, labels/control defaults, arbitrary BeanInfo metadata, Java source origins and a project-wide Model selection UI remain later work. Those retain their later roadmap gates. The processor is a Java 21 build artifact depending only on the contracts; it is not packaged in either host or required on the application runtime path. Wire DTOs and protocol versions do not change. Automated tests compile real source with the processor, verify deterministic inherited/collection signatures and non-execution, clean-build removal, missing selections, schema bounds, generated/fallback equivalence, stale signatures and duplicate resources. Real classpath and module-path fixture processes require generated Person and nested Address metadata and retain their non-initialization assertions. The live shared-host scenario requires generated metadata provenance before handing discovery to the binding inspector, then runs existing source-preserving binding authoring and Undo/Redo tests in standalone and registered IntelliJ. No manual testing is requested; earlier skipped manual checks remain skipped. diff --git a/docs/generated-viewmodels.md b/docs/generated-viewmodels.md new file mode 100644 index 0000000..54cabaa --- /dev/null +++ b/docs/generated-viewmodels.md @@ -0,0 +1,60 @@ +# Generated Java ViewModels and registry defaults + +The existing Model processor now optionally generates Java subclasses from the same `ModelMetadata` descriptions used by discovery. Generated accessors use NeitzelLib's stored Attribute properties; they do not create another property map or identity registry. Both hosts discover the configured default ViewModel from the compiled registry without executing providers, and Build and Refresh runs the configured generator in the application build. + +## Configure the application build + +Keep Model selections, generated package and hand-written defaults in the application module's checked-in compiler configuration, alongside the [metadata processor path](generated-model-metadata.md): + +```xml + + -Adesigner.models=com.example.Person,com.example.Employee + -Adesigner.viewModels=true + -Adesigner.viewModelModels=com.example.Person,com.example.Employee + -Adesigner.generatedPackage=com.example.generated + -Adesigner.viewModelDefaults=com.example.Person=com.example.CustomPersonViewModel + +``` + +`viewModelModels` selects the generated subset of metadata Models; unknown selections fail compilation, and nested canonical class names are normalized; when omitted, it uses `designer.models`. `viewModelDefaults` is optional; a Model otherwise defaults to its generated class. Use a distinct generated package per participating module. The module must depend on the qualified NeitzelLib FX runtime. The processor remains a build dependency; runtime applications do not depend on designer hosts or target tooling. + +Output goes to Maven's `target/generated-sources/annotations`, with names such as `PersonViewModel`. Hand-written subclasses remain ordinary source files and can extend the generated superclass in a clean compilation. Generated classes have a concrete Model constructor, typed `nameProperty()`, `getName()` and `setName(...)` methods. String/numeric/boolean properties use the runtime's specialized JavaFX classes. Complex Attributes expose typed child ViewModels, while known collections expose observable lists of Model elements. Existing runtime null, collection-setter and write-back semantics remain in force. + +Generation includes available source ancestors and nested Models, visits cycles once, and preserves safe generated superclass APIs. Incompatible writable narrowing, name collisions, reserved runtime APIs, unsupported generic Model declarations and custom BeanInfo mappings are diagnosed. A nested complex type without an available generated mapping is rejected rather than exposed with an incorrect Model-valued accessor. Cross-module nested-generation mapping and broader generic/BeanInfo configurations remain outside this qualified slice. + +Lombok accessor generation is tested with either processor ordering. A compiler source round allows accessors to complete; compiler source-tree ownership prevents test/dependent compilations from duplicating main-source registries. Clean builds remove obsolete generated sources, classes and resources. Removing an Attribute makes custom references fail compilation; the generator does not rewrite custom source. Build and Refresh already uses clean isolated builds and immutable snapshots. External incremental builds must clean after selection changes/removals. + +## Runtime registry and selection + +Each participating module generates `GeneratedViewModelRegistry`, a marker service with repeatable `ViewModelRegistration` annotations naming the Model, generated base and selected default. A generated compile-time assignment checks that a custom default extends its generated base. Classpath builds package the service descriptor automatically. + +Named modules must declare the generated provider and make generated constructors accessible, for example: + +```java +provides de.neitzel.fx.view.ViewModelRegistry + with com.example.generated.GeneratedViewModelRegistry; +exports com.example.generated; +opens com.example.generated to fx; +``` + +Existing Model and Controller access requirements still apply. Both classpath and named-module fixtures are qualified against the pinned runtime. The library inspects `ServiceLoader.Provider.type()` and annotations without constructing registry objects. Conflicting defaults identify both contributing registry classes. Default lookup chooses the most-specific configured Model ancestor; unconfigured subclasses can use their ancestor's default. + +A compatible explicit `@ViewModelClass` takes precedence before first creation and must extend the applicable generated base. An existing compatible subclass satisfies later requests for its generated superclass; incompatible requests fail. Nested and direct loading share the same Model-identity mapping, including cycles and null children. Failed subclass construction withdraws its partial identity entry. Messages and ordinary binding behavior remain owned by the existing runtime. + +The library extension is [NeitzelLib main `90e08b7`](https://gitea.neitzel.de/konrad/NeitzelLib/src/commit/90e08b7/fx/docs/generated-viewmodels.md). The [runtime manifest](runtime/neitzellib-baseline.json) pins the tested local artifact bytes. No released runtime version or remote artifact deployment is claimed. + +## Host integration and boundaries + +Standalone and IntelliJ use the same registry-aware target discovery. The browser shows the selected generated/custom class, uses the shared Model Attributes for authoring and retains the existing source-preserving edit/history path. Neither host loads application classes. The current UI invokes generation through **Build and Refresh** and checked-in Maven settings; a dedicated selection/settings editor and reviewed FXML form generation remain later work. + +Automated acceptance covers clean consumer compilation, custom subclasses, both Lombok orders, removed Attributes, narrowing/reserved-name/default/BeanInfo errors, runtime stored identity/write-back, typed nested access, cycles/nulls, inheritance, collection element identity, Messages, compatible explicit/default selection, conflicting services and constructor failure cleanup. Real Maven classpath/module-path applications compile and load the generated classes independently of a designer. The shared live-host test discovers the generated custom default without changing FXML; existing authoring, Undo/Redo, diagnostics and lifecycle scenarios remain required in both hosts. No manual testing is requested. + +## Qualification record (2026-11-13) + +The complete bootstrap passed module builds, target linkage, Java bytecode baselines, plugin packaging, documentation checks, both standalone launch modes, native shared-Kit workflows, 48 worker lifecycle cycles and abrupt owner death. The final processor-selection check then passed all ten processor tests (including six new generation tests) and was installed for a fresh fixture run: 38 target processes passed 548 checks across classpath and module-path applications. Combined current reports contain 151 designer unit/adapter tests and zero failures, errors or skips. The independently built NeitzelLib runtime passed 116 tests with zero failures, errors or skips; its Surefire XML was checked explicitly. + +Evidence logs are `/tmp/generated-vm-bootstrap.log`, `/tmp/generated-vm-processor-final.log`, `/tmp/generated-vm-fixtures-final.log`, `/tmp/generated-vm-registered-ide.log`, `/tmp/generated-vm-ide-modulepath-retry.log`, `/tmp/generated-vm-ide-modulepath-final.log` and `/tmp/viewmodel-runtime-qualified.log`; host evidence is retained under `target/connected-workflow` and `target/ide-workflow`. These local outputs are disposable, not release artifacts. No manual testing was performed, and previously skipped human scenarios remain skipped. + +The first registered IntelliJ run passed the classpath workflow but stopped in module-path mode at the previously recorded immediate exact-save assertion, before generation checks. An unchanged module-path retry timed out at initial canvas rendering (`WorkflowProbe` line 438), also before generation checks. No assertion was weakened. Existing startup/persistence/preview-gate reliability work remains open; this slice does not claim to fix those intermittent failures. + +The final unchanged module-path retry hit the same initial-render timeout. Consequently, **registered IntelliJ module-path whole-host qualification remains open** for this slice. Registered IntelliJ classpath passed generated-registry discovery, full shared editing/history/diagnostic checks, exclusive JavaFX 26 native verification and worker shutdown. Standalone and real target-process module-path checks passed, including generated classes and registry behavior; these do not substitute for the missing registered IDE pass. Follow up on initial rendering and save synchronization before declaring that host gate complete. diff --git a/docs/repository-bootstrap-plan.md b/docs/repository-bootstrap-plan.md index 05ba590..c48bb72 100644 --- a/docs/repository-bootstrap-plan.md +++ b/docs/repository-bootstrap-plan.md @@ -149,3 +149,9 @@ The [shared highlighting/fixes slice](source-highlighting-fixes.md) adds current ## Generated Model metadata The [generated Model metadata slice](generated-model-metadata.md) adds an opt-in compiler processor, versioned deterministic resources, generated-first target discovery with signature integrity checks, and shared provenance in both hosts. Existing binding authoring consumes the resulting Attributes without host-specific logic or new undo paths. Missing metadata retains compiled fallback; invalid present metadata is diagnosed. Java ViewModel/registry and FXML form generation, custom BeanInfo semantics and broader source origins remain later work. + +## Generated Java ViewModels and registry defaults + +The [ViewModel generation slice](generated-viewmodels.md) extends the metadata processor with typed replaceable Java sources, checked-in Maven settings and declarative registry services. NeitzelLib supplies stored-property lookup and compatible shared identity/default selection. Both hosts discover generated/custom defaults through target workers and keep existing authoring/history behavior. Clean consumer builds, Lombok, generated inheritance/nesting and rejection cases have automated coverage. Dedicated configuration selection UI, broader cross-module/generic mappings and reviewed FXML form generation remain later work. + +Qualification: automated standalone and registered IntelliJ classpath workflows pass. Real module-path consumer/runtime tests pass, but registered IntelliJ module-path remains open after the existing save assertion and initial-render timeouts; see the qualification record above. No manual testing was requested or substituted. diff --git a/docs/runtime/neitzellib-baseline.json b/docs/runtime/neitzellib-baseline.json index 137294f..c79e634 100644 --- a/docs/runtime/neitzellib-baseline.json +++ b/docs/runtime/neitzellib-baseline.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "status": "verified-local-development-snapshot", "sourceRepository": "ssh://git@gitea.neitzel.de:2222/konrad/NeitzelLib.git", - "sourceRevision": "2d83fb467cc29b6e7c5aa92c86fc3fb49a504dc0", + "sourceRevision": "90e08b7835501f6810f12b845d7a9dacd7749425", "minimumReleasedVersion": null, "requiredCapability": "component-fxml-attributes-after-initialization", "javaVersion": "21", @@ -21,7 +21,7 @@ "skipped": 0 }, "fx": { - "tests": 101, + "tests": 105, "failures": 0, "errors": 0, "skipped": 0 @@ -51,7 +51,7 @@ "version": "1.0-SNAPSHOT", "extension": "jar", "repositoryPath": "de/neitzel/lib/core/1.0-SNAPSHOT/core-1.0-SNAPSHOT.jar", - "sha256": "6610e43792818fd5d4d3545d487b28d0cd3faa9b849046a47a4db02c984a144a" + "sha256": "2fc2b717f48628f3fd24437fe480f5acfeb55cd2c7d4a65fdd1df0275cd28810" }, { "groupId": "de.neitzel.lib", @@ -67,7 +67,7 @@ "version": "1.0-SNAPSHOT", "extension": "jar", "repositoryPath": "de/neitzel/lib/injection/1.0-SNAPSHOT/injection-1.0-SNAPSHOT.jar", - "sha256": "a582aa71f034370865d486706f55a1fbcc40446c47bf402c28049b205f7c9e25" + "sha256": "45ef4ce24a23164faa42bd3568015be30808f87328fa4ef2ec8cc57ba1eb027d" }, { "groupId": "de.neitzel.lib", @@ -83,7 +83,8 @@ "version": "1.0-SNAPSHOT", "extension": "jar", "repositoryPath": "de/neitzel/lib/fx/1.0-SNAPSHOT/fx-1.0-SNAPSHOT.jar", - "sha256": "a668ac49f5a44d63e7e115fd93456b581ab05725870affcaf2dede6ec5dbe431" + "sha256": "cc0cbde64cc09e1b0711727752e7965442044330d976eae054e14237456a8f40" } - ] + ], + "generatedViewModelSupport": "stored-property-access-and-declarative-service-registry" } diff --git a/docs/runtime/neitzellib-baseline.md b/docs/runtime/neitzellib-baseline.md index 979e9d6..82baef5 100644 --- a/docs/runtime/neitzellib-baseline.md +++ b/docs/runtime/neitzellib-baseline.md @@ -33,3 +33,9 @@ Use a clean checkout at the manifest's source revision; the canonical library ch The command-line failure-ignore property does not override the existing hardcoded POM setting. Inspect every `TEST-*.xml` report under `core`, `injection` and `fx`: require nonempty reports and zero failures/errors/skips before accepting the install. Re-run the direct adapter-free scenario before changing the recorded capability baseline. Record source revision and installed artifact hashes together after acceptance; do not silently reuse the old hashes for a new build. The regression source covers custom/inherited properties, numeric/boolean/enum conversion, XML text escaping, attached layout constraints, post-initialization setters, shared Model identity/write-back and invalid values. The original canonical library branch and its working-tree changes were left untouched; only the isolated eight-file Component change was integrated into main. + +## Generated ViewModel runtime extension — 2026-11-13 + +The active development snapshot now includes NeitzelLib `90e08b7`: [stored-property access and generated defaults](https://gitea.neitzel.de/konrad/NeitzelLib/src/commit/90e08b7/fx/docs/generated-viewmodels.md). The earlier Component baseline above remains historical qualification. The new isolated clean build passed core 1, injection 10 and fx 105 tests with no failures/errors/skips. Every installed class was compared with the tested class output before updating the manifest hashes. The canonical library checkout and its unrelated changes remain untouched. No released minimum or remote artifact publication is claimed. + +Designer fixtures additionally verify generated runtime behavior in real classpath and module-path applications. The library build evidence is `/tmp/viewmodel-runtime-qualified.log`; generated fixture evidence is `/tmp/generated-vm-fixtures-final.log`. The prior 38 adapter-free checks remain historical; current whole-host qualification is recorded with the generated ViewModel slice. diff --git a/integration-fixtures/preview/classpath/app/pom.xml b/integration-fixtures/preview/classpath/app/pom.xml index c4f0db4..583ffed 100644 --- a/integration-fixtures/preview/classpath/app/pom.xml +++ b/integration-fixtures/preview/classpath/app/pom.xml @@ -30,7 +30,7 @@ org.apache.maven.pluginsmaven-compiler-plugin de.neitzel.fxdesignerdesigner-model-processor0.1.0-SNAPSHOT - -Adesigner.models=fixture.model.Person,fixture.model.Address + -Adesigner.models=fixture.model.Person,fixture.model.Address,fixture.model.GeneratedPerson,fixture.model.GeneratedEmployee-Adesigner.viewModels=true-Adesigner.viewModelModels=fixture.model.GeneratedPerson,fixture.model.GeneratedEmployee-Adesigner.generatedPackage=fixture.generated-Adesigner.viewModelDefaults=fixture.model.GeneratedPerson=fixture.components.CustomGeneratedPerson diff --git a/integration-fixtures/preview/classpath/app/src/main/java/fixture/components/CustomGeneratedPerson.java b/integration-fixtures/preview/classpath/app/src/main/java/fixture/components/CustomGeneratedPerson.java new file mode 100644 index 0000000..50e1eb0 --- /dev/null +++ b/integration-fixtures/preview/classpath/app/src/main/java/fixture/components/CustomGeneratedPerson.java @@ -0,0 +1,11 @@ +package fixture.components; + +public final class CustomGeneratedPerson extends fixture.generated.GeneratedPersonViewModel { + public CustomGeneratedPerson(fixture.model.GeneratedPerson model) { + super(model); + } + + public String greeting() { + return "Hello " + getName(); + } +} diff --git a/integration-fixtures/preview/classpath/app/src/main/java/fixture/components/GeneratedController.java b/integration-fixtures/preview/classpath/app/src/main/java/fixture/components/GeneratedController.java new file mode 100644 index 0000000..4f75483 --- /dev/null +++ b/integration-fixtures/preview/classpath/app/src/main/java/fixture/components/GeneratedController.java @@ -0,0 +1,5 @@ +package fixture.components; + +@jakarta.inject.Singleton +@de.neitzel.fx.view.ViewModelClass(fixture.generated.GeneratedPersonViewModel.class) +public class GeneratedController {} diff --git a/integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedAddress.java b/integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedAddress.java new file mode 100644 index 0000000..a090456 --- /dev/null +++ b/integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedAddress.java @@ -0,0 +1,22 @@ +package fixture.model; + +public class GeneratedAddress { + private String street = "Generated street"; + private GeneratedAddress next; + + public String getStreet() { + return street; + } + + public void setStreet(String v) { + street = v; + } + + public GeneratedAddress getNext() { + return next; + } + + public void setNext(GeneratedAddress v) { + next = v; + } +} diff --git a/integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedEmployee.java b/integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedEmployee.java new file mode 100644 index 0000000..0d7d23a --- /dev/null +++ b/integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedEmployee.java @@ -0,0 +1,13 @@ +package fixture.model; + +public class GeneratedEmployee extends GeneratedPerson { + private int salary = 42; + + public int getSalary() { + return salary; + } + + public void setSalary(int v) { + salary = v; + } +} diff --git a/integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedPerson.java b/integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedPerson.java new file mode 100644 index 0000000..d353793 --- /dev/null +++ b/integration-fixtures/preview/classpath/app/src/main/java/fixture/model/GeneratedPerson.java @@ -0,0 +1,35 @@ +package fixture.model; + +public class GeneratedPerson { + static { + System.setProperty("fixture.generated.initialized", "yes"); + } + + private String name = "Generated sample"; + private GeneratedAddress home = new GeneratedAddress(); + private java.util.List visits = new java.util.ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String v) { + name = v; + } + + public GeneratedAddress getHome() { + return home; + } + + public void setHome(GeneratedAddress v) { + home = v; + } + + public java.util.List getVisits() { + return visits; + } + + public void setVisits(java.util.List v) { + visits = v; + } +} diff --git a/integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/Generated.fxml b/integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/Generated.fxml new file mode 100644 index 0000000..7fb4f2f --- /dev/null +++ b/integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/Generated.fxml @@ -0,0 +1,3 @@ + + + diff --git a/integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/GeneratedAddress.fxml b/integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/GeneratedAddress.fxml new file mode 100644 index 0000000..4114d60 --- /dev/null +++ b/integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/GeneratedAddress.fxml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/GeneratedExplicit.fxml b/integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/GeneratedExplicit.fxml new file mode 100644 index 0000000..79170aa --- /dev/null +++ b/integration-fixtures/preview/classpath/app/src/main/resources/fixture/components/GeneratedExplicit.fxml @@ -0,0 +1,3 @@ + + + diff --git a/integration-fixtures/preview/classpath/app/src/test/java/fixture/checks/GeneratedViewModelChecks.java b/integration-fixtures/preview/classpath/app/src/test/java/fixture/checks/GeneratedViewModelChecks.java new file mode 100644 index 0000000..4b93039 --- /dev/null +++ b/integration-fixtures/preview/classpath/app/src/test/java/fixture/checks/GeneratedViewModelChecks.java @@ -0,0 +1,109 @@ +package fixture.checks; + +import de.neitzel.fx.view.*; +import de.neitzel.fxdesigner.target.BindingDiscovery; +import de.neitzel.injection.ApplicationContext; +import fixture.components.CustomGeneratedPerson; +import fixture.generated.*; +import fixture.model.*; +import java.util.concurrent.*; +import javafx.application.Platform; +import javafx.scene.control.TextField; + +public final class GeneratedViewModelChecks { + public static void run() throws Exception { + var loader = GeneratedViewModelChecks.class.getClassLoader(); + String xml; + try (var in = loader.getResourceAsStream("fixture/components/Generated.fxml")) { + xml = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); + } + var metadata = BindingDiscovery.inspect(xml, "home.street", "both", loader); + PreviewChecks.check( + metadata.viewModel().equals(CustomGeneratedPerson.class.getName()), + "Registry discovery default"); + PreviewChecks.check( + metadata.problems().stream().noneMatch(p -> p.severity().equals("ERROR")), + "Generated binding validation: " + metadata.problems()); + PreviewChecks.check( + System.getProperty("fixture.generated.initialized") == null, + "Registry discovery initialized Model"); + var ready = new CountDownLatch(1); + Platform.startup( + () -> { + Platform.setImplicitExit(false); + ready.countDown(); + }); + if (!ready.await(10, TimeUnit.SECONDS)) throw new AssertionError("Toolkit startup"); + try { + PreviewChecks.fx( + () -> { + var model = new GeneratedPerson(); + model.getHome().setNext(model.getHome()); + model.getVisits().add(model.getHome()); + var context = new ApplicationContext("fixture.components"); + var view = + ViewLoader.load( + loader.getResource("fixture/components/Generated.fxml"), model, context); + var vm = (CustomGeneratedPerson) view.getViewModel(); + PreviewChecks.check( + vm.nameProperty() == view.property("name"), "Stored property identity"); + ((TextField) view.getRoot().lookup("#name")).setText("Typed writeback"); + PreviewChecks.check( + model.getName().equals("Typed writeback") && vm.getName().equals(model.getName()), + "Typed writeback"); + var child = vm.getHome(); + PreviewChecks.check(child.getNext() == child, "Typed cycle identity"); + var direct = + ViewLoader.load( + loader.getResource("fixture/components/GeneratedAddress.fxml"), + model.getHome(), + context); + PreviewChecks.check(direct.getViewModel() == child, "Nested/direct identity"); + child.setStreet("Changed child"); + PreviewChecks.check( + model.getHome().getStreet().equals("Changed child"), "Child writeback"); + PreviewChecks.check( + vm.getVisits().getFirst() == model.getHome(), "Collection retains Models"); + var again = + ViewLoader.load( + loader.getResource("fixture/components/GeneratedExplicit.fxml"), + model, + context); + PreviewChecks.check( + again.getViewModel() == vm, "Custom satisfies generated superclass request"); + var fresh = + ViewLoader.load( + loader.getResource("fixture/components/GeneratedExplicit.fxml"), + new GeneratedPerson(), + context); + PreviewChecks.check( + fresh.getViewModel().getClass() == GeneratedPersonViewModel.class, + "Explicit overrides configured default"); + vm.setHome(null); + PreviewChecks.check( + model.getHome() == null && vm.getHome() == null, "Null nested writeback"); + var employee = new GeneratedEmployee(); + var inherited = + ViewLoader.load( + loader.getResource("fixture/components/Generated.fxml"), employee, context); + PreviewChecks.check( + inherited.getViewModel() instanceof GeneratedEmployeeViewModel, + "Most-specific generated default"); + var evm = (GeneratedEmployeeViewModel) inherited.getViewModel(); + evm.setSalary(55); + evm.setName("Inherited"); + PreviewChecks.check( + employee.getSalary() == 55 && employee.getName().equals("Inherited"), + "Inherited typed access"); + vm.addMessage(new Message(Message.Severity.INFO, "Generated notice", "name")); + PreviewChecks.check( + view.property("latestMessage").getValue().equals("Generated notice"), + "Shared messages"); + return null; + }); + System.out.println("PASS generated " + PreviewChecks.checks); + } finally { + Platform.exit(); + } + } +} diff --git a/integration-fixtures/preview/classpath/app/src/test/java/fixture/checks/PreviewChecks.java b/integration-fixtures/preview/classpath/app/src/test/java/fixture/checks/PreviewChecks.java index 57b5ee1..0f02da9 100644 --- a/integration-fixtures/preview/classpath/app/src/test/java/fixture/checks/PreviewChecks.java +++ b/integration-fixtures/preview/classpath/app/src/test/java/fixture/checks/PreviewChecks.java @@ -41,6 +41,10 @@ public final class PreviewChecks { public static void main(String[] args) throws Exception { String mode = args[0]; + if (mode.equals("generated")) { + GeneratedViewModelChecks.run(); + return; + } if (mode.equals("viewmodels")) { ViewModelRuntimeChecks.run(); return; diff --git a/integration-fixtures/preview/modulepath/app/pom.xml b/integration-fixtures/preview/modulepath/app/pom.xml index 9e5f7a1..e33a45a 100644 --- a/integration-fixtures/preview/modulepath/app/pom.xml +++ b/integration-fixtures/preview/modulepath/app/pom.xml @@ -24,7 +24,7 @@ org.apache.maven.pluginsmaven-compiler-plugin de.neitzel.fxdesignerdesigner-model-processor0.1.0-SNAPSHOT - -Adesigner.models=fixture.model.Person,fixture.model.Address + -Adesigner.models=fixture.model.Person,fixture.model.Address,fixture.model.GeneratedPerson,fixture.model.GeneratedEmployee-Adesigner.viewModels=true-Adesigner.viewModelModels=fixture.model.GeneratedPerson,fixture.model.GeneratedEmployee-Adesigner.generatedPackage=fixture.generated-Adesigner.viewModelDefaults=fixture.model.GeneratedPerson=fixture.components.CustomGeneratedPerson diff --git a/integration-fixtures/preview/modulepath/app/src/main/java/fixture/components/CustomGeneratedPerson.java b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/components/CustomGeneratedPerson.java new file mode 100644 index 0000000..50e1eb0 --- /dev/null +++ b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/components/CustomGeneratedPerson.java @@ -0,0 +1,11 @@ +package fixture.components; + +public final class CustomGeneratedPerson extends fixture.generated.GeneratedPersonViewModel { + public CustomGeneratedPerson(fixture.model.GeneratedPerson model) { + super(model); + } + + public String greeting() { + return "Hello " + getName(); + } +} diff --git a/integration-fixtures/preview/modulepath/app/src/main/java/fixture/components/GeneratedController.java b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/components/GeneratedController.java new file mode 100644 index 0000000..4f75483 --- /dev/null +++ b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/components/GeneratedController.java @@ -0,0 +1,5 @@ +package fixture.components; + +@jakarta.inject.Singleton +@de.neitzel.fx.view.ViewModelClass(fixture.generated.GeneratedPersonViewModel.class) +public class GeneratedController {} diff --git a/integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedAddress.java b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedAddress.java new file mode 100644 index 0000000..a090456 --- /dev/null +++ b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedAddress.java @@ -0,0 +1,22 @@ +package fixture.model; + +public class GeneratedAddress { + private String street = "Generated street"; + private GeneratedAddress next; + + public String getStreet() { + return street; + } + + public void setStreet(String v) { + street = v; + } + + public GeneratedAddress getNext() { + return next; + } + + public void setNext(GeneratedAddress v) { + next = v; + } +} diff --git a/integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedEmployee.java b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedEmployee.java new file mode 100644 index 0000000..0d7d23a --- /dev/null +++ b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedEmployee.java @@ -0,0 +1,13 @@ +package fixture.model; + +public class GeneratedEmployee extends GeneratedPerson { + private int salary = 42; + + public int getSalary() { + return salary; + } + + public void setSalary(int v) { + salary = v; + } +} diff --git a/integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedPerson.java b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedPerson.java new file mode 100644 index 0000000..d353793 --- /dev/null +++ b/integration-fixtures/preview/modulepath/app/src/main/java/fixture/model/GeneratedPerson.java @@ -0,0 +1,35 @@ +package fixture.model; + +public class GeneratedPerson { + static { + System.setProperty("fixture.generated.initialized", "yes"); + } + + private String name = "Generated sample"; + private GeneratedAddress home = new GeneratedAddress(); + private java.util.List visits = new java.util.ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String v) { + name = v; + } + + public GeneratedAddress getHome() { + return home; + } + + public void setHome(GeneratedAddress v) { + home = v; + } + + public java.util.List getVisits() { + return visits; + } + + public void setVisits(java.util.List v) { + visits = v; + } +} diff --git a/integration-fixtures/preview/modulepath/app/src/main/java/module-info.java b/integration-fixtures/preview/modulepath/app/src/main/java/module-info.java index 2e3f552..1503475 100644 --- a/integration-fixtures/preview/modulepath/app/src/main/java/module-info.java +++ b/integration-fixtures/preview/modulepath/app/src/main/java/module-info.java @@ -1,5 +1,13 @@ module fixture.app { requires fx; + + provides de.neitzel.fx.view.ViewModelRegistry with + fixture.generated.GeneratedViewModelRegistry; + + exports fixture.generated; + + opens fixture.generated; + requires de.neitzel.fxdesigner.preview; requires injection; requires jakarta.inject; diff --git a/integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/Generated.fxml b/integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/Generated.fxml new file mode 100644 index 0000000..7fb4f2f --- /dev/null +++ b/integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/Generated.fxml @@ -0,0 +1,3 @@ + + + diff --git a/integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/GeneratedAddress.fxml b/integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/GeneratedAddress.fxml new file mode 100644 index 0000000..4114d60 --- /dev/null +++ b/integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/GeneratedAddress.fxml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/GeneratedExplicit.fxml b/integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/GeneratedExplicit.fxml new file mode 100644 index 0000000..79170aa --- /dev/null +++ b/integration-fixtures/preview/modulepath/app/src/main/resources/fixture/components/GeneratedExplicit.fxml @@ -0,0 +1,3 @@ + + + diff --git a/integration-fixtures/preview/modulepath/checks/src/main/java/fixture/checks/GeneratedViewModelChecks.java b/integration-fixtures/preview/modulepath/checks/src/main/java/fixture/checks/GeneratedViewModelChecks.java new file mode 100644 index 0000000..4b93039 --- /dev/null +++ b/integration-fixtures/preview/modulepath/checks/src/main/java/fixture/checks/GeneratedViewModelChecks.java @@ -0,0 +1,109 @@ +package fixture.checks; + +import de.neitzel.fx.view.*; +import de.neitzel.fxdesigner.target.BindingDiscovery; +import de.neitzel.injection.ApplicationContext; +import fixture.components.CustomGeneratedPerson; +import fixture.generated.*; +import fixture.model.*; +import java.util.concurrent.*; +import javafx.application.Platform; +import javafx.scene.control.TextField; + +public final class GeneratedViewModelChecks { + public static void run() throws Exception { + var loader = GeneratedViewModelChecks.class.getClassLoader(); + String xml; + try (var in = loader.getResourceAsStream("fixture/components/Generated.fxml")) { + xml = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); + } + var metadata = BindingDiscovery.inspect(xml, "home.street", "both", loader); + PreviewChecks.check( + metadata.viewModel().equals(CustomGeneratedPerson.class.getName()), + "Registry discovery default"); + PreviewChecks.check( + metadata.problems().stream().noneMatch(p -> p.severity().equals("ERROR")), + "Generated binding validation: " + metadata.problems()); + PreviewChecks.check( + System.getProperty("fixture.generated.initialized") == null, + "Registry discovery initialized Model"); + var ready = new CountDownLatch(1); + Platform.startup( + () -> { + Platform.setImplicitExit(false); + ready.countDown(); + }); + if (!ready.await(10, TimeUnit.SECONDS)) throw new AssertionError("Toolkit startup"); + try { + PreviewChecks.fx( + () -> { + var model = new GeneratedPerson(); + model.getHome().setNext(model.getHome()); + model.getVisits().add(model.getHome()); + var context = new ApplicationContext("fixture.components"); + var view = + ViewLoader.load( + loader.getResource("fixture/components/Generated.fxml"), model, context); + var vm = (CustomGeneratedPerson) view.getViewModel(); + PreviewChecks.check( + vm.nameProperty() == view.property("name"), "Stored property identity"); + ((TextField) view.getRoot().lookup("#name")).setText("Typed writeback"); + PreviewChecks.check( + model.getName().equals("Typed writeback") && vm.getName().equals(model.getName()), + "Typed writeback"); + var child = vm.getHome(); + PreviewChecks.check(child.getNext() == child, "Typed cycle identity"); + var direct = + ViewLoader.load( + loader.getResource("fixture/components/GeneratedAddress.fxml"), + model.getHome(), + context); + PreviewChecks.check(direct.getViewModel() == child, "Nested/direct identity"); + child.setStreet("Changed child"); + PreviewChecks.check( + model.getHome().getStreet().equals("Changed child"), "Child writeback"); + PreviewChecks.check( + vm.getVisits().getFirst() == model.getHome(), "Collection retains Models"); + var again = + ViewLoader.load( + loader.getResource("fixture/components/GeneratedExplicit.fxml"), + model, + context); + PreviewChecks.check( + again.getViewModel() == vm, "Custom satisfies generated superclass request"); + var fresh = + ViewLoader.load( + loader.getResource("fixture/components/GeneratedExplicit.fxml"), + new GeneratedPerson(), + context); + PreviewChecks.check( + fresh.getViewModel().getClass() == GeneratedPersonViewModel.class, + "Explicit overrides configured default"); + vm.setHome(null); + PreviewChecks.check( + model.getHome() == null && vm.getHome() == null, "Null nested writeback"); + var employee = new GeneratedEmployee(); + var inherited = + ViewLoader.load( + loader.getResource("fixture/components/Generated.fxml"), employee, context); + PreviewChecks.check( + inherited.getViewModel() instanceof GeneratedEmployeeViewModel, + "Most-specific generated default"); + var evm = (GeneratedEmployeeViewModel) inherited.getViewModel(); + evm.setSalary(55); + evm.setName("Inherited"); + PreviewChecks.check( + employee.getSalary() == 55 && employee.getName().equals("Inherited"), + "Inherited typed access"); + vm.addMessage(new Message(Message.Severity.INFO, "Generated notice", "name")); + PreviewChecks.check( + view.property("latestMessage").getValue().equals("Generated notice"), + "Shared messages"); + return null; + }); + System.out.println("PASS generated " + PreviewChecks.checks); + } finally { + Platform.exit(); + } + } +} diff --git a/integration-fixtures/preview/modulepath/checks/src/main/java/fixture/checks/PreviewChecks.java b/integration-fixtures/preview/modulepath/checks/src/main/java/fixture/checks/PreviewChecks.java index 57b5ee1..0f02da9 100644 --- a/integration-fixtures/preview/modulepath/checks/src/main/java/fixture/checks/PreviewChecks.java +++ b/integration-fixtures/preview/modulepath/checks/src/main/java/fixture/checks/PreviewChecks.java @@ -41,6 +41,10 @@ public final class PreviewChecks { public static void main(String[] args) throws Exception { String mode = args[0]; + if (mode.equals("generated")) { + GeneratedViewModelChecks.run(); + return; + } if (mode.equals("viewmodels")) { ViewModelRuntimeChecks.run(); return; diff --git a/scripts/check-preview-fixtures.py b/scripts/check-preview-fixtures.py index b6ef821..a97ccdd 100644 --- a/scripts/check-preview-fixtures.py +++ b/scripts/check-preview-fixtures.py @@ -64,7 +64,7 @@ with tempfile.TemporaryDirectory(prefix="designer-preview-") as temp: first = None for mode in ("discovery", "bindings", "standard", "async", "timeout", "cancel", "readiness-failure", "wrong-model", "invalid-attribute", - "cleanup-failure", "factory-failure", "viewmodels", "full", "full-empty", "card", "unstable"): + "cleanup-failure", "factory-failure", "viewmodels", "generated", "full", "full-empty", "card", "unstable"): log = launch(path, arrangement, mode, arrangement + "-" + mode) if mode == "standard": first = re.search(r"SESSION (\S+) models=1", log)[1]