Skip to content

Commit

Permalink
Added a feature to allow traits to "shade" entity properties. This wi…
Browse files Browse the repository at this point in the history
…ll make it easier to create custom traits and still maintain property ordering
  • Loading branch information
EricWittmann committed Jan 7, 2025
1 parent 7fba1c9 commit 875a1d3
Show file tree
Hide file tree
Showing 19 changed files with 490 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@

package io.apicurio.umg;

import java.util.Collection;

import io.apicurio.umg.logging.Logger;
import io.apicurio.umg.models.spec.SpecificationModel;
import io.apicurio.umg.pipe.GeneratorState;
import io.apicurio.umg.pipe.Pipeline;
import io.apicurio.umg.pipe.concept.CreateEntityModelsStage;
import io.apicurio.umg.pipe.concept.CreateImplicitUnionRulesStage;
import io.apicurio.umg.pipe.concept.CreateNamespaceModelsStage;
import io.apicurio.umg.pipe.concept.CreateParentTraitsStage;
import io.apicurio.umg.pipe.concept.CreatePropertyComparatorStage;
import io.apicurio.umg.pipe.concept.CreatePropertyModelsStage;
import io.apicurio.umg.pipe.concept.CreateTraitModelsStage;
Expand All @@ -36,6 +33,7 @@
import io.apicurio.umg.pipe.concept.NormalizePropertiesStage;
import io.apicurio.umg.pipe.concept.NormalizeTraitsStage;
import io.apicurio.umg.pipe.concept.NormalizeVisitorsStage;
import io.apicurio.umg.pipe.concept.RemoveShadedPropertyModelsStage;
import io.apicurio.umg.pipe.concept.RemoveTransparentTraitsStage;
import io.apicurio.umg.pipe.concept.ResolveVisitorEntityStage;
import io.apicurio.umg.pipe.concept.SpecificationValidationStage;
Expand Down Expand Up @@ -71,6 +69,8 @@
import io.apicurio.umg.pipe.java.OrganizeImportsStage;
import io.apicurio.umg.pipe.java.RemoveUnusedImportsStage;

import java.util.Collection;

/**
* @author [email protected]
*/
Expand Down Expand Up @@ -110,9 +110,10 @@ public void generate() throws Exception {
pipe.addStage(new CreateNamespaceModelsStage());
pipe.addStage(new CreateTraitModelsStage());
pipe.addStage(new CreateEntityModelsStage());
// pipe.addStage(new CreateParentTraitsStage());
pipe.addStage(new CreatePropertyModelsStage());
pipe.addStage(new CreateParentTraitsStage());
pipe.addStage(new CreateVisitorsStage());
pipe.addStage(new RemoveShadedPropertyModelsStage());

// Implicit model creation phase
pipe.addStage(new CreateImplicitUnionRulesStage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class PropertyModel {

private PropertyType type;

private boolean shaded;

public UnionRule getRuleFor(String rawUnionSubtype) {
if (unionRules != null) {
return unionRules.stream().filter(rule -> rule.getUnionType().equals(rawUnionSubtype)).findFirst().orElse(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ protected void doProcess() {
* Returns true if the given entity/property combination is not unique across the models. If the
* combination is unique, then no parent trait is needed. If it is NOT unique (meaning there
* are other entities with the same property) then a parent IS needed.
*
* <br/>
* In other words, we're trying to find multiple entities that share a child entity property.
* So if "Foo" and "Bar" entities both have a property called "widget" of type "Widget", then
* we want those entities to both have a "WigetParent" trait.
* we want those entities to both have a "WidgetParent" trait.
*
* @param entity
* @param property
Expand All @@ -90,7 +90,8 @@ private boolean needsParent(EntityModel entity, PropertyModel property) {
.filter(e -> !e.getName().equals(entity.getName()))
.filter(e -> e.hasProperty(property.getName()))
.filter(e -> e.getProperties().get(property.getName()).equals(property))
.count() > 0;
.findFirst()
.isPresent();
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package io.apicurio.umg.pipe.concept;

import io.apicurio.umg.beans.Property;
import io.apicurio.umg.models.concept.EntityModel;
import io.apicurio.umg.models.concept.PropertyModel;
import io.apicurio.umg.models.concept.PropertyType;
import io.apicurio.umg.models.concept.TraitModel;
import io.apicurio.umg.pipe.AbstractStage;

import java.util.Collection;
import java.util.Map;

/**
* Creates the models for all properties for both Traits and Entities. This stage
* assumes that the Trait and Entity models have already been created by previous
* stages in the pipe.
*/
public class CreatePropertyModelsStage extends AbstractStage {

@Override
Expand Down Expand Up @@ -42,11 +51,47 @@ protected void doProcess() {
.unionRules(property.getUnionRules())
.rawType(property.getType())
.type(PropertyType.parse(property.getType()))
.shaded(isPropertyShaded(property, entityModel))
.build();
info("Created entity property model: %s/%s", entityModel.fullyQualifiedName(), propertyModel.getName());
entityModel.getProperties().put(property.getName(), propertyModel);
});
});
});
}

/**
* A property is considered shaded if there exists a Trait on the property's Entity
* that contains the same property. Why do this? The reason is to make it more
* convenient to create simple "Parent" interfaces (traits) for certain properties
* that may be shared across multiple Entities.
* <br />
* For example, in the OpenAPI specification, the same "servers" property exists
* at three levels:
* <br />
* <ol>
* <li>Document</li>
* <li>PathItem</li>
* <li>Operation</li>
* </ol>
* <br />
* It is convenient to create a "ServerParent" trait that can be shared by all
* three entities. This can be done by simply creating such a Trait and then
* removing the "servers" property from the three entity definitions. But in
* that case the property order is verbose to express and maintain. Instead,
* the property can be kept in the definitions of the entities AND ALSO broken
* out into a shared Trait.
*/
private boolean isPropertyShaded(Property property, EntityModel entityModel) {
Collection<TraitModel> traits = entityModel.getTraits();
if (traits != null) {
for (TraitModel trait : traits) {
Map<String, PropertyModel> traitProperties = trait.getProperties();
if (traitProperties.keySet().contains(property.getName())) {
return true;
}
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@
import io.apicurio.umg.models.concept.TraitModel;
import io.apicurio.umg.pipe.AbstractStage;

/**
* This stage is responsible for pushing properties as far up the trait and entity
* hierarchies as possible, to facilitate sharing across the specifications as best we can.
* For example, if v1 and v2 of a specification both define a "Foo" entity with a
* "widget" property, the definition of the property should ultimately live on a shared
* parent interface. So the following interfaces will be generated:
*
* <ul>
* <li>V1Foo</li>
* <li>V2Foo</li>
* <li>Foo</li>
* </ul>
*
* This stage ensures that, in the example above, the "widget" property will live
* on "Foo" rather than living separately on both "V1Foo" and "V2Foo".
*/
public class NormalizePropertiesStage extends AbstractStage {
@Override
protected void doProcess() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
import io.apicurio.umg.models.concept.TraitModel;
import io.apicurio.umg.pipe.AbstractStage;

/**
* This stage is responsible for generating a hierarchy of traits from the
* collection of traits defined in the various specification configs being
* processed. It tries to identify commonalities between all spec versions
* so that traits can be shared across them.
*/
public class NormalizeTraitsStage extends AbstractStage {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.apicurio.umg.pipe.concept;

import io.apicurio.umg.models.concept.PropertyModel;
import io.apicurio.umg.pipe.AbstractStage;

import java.util.Set;

/**
* Removes any shaded property models from their entities. A shaded property is one
* that is defined on an Entity but *also* defined on a Trait of that Entity. This
* is redundant and only supported to make property ordering easier in the spec
* definitions (this allows you to define a property in both a Trait and an Entity -
* only the Trait property will be used when generating code, but the property in the
* Entity will be used for ordering).
*/
public class RemoveShadedPropertyModelsStage extends AbstractStage {

@Override
protected void doProcess() {
info("-- Removing shaded Entity properties --");
getState().getConceptIndex().getAllEntitiesWithCopy().forEach(entity -> {
Set<String> propertyNames = Set.copyOf(entity.getProperties().keySet());
for (String propertyName : propertyNames) {
PropertyModel propertyModel = entity.getProperties().get(propertyName);
if (propertyModel.isShaded()) {
entity.getProperties().remove(propertyName);
}
}
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,27 @@ private void validateEntityProperties(SpecificationVersion specVer, Entity entit
fail("Entity '%s' in specification '%s' is missing its property ordering.", entity.getName(), specVer.getName());
}

// Check for any properties that are shaded by a Trait but with a different type. Traits
// can shade Entity properties by the types must be exactly the same.
{
List<Property> entityProperties = entity.getProperties();
for (Property entityProperty : entityProperties) {
List<String> traitNames = entity.getTraits();
for (String traitName : traitNames ) {
Trait trait = specVer.getTraits().stream().filter(t -> t.getName().equals(traitName)).findFirst().get();
List<Property> traitProperties = trait.getProperties();
for (Property traitProperty : traitProperties) {
if (traitProperty.getName().equals(entityProperty.getName())) {
if (!entityProperty.getType().equals(traitProperty.getType())) {
fail("Entity '%s' in specification '%s' contains a property '%s' that is illegally shaded by trait '%s' (types do not match).",
entity.getName(), specVer.getName(), entityProperty.getName(), traitName);
}
}
}
}
}
}

// Validate the "propertyOrder" field.
{
List<String> propertyOrder = entity.getPropertyOrder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ protected void createSetterBody(JavaSource<?> javaEntity, PropertyModel property

body.append("if (value != null) {");
body.append(" ((NodeImpl) value)._setParentPropertyName(\"${propertyName}\");");
body.append(" ((NodeImpl) value)._setParentPropertyType(ParentPropertyType.standard);");
body.append("}");
} else if (isUnion(property)) {
JavaEnumSource parentPropertyTypeSource = getState().getJavaIndex().lookupEnum(getParentPropertyTypeEnumFQN());
Expand All @@ -243,6 +244,7 @@ protected void createSetterBody(JavaSource<?> javaEntity, PropertyModel property
body.append("if (value != null) {");
body.append(" if (value.isEntity()) {");
body.append(" ((NodeImpl) value)._setParentPropertyName(\"${propertyName}\");");
body.append(" ((NodeImpl) value)._setParentPropertyType(ParentPropertyType.standard);");
body.append(" } else if (value.isEntityList()) {");
body.append(" List<?> entityList = (List<?>) ((UnionValue<?>) value).getValue();");
body.append(" for (Object entity : entityList) {");
Expand Down
42 changes: 40 additions & 2 deletions generator/src/test/java/io/apicurio/umg/GeneratorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
public class GeneratorTest {

@Test
public void testGenerator() throws Exception {
public void testGenerator_OpenApi() throws Exception {
File outputDir;

String outputDirPath = System.getenv("GENERATE_TEST_OUTPUT_DIR");
if (outputDirPath != null) {
outputDir = new File(outputDirPath);
outputDir = new File(new File(outputDirPath), "testGenerator_OpenApi");
outputDir.mkdirs();

System.out.println("[OpenAPI] Output directory: " + outputDir);
} else {
outputDir = Files.createTempDirectory(GeneratorTest.class.getSimpleName()).toFile();
}
Expand All @@ -44,7 +46,43 @@ public void testGenerator() throws Exception {
FileUtils.deleteDirectory(umgTestOutputDir);
}
}
}

@Test
public void testGenerator_ParentTrait() throws Exception {
File outputDir;

String outputDirPath = System.getenv("GENERATE_TEST_OUTPUT_DIR");
if (outputDirPath != null) {
outputDir = new File(new File(outputDirPath), "testGenerator_ParentTrait");
outputDir.mkdirs();

System.out.println("[Parent Trait] Output directory: " + outputDir);
} else {
outputDir = Files.createTempDirectory(GeneratorTest.class.getSimpleName()).toFile();
}

File umgTestOutputDir = Files.createTempDirectory(GeneratorTest.class.getSimpleName() + "-test").toFile();
UnifiedModelGeneratorConfig config = UnifiedModelGeneratorConfig.builder()
.outputDirectory(outputDir)
.testOutputDirectory(umgTestOutputDir)
.generateTestFixtures(false)
.rootNamespace("io.apicurio.umg.test").build();
// Load the specs
List<SpecificationModel> specs = List.of(
SpecificationLoader.loadSpec(GeneratorTest.class.getResource("parent-trait-spec.yaml"))
);
// Create a unified model generator
UnifiedModelGenerator generator = new UnifiedModelGenerator(config, specs);
// Generate the source code into the target output directory.
try {
generator.generate();
} finally {
if (outputDirPath == null) {
FileUtils.deleteDirectory(outputDir);
FileUtils.deleteDirectory(umgTestOutputDir);
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Parent Trait Test API 1.0
version: 1.0
versions:
- version: 1.0
url: https://example.com/ParentTrait/versions/1.0.md
prefix: Pt10
namespace: io.apicurio.umg.test.v10

traits:
- name: FooParent
properties:
- name: foo
type: Foo

entities:
- name: Document
root: true
traits:
- FooParent
properties:
- name: id
type: string
- name: foo
type: Foo
- name: bar
type: Bar
- name: definitions
type: Definitions
propertyOrder:
- $this

- name: Foo
properties:
- name: name
type: string
- name: description
type: string
propertyOrder:
- $this

- name: Bar
properties:
- name: type
type: string
- name: color
type: string
propertyOrder:
- $this

- name: Baz
properties:
- name: length
type: number
propertyOrder:
- $this

- name: Definitions
traits:
- FooParent
properties:
- name: foo
type: Foo
- name: baz
type: Baz
propertyOrder:
- $this

Loading

0 comments on commit 875a1d3

Please sign in to comment.