Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2af877e
feat(generator): add DiffVisitor/DiffTraverser base infrastructure (C…
jsenko Jun 29, 2026
7a4bb38
feat(generator): DiffTraverser generation stage (C36 Phase 2)
jsenko Jun 29, 2026
0e6fdf0
feat(generator): typed per-field DiffVisitor, consolidate pairing str…
jsenko Jun 30, 2026
7c236df
refactor: split PairingStrategy into MapPairingStrategy and ListPairi…
jsenko Jun 30, 2026
9394bc8
refactor: remove PairingStrategy marker, add P generic to strategy in…
jsenko Jun 30, 2026
798fc9d
refactor: remove key/index params from item visitor methods (C36)
jsenko Jun 30, 2026
14a6b1c
refactor: extract PairingStrategyProvider, strip strategy from DiffVi…
jsenko Jun 30, 2026
824e0d7
feat: thread P generic through diff infrastructure, add DefaultPairin…
jsenko Jun 30, 2026
177c474
refactor: make generated DiffTraverser and DiffVisitor generic with <…
jsenko Jun 30, 2026
fca63a9
refactor: pass original/updated collections to diff methods, singular…
jsenko Jun 30, 2026
c571a9d
fix: exclude visitors/diff from JSweet transpilation (C36)
jsenko Jun 30, 2026
57eb14b
refactor: remove DiffVisitor base, rename strategies, add traverse(An…
jsenko Jun 30, 2026
3adf485
feat(generator): union traverse methods, auto-recursion for entity fi…
jsenko Jul 1, 2026
969bde3
fix(generator): add auto-recursion for union collections in DiffTrave…
jsenko Jul 1, 2026
a7a621c
refactor: replace traverseNode with traverse(Any, Any) dispatch (C36)
jsenko Jul 1, 2026
caf138f
fix: skip entity dispatch branches covered by union dispatch (C36)
jsenko Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data-models/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@
<exclude>**/_*/**</exclude>
<exclude>**/jsonschema/compat/**</exclude>
<exclude>**/jsonschema/ref/**</exclude>
<exclude>**/visitors/diff/**</exclude>
<exclude>**/*DiffTraverser.java</exclude>
<exclude>**/*DiffVisitor.java</exclude>
</excludes>
</configuration>
<executions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
import io.apitomy.umg.pipe.java.CreateReadersStage;
import io.apitomy.umg.pipe.java.CreateTestFixturesStage;
import io.apitomy.umg.pipe.java.CreateTraitInterfacesStage;
import io.apitomy.umg.pipe.java.CreateDiffTraversersStage;
import io.apitomy.umg.pipe.java.CreateDiffVisitorsStage;
import io.apitomy.umg.pipe.java.CreateTraversersStage;
import io.apitomy.umg.pipe.java.CreateCollectionUnionValuesStage;
import io.apitomy.umg.pipe.java.CreatePrimitiveUnionValuesStage;
Expand Down Expand Up @@ -163,6 +165,8 @@ public void generate() throws Exception {
pipe.addStage(new CreateWriterDispatchersStage());
pipe.addStage(new CreateClonerDispatchersStage());
pipe.addStage(new CreateTraversersStage());
pipe.addStage(new CreateDiffVisitorsStage());
pipe.addStage(new CreateDiffTraversersStage());
pipe.addStage(new CreateReaderFactoryStage());
pipe.addStage(new CreateWriterFactoryStage());
pipe.addStage(new CreateClonerFactoryStage());
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package io.apitomy.umg.pipe.java;

import java.util.Collection;

import org.apache.commons.lang3.StringUtils;
import org.jboss.forge.roaster.Roaster;
import org.jboss.forge.roaster.model.source.JavaClassSource;
import org.jboss.forge.roaster.model.source.MethodSource;

import io.apitomy.umg.beans.SpecificationVersion;
import io.apitomy.umg.pipe.java.method.CodeGenContext;
import io.apitomy.umg.models.concept.EntityModel;
import io.apitomy.umg.models.concept.PropertyModel;
import io.apitomy.umg.models.concept.NamespaceModel;
import io.apitomy.umg.models.concept.PropertyModelWithOrigin;
import io.apitomy.umg.models.concept.type.ListType;
import io.apitomy.umg.models.concept.type.MapType;
import io.apitomy.umg.models.java.type.JavaType;
import io.apitomy.umg.models.java.type.JavaTypeFactory;

/**
* Creates a DiffVisitor abstract class per spec version. Each generated visitor
* contains typed, field-specific methods for every entity and property in the spec.
*/
public class CreateDiffVisitorsStage extends AbstractJavaStage {

@Override
protected void doProcess() {
getState().getSpecIndex().getAllSpecificationVersions().forEach(this::createDiffVisitor);
}

private void createDiffVisitor(SpecificationVersion specVer) {
String packageName = getTraverserPackageName(specVer);
String className = specVer.getPrefix() + "DiffVisitor";

debug("Creating diff visitor: " + className);

JavaClassSource classSource = Roaster.create(JavaClassSource.class)
.setPackage(packageName)
.setName(className)
.setPublic()
.setAbstract(true);

classSource.addTypeVariable("P");

JavaTypeFactory jtf = getJavaTypeFactory();

specVer.getEntities().forEach(entity -> {
EntityModel entityModel = getState().getConceptIndex()
.lookupEntity(specVer.getNamespace() + "." + entity.getName());
if (entityModel == null) {
return;
}

createEntityVisitMethod(classSource, entityModel);
createPropertyMethods(classSource, entityModel, jtf);
});

// Create diff methods for union types
String namespace = specVer.getNamespace();
var nsModel = getState().getConceptIndex().lookupNamespace(namespace);
getState().getConceptIndex().findTypes(namespace).stream()
.filter(t -> t instanceof io.apitomy.umg.models.concept.type.UnionType)
.map(t -> (io.apitomy.umg.models.concept.type.UnionType) t)
.forEach(unionType -> {
var jt = jtf.createJavaType(unionType, nsModel);
jt.addImportsTo(classSource);
String unionTypeName = jt.getSimpleName();
String methodName = "diff" + unionTypeName;
if (!hasMethod(classSource, methodName)) {
MethodSource<JavaClassSource> method = classSource.addMethod()
.setName(methodName)
.setReturnTypeVoid()
.setPublic();
method.addParameter(jt.toJavaTypeString(), "original");
method.addParameter(jt.toJavaTypeString(), "updated");
method.setBody("");
}
});

getState().getJavaIndex().index(classSource);
}

private void createEntityVisitMethod(JavaClassSource classSource, EntityModel entityModel) {
var javaEntity = lookupJavaEntity(entityModel);
classSource.addImport(javaEntity);

String methodName = "visit" + entityModel.getName();
MethodSource<JavaClassSource> method = classSource.addMethod()
.setName(methodName)
.setReturnType(boolean.class)
.setPublic();
method.addParameter(javaEntity.getName(), "original");
method.addParameter(javaEntity.getName(), "updated");
method.setBody("return true;");
}

private void createPropertyMethods(JavaClassSource classSource, EntityModel entityModel,
JavaTypeFactory jtf) {
Collection<PropertyModelWithOrigin> allProperties =
getState().getConceptIndex().getAllEntityProperties(entityModel);

for (PropertyModelWithOrigin propWithOrigin : allProperties) {
PropertyModel property = propWithOrigin.getProperty();
if (isStarProperty(property) || isRegexProperty(property)) {
continue;
}

String entityName = entityModel.getName();
String fieldSuffix = fieldMethodSuffix(property);
NamespaceModel ns = propWithOrigin.getOrigin().getNamespace();

if (isEntity(property)) {
createEntityFieldMethod(classSource, entityName, fieldSuffix, property, ns, jtf);
} else if (isEntityList(property)) {
createListFieldMethods(classSource, entityName, fieldSuffix, property, ns, jtf);
} else if (isEntityMap(property)) {
createMapFieldMethods(classSource, entityName, fieldSuffix, property, ns, jtf);
} else if (isUnion(property)) {
createUnionFieldMethod(classSource, entityName, fieldSuffix, property, ns, jtf);
} else if (isUnionList(property)) {
createListFieldMethods(classSource, entityName, fieldSuffix, property, ns, jtf);
} else if (isUnionMap(property)) {
createMapFieldMethods(classSource, entityName, fieldSuffix, property, ns, jtf);
} else if (isPrimitive(property) || isPrimitiveList(property) || isPrimitiveMap(property)) {
createPrimitiveFieldMethod(classSource, entityName, fieldSuffix, property, ns, jtf);
}
}
}

private void createPrimitiveFieldMethod(JavaClassSource classSource, String entityName,
String fieldSuffix, PropertyModel property, NamespaceModel ns, JavaTypeFactory jtf) {
JavaType jt = jtf.createJavaType(property.getResolvedType(), ns);
jt.addImportsTo(classSource);

String methodName = "diff" + entityName + fieldSuffix;
MethodSource<JavaClassSource> method = classSource.addMethod()
.setName(methodName)
.setReturnTypeVoid()
.setPublic();
method.addParameter(jt.toJavaTypeString(), "original");
method.addParameter(jt.toJavaTypeString(), "updated");
method.setBody("");
}

private void createEntityFieldMethod(JavaClassSource classSource, String entityName,
String fieldSuffix, PropertyModel property, NamespaceModel ns, JavaTypeFactory jtf) {
JavaType jt = jtf.createJavaType(property.getResolvedType(), ns);
jt.addImportsTo(classSource);

String methodName = "diff" + entityName + fieldSuffix;
MethodSource<JavaClassSource> method = classSource.addMethod()
.setName(methodName)
.setReturnTypeVoid()
.setPublic();
method.addParameter(jt.toJavaTypeString(), "original");
method.addParameter(jt.toJavaTypeString(), "updated");
method.setBody("");
}

private void createUnionFieldMethod(JavaClassSource classSource, String entityName,
String fieldSuffix, PropertyModel property, NamespaceModel ns, JavaTypeFactory jtf) {
JavaType jt = jtf.createJavaType(property.getResolvedType(), ns);
jt.addImportsTo(classSource);

String methodName = "diff" + entityName + fieldSuffix;
MethodSource<JavaClassSource> method = classSource.addMethod()
.setName(methodName)
.setReturnTypeVoid()
.setPublic();
method.addParameter(jt.toJavaTypeString(), "original");
method.addParameter(jt.toJavaTypeString(), "updated");
method.setBody("");
}

private void createListFieldMethods(JavaClassSource classSource, String entityName,
String fieldSuffix, PropertyModel property, NamespaceModel ns, JavaTypeFactory jtf) {
ListType listType = (ListType) property.getResolvedType();
JavaType valueJt = jtf.createJavaType(listType.getValueType(), ns);
valueJt.addImportsTo(classSource);

String collectionDiffFQN = getState().getConfig().getRootNamespace()
+ ".visitors.diff.CollectionDiff";
classSource.addImport(collectionDiffFQN);

classSource.addImport(java.util.List.class);

String diffMethodName = "diff" + entityName + fieldSuffix;
MethodSource<JavaClassSource> diffMethod = classSource.addMethod()
.setName(diffMethodName)
.setReturnTypeVoid()
.setPublic();
diffMethod.addParameter("List<" + valueJt.toJavaTypeString() + ">", "original");
diffMethod.addParameter("List<" + valueJt.toJavaTypeString() + ">", "updated");
diffMethod.addParameter("CollectionDiff<P," + valueJt.toJavaTypeString() + ">", "diff");
diffMethod.setBody("");

String visitMethodName = "visit" + entityName + fieldSuffix + "Item";
MethodSource<JavaClassSource> visitMethod = classSource.addMethod()
.setName(visitMethodName)
.setReturnTypeVoid()
.setPublic();
visitMethod.addParameter(valueJt.toJavaTypeString(), "original");
visitMethod.addParameter(valueJt.toJavaTypeString(), "updated");
visitMethod.setBody("");
}

private void createMapFieldMethods(JavaClassSource classSource, String entityName,
String fieldSuffix, PropertyModel property, NamespaceModel ns, JavaTypeFactory jtf) {
MapType mapType = (MapType) property.getResolvedType();
JavaType valueJt = jtf.createJavaType(mapType.getValueType(), ns);
valueJt.addImportsTo(classSource);

String collectionDiffFQN = getState().getConfig().getRootNamespace()
+ ".visitors.diff.CollectionDiff";
classSource.addImport(collectionDiffFQN);
classSource.addImport(java.util.Map.class);

String diffMethodName = "diff" + entityName + fieldSuffix;
MethodSource<JavaClassSource> diffMethod = classSource.addMethod()
.setName(diffMethodName)
.setReturnTypeVoid()
.setPublic();
diffMethod.addParameter("Map<String," + valueJt.toJavaTypeString() + ">", "original");
diffMethod.addParameter("Map<String," + valueJt.toJavaTypeString() + ">", "updated");
diffMethod.addParameter("CollectionDiff<P," + valueJt.toJavaTypeString() + ">", "diff");
diffMethod.setBody("");

String singularSuffix = singularize(fieldSuffix);
String visitMethodName = "visit" + entityName + singularSuffix;
MethodSource<JavaClassSource> visitMethod = classSource.addMethod()
.setName(visitMethodName)
.setReturnTypeVoid()
.setPublic();
visitMethod.addParameter(valueJt.toJavaTypeString(), "original");
visitMethod.addParameter(valueJt.toJavaTypeString(), "updated");
visitMethod.setBody("");
}

private static String singularize(String name) {
return CodeGenContext.singularize(name);
}

/**
* Turns a property name into a method name suffix.
* "title" → "Title", "$ref" → "$ref", "allOf" → "AllOf"
*/
private String fieldMethodSuffix(PropertyModel property) {
String name = property.getName();
if (name.startsWith("$")) {
return name;
}
return StringUtils.capitalize(name);
}

private boolean hasMethod(JavaClassSource classSource, String methodName) {
return classSource.getMethods().stream().anyMatch(m -> m.getName().equals(methodName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ protected void doProcess() {
"io.apitomy.umg.base.visitors.TraversalStep",
"io.apitomy.umg.base.visitors.TraversalContextImpl",
"io.apitomy.umg.base.visitors.ReverseTraverser",
"io.apitomy.umg.base.visitors.diff.AbstractDiffTraverser",
"io.apitomy.umg.base.visitors.diff.CollectionDiff",
"io.apitomy.umg.base.visitors.diff.DefaultPairingKey",
"io.apitomy.umg.base.visitors.diff.DefaultListPairingStrategy",
"io.apitomy.umg.base.visitors.diff.DefaultMapPairingStrategy",
"io.apitomy.umg.base.visitors.diff.DefaultPairingStrategyProvider",
"io.apitomy.umg.base.union.ListUnionValueImpl",
"io.apitomy.umg.base.union.MapUnionValueImpl",
"io.apitomy.umg.base.union.EntityListUnionValueImpl",
Expand All @@ -64,6 +70,10 @@ protected void doProcess() {
"io.apitomy.umg.base.union.MapUnionValue",
"io.apitomy.umg.base.union.PrimitiveUnionValue",
"io.apitomy.umg.base.union.StringListUnionValue",
"io.apitomy.umg.base.visitors.diff.ListPairingStrategy",
"io.apitomy.umg.base.visitors.diff.MapPairingStrategy",
"io.apitomy.umg.base.visitors.diff.PairingStrategyProvider",

"io.apitomy.umg.base.union.Union",
"io.apitomy.umg.base.union.UnionValue"
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ public Class<?> primitiveTypeToClass(Type type) {
return PrimitiveTypeUtil.primitiveTypeToClass(type);
}

public String singularize(String name) {
public static String singularize(String name) {
return inflector.singularize(name);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.apitomy.umg.base.visitors.diff;

import java.util.List;
import java.util.Map;

import io.apitomy.umg.base.Any;

/**
* Base class for generated diff traversers. Provides shared collection pairing logic.
* Subclasses (generated per spec version) provide entity-specific field iteration
* and call typed visitor methods.
*
* @param <P> the pairing key type
* @param <V> the spec-version-specific DiffVisitor subclass
*/
public class AbstractDiffTraverser<P, V> {

protected final V visitor;
protected final PairingStrategyProvider<P> pairingProvider;

@SuppressWarnings("unchecked")
protected AbstractDiffTraverser(V visitor) {
this.visitor = visitor;
this.pairingProvider = (PairingStrategyProvider<P>) new DefaultPairingStrategyProvider();
}

protected AbstractDiffTraverser(V visitor, PairingStrategyProvider<P> pairingProvider) {
this.visitor = visitor;
this.pairingProvider = pairingProvider;
}

/**
* Public entry point — accepts Any (Node or Union).
* Generated subclasses override this with type-based dispatch
* to union traverse methods and entity traverse methods.
*/
public void traverse(Any original, Any updated) {
}

protected <T> CollectionDiff<P, T> pairMap(String propertyName, Map<String, T> original, Map<String, T> updated) {
MapPairingStrategy<P, T> strategy = pairingProvider.getMapStrategy(propertyName);
return strategy.pair(original, updated);
}

protected <T> CollectionDiff<P, T> pairList(String propertyName, List<T> original, List<T> updated) {
ListPairingStrategy<P, T> strategy = pairingProvider.getListStrategy(propertyName);
return strategy.pair(original, updated);
}

}
Loading
Loading