Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,25 @@ public B flattenMixins() {
return (B) this;
}

/**
* Adds a mixin reference to the shape without triggering member recalculation.
*
* <p>This is used during mixin flattening to re-add interface mixin references
* after members/traits have already been flattened. Unlike {@link #addMixin(Shape)},
* this method does not trigger {@code NamedMemberUtils.cleanMixins()} in subclasses.
*
* @param shape Mixin shape to add as a reference.
* @return Returns the builder.
*/
@SuppressWarnings("unchecked")
public B addMixinRef(Shape shape) {
if (mixins == null) {
mixins = new LinkedHashMap<>();
}
mixins.put(shape.getId(), shape);
return (B) this;
}

Map<ShapeId, Shape> getMixins() {
return mixins == null ? Collections.emptyMap() : mixins;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import java.util.Map;
import java.util.Set;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.utils.SetUtils;
import software.amazon.smithy.utils.ToSmithyBuilder;
Expand All @@ -20,10 +22,12 @@ public final class MixinTrait extends AbstractTrait implements ToSmithyBuilder<M
public static final ShapeId ID = ShapeId.from("smithy.api#mixin");

private final Set<ShapeId> localTraits;
private final boolean isInterface;

private MixinTrait(Builder builder) {
super(ID, builder.sourceLocation);
this.localTraits = SetUtils.orderedCopyOf(builder.localTraits);
this.isInterface = builder.isInterface;
}

/**
Expand All @@ -35,31 +39,53 @@ public Set<ShapeId> getLocalTraits() {
return localTraits;
}

/**
* Gets whether this mixin should generate a Java interface.
*
* @return returns true if this mixin should generate a Java interface.
*/
public boolean isInterface() {
return isInterface;
}

/**
* Checks if the given shape is a mixin with {@code interface = true}.
*
* @param shape Shape to check.
* @return returns true if the shape has a mixin trait with interface set to true.
*/
public static boolean isInterfaceMixin(Shape shape) {
return shape.getTrait(MixinTrait.class).map(MixinTrait::isInterface).orElse(false);
}

@Override
protected Node createNode() {
ObjectNode.Builder builder = Node.objectNodeBuilder().sourceLocation(getSourceLocation());

// smithy.api#mixin is always present, so no need to serialize it.
if (localTraits.size() <= 1) {
return Node.objectNode();
if (localTraits.size() > 1) {
List<Node> nonImplicitValues = new ArrayList<>();
for (ShapeId trait : localTraits) {
if (!trait.equals(ID)) {
nonImplicitValues.add(Node.from(trait.toString()));
}
}
builder.withMember("localTraits", Node.fromNodes(nonImplicitValues));
}

List<Node> nonImplicitValues = new ArrayList<>();
for (ShapeId trait : localTraits) {
if (!trait.equals(ID)) {
nonImplicitValues.add(Node.from(trait.toString()));
}
if (isInterface) {
builder.withMember("interface", Node.from(true));
}

return Node.objectNodeBuilder()
.sourceLocation(getSourceLocation())
.withMember("localTraits", Node.fromNodes(nonImplicitValues))
.build();
return builder.build();
}

@Override
public Builder toBuilder() {
return builder()
.sourceLocation(getSourceLocation())
.localTraits(localTraits);
.localTraits(localTraits)
.isInterface(isInterface);
}

/**
Expand Down Expand Up @@ -104,6 +130,7 @@ public static Builder builder() {
*/
public static final class Builder extends AbstractTraitBuilder<MixinTrait, Builder> {
private final Set<ShapeId> localTraits = new LinkedHashSet<>();
private boolean isInterface;

@Override
public MixinTrait build() {
Expand All @@ -122,6 +149,11 @@ public Builder addLocalTrait(ShapeId trait) {
localTraits.add(trait);
return this;
}

public Builder isInterface(boolean isInterface) {
this.isInterface = isInterface;
return this;
}
}

public static final class Provider implements TraitService {
Expand All @@ -133,7 +165,9 @@ public ShapeId getShapeId() {
@Override
public MixinTrait createTrait(ShapeId target, Node value) {
Builder builder = builder().sourceLocation(value);
value.expectObjectNode().getArrayMember("localTraits", ShapeId::fromNode, builder::localTraits);
ObjectNode objectNode = value.expectObjectNode();
objectNode.getArrayMember("localTraits", ShapeId::fromNode, builder::localTraits);
objectNode.getBooleanMember("interface", builder::isInterface);
return builder.build();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,128 @@
package software.amazon.smithy.model.transform;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.AbstractShapeBuilder;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.MixinTrait;

/**
* Flattens mixins out of the model.
*
* <p>Interface mixins (those with {@code interface = true}) are preserved in the model
* and their references are maintained on shapes that use them, while non-interface
* mixins are removed as before.
*/
final class FlattenAndRemoveMixins {
Model transform(ModelTransformer transformer, Model model) {
Set<ShapeId> interfaceMixinIds = new HashSet<>();
for (Shape shape : model.toSet()) {
if (MixinTrait.isInterfaceMixin(shape)) {
interfaceMixinIds.add(shape.getId());
}
}

List<Shape> updatedShapes = new ArrayList<>();
List<ShapeId> toRemove = new ArrayList<>();
List<Shape> toRemove = new ArrayList<>();

for (Shape shape : model.toSet()) {
if (shape.hasTrait(MixinTrait.ID)) {
toRemove.add(shape.getId());
if (shape.hasTrait(MixinTrait.ID) && !interfaceMixinIds.contains(shape.getId())) {
// Non-interface mixin: remove from model
toRemove.add(shape);
} else if (!shape.getMixins().isEmpty()) {
updatedShapes.add(Shape.shapeToBuilder(shape).flattenMixins().build());
// Shape has mixins (either an interface mixin with parents, or a concrete shape).
// Flatten all mixins, then re-add references to effective interface mixins.
Set<ShapeId> effectiveInterfaceMixins = collectEffectiveInterfaceMixins(
shape,
model,
interfaceMixinIds);
if (effectiveInterfaceMixins.isEmpty() && !shape.hasTrait(MixinTrait.ID)) {
// Simple case: no interface mixins involved, just flatten everything
updatedShapes.add(Shape.shapeToBuilder(shape).flattenMixins().build());
} else if (!effectiveInterfaceMixins.isEmpty()) {
updatedShapes.add(flattenAndPreserveInterfaceRefs(
shape,
model,
effectiveInterfaceMixins));
}
}
}

if (!updatedShapes.isEmpty() || !toRemove.isEmpty()) {
Model.Builder builder = model.toBuilder();
updatedShapes.forEach(builder::addShape);
// Don't use the removeShapes transform because that would further mutate shapes and remove the things
// that were just flattened into the shapes. It's safe to just remove mixin shapes here.
toRemove.forEach(builder::removeShape);
toRemove.forEach(s -> builder.removeShape(s.getId()));
model = builder.build();
}

return model;
}

/**
* Collect the interface mixin IDs that should be preserved as references on this shape.
* Direct interface mixins are kept as-is; non-interface mixins are walked to find
* transitive interface ancestors.
*/
private Set<ShapeId> collectEffectiveInterfaceMixins(
Shape shape,
Model model,
Set<ShapeId> interfaceMixinIds
) {
Set<ShapeId> result = new LinkedHashSet<>();
for (ShapeId mixinId : shape.getMixins()) {
if (interfaceMixinIds.contains(mixinId)) {
result.add(mixinId);
} else {
collectTransitiveInterfaceMixins(model, model.expectShape(mixinId), interfaceMixinIds, result);
}
}
return result;
}

/**
* Flatten a shape's mixins and re-add references to the given interface mixins.
* Traits that will be re-inherited from the interface mixins are removed from the
* builder's introduced traits so they don't appear double-counted.
*/
private Shape flattenAndPreserveInterfaceRefs(
Shape shape,
Model model,
Set<ShapeId> interfaceMixinIds
) {
AbstractShapeBuilder<?, ?> builder = Shape.shapeToBuilder(shape);
Set<ShapeId> introducedTraitIds = shape.getIntroducedTraits().keySet();
builder.flattenMixins();

// Remove traits that will be re-inherited, then re-add the mixin refs
for (ShapeId ifaceId : interfaceMixinIds) {
Shape ifaceMixin = model.expectShape(ifaceId);
for (ShapeId traitId : MixinTrait.getNonLocalTraitsFromMap(ifaceMixin.getAllTraits()).keySet()) {
if (!introducedTraitIds.contains(traitId)) {
builder.removeTrait(traitId);
}
}
builder.addMixinRef(ifaceMixin);
}

return builder.build();
}

private void collectTransitiveInterfaceMixins(
Model model,
Shape shape,
Set<ShapeId> interfaceMixinIds,
Set<ShapeId> result
) {
for (ShapeId parentId : shape.getMixins()) {
if (interfaceMixinIds.contains(parentId)) {
result.add(parentId);
} else {
collectTransitiveInterfaceMixins(model, model.expectShape(parentId), interfaceMixinIds, result);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.model.validation.validators;

import java.util.ArrayList;
import java.util.List;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.traits.MixinTrait;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;

/**
* Validates constraints on the mixin trait's properties.
*/
public final class MixinTraitValidator extends AbstractValidator {
@Override
public List<ValidationEvent> validate(Model model) {
List<ValidationEvent> events = new ArrayList<>();
for (Shape shape : model.getShapesWithTrait(MixinTrait.class)) {
MixinTrait trait = shape.expectTrait(MixinTrait.class);
if (trait.isInterface() && shape.isUnionShape()) {
events.add(error(shape,
trait,
"The `interface` property of the `@mixin` trait cannot be used on union shapes."));
}
}
return events;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ software.amazon.smithy.model.validation.validators.IdempotencyTokenIgnoredValida
software.amazon.smithy.model.validation.validators.JsonNameValidator
software.amazon.smithy.model.validation.validators.LengthTraitValidator
software.amazon.smithy.model.validation.validators.MediaTypeValidator
software.amazon.smithy.model.validation.validators.MixinTraitValidator
software.amazon.smithy.model.validation.validators.MemberShouldReferenceResourceValidator
software.amazon.smithy.model.validation.validators.NoInlineDocumentSupportValidator
software.amazon.smithy.model.validation.validators.OperationValidator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,7 @@ structure unitType {}
@trait(selector: ":not(member)")
structure mixin {
localTraits: LocalMixinTraitList
interface: Boolean
}

@private
Expand Down
Loading
Loading