Skip to content

Commit

Permalink
Compute introduced properties after the fact
Browse files Browse the repository at this point in the history
  • Loading branch information
JordonPhillips committed Jan 6, 2022
1 parent bdcfe21 commit 11cb675
Show file tree
Hide file tree
Showing 9 changed files with 491 additions and 124 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.smithy.model.knowledge;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ListShape;
import software.amazon.smithy.model.shapes.MapShape;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.SetShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.ShapeVisitor;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.shapes.UnionShape;

/**
* Index of shapes stripped down to only their introduced properties.
*
* <p>This is primarily useful in serialization to determine what
* properties to actually serialize vs what is computed as part of
* mixins.
*/
public final class PreMixinIndex implements KnowledgeIndex {

private final Map<ShapeId, Shape> preMixinShapes = new HashMap<>();

public PreMixinIndex(Model model) {
MixinUnroller unroller = new MixinUnroller(model);
model.shapes()
.filter(shape -> !shape.isMemberShape())
.forEach(shape -> {
if (!shape.getMixins().isEmpty()) {
preMixinShapes.put(shape.getId(), shape.accept(unroller));
}
});
}

public static PreMixinIndex of(Model model) {
return model.getKnowledge(PreMixinIndex.class, PreMixinIndex::new);
}

/**
* Gets a version of the shape that has mixin properties stripped out.
*
* <p>NOTE: mixin members with introduced traits WILL be present in
* their entirety. The only way to determine if those members originally
* were defined in a mixin is to have an original copy of the shape to
* compare against. Any members of the shape that themselves have mixins
* are inherited.
*
* @param shape The shape to strip mixin data from.
* @return A version of the shape without mixin data.
*/
public Shape getPreMixinShape(Shape shape) {
return preMixinShapes.getOrDefault(shape.getId(), shape);
}

private static final class MixinUnroller extends ShapeVisitor.Default<Shape> {

private final Model model;

private MixinUnroller(Model model) {
this.model = model;
}

@Override
protected Shape getDefault(Shape shape) {
return Shape.shapeToBuilder(shape)
.clearMixins()
.traits(shape.getIntroducedTraits().values())
.build();
}

@Override
public Shape listShape(ListShape shape) {
return shape.toBuilder()
.clearMixins()
.traits(shape.getIntroducedTraits().values())
.member((MemberShape) shape.getMember().accept(this))
.build();
}

@Override
public Shape setShape(SetShape shape) {
return shape.toBuilder()
.clearMixins()
.traits(shape.getIntroducedTraits().values())
.member((MemberShape) shape.getMember().accept(this))
.build();
}

@Override
public Shape mapShape(MapShape shape) {
return shape.toBuilder()
.clearMixins()
.traits(shape.getIntroducedTraits().values())
.key((MemberShape) shape.getKey().accept(this))
.value((MemberShape) shape.getValue().accept(this))
.build();
}

@Override
public Shape structureShape(StructureShape shape) {
StructureShape.Builder builder = shape.toBuilder()
.clearMixins()
.traits(shape.getIntroducedTraits().values());
unrollMembers(shape, builder::addMember);
return builder.build();
}

@Override
public Shape unionShape(UnionShape shape) {
UnionShape.Builder builder = shape.toBuilder()
.clearMixins()
.traits(shape.getIntroducedTraits().values());
unrollMembers(shape, builder::addMember);
return builder.build();
}

private void unrollMembers(Shape shape, Consumer<MemberShape> consumer) {
for (MemberShape member : shape.members()) {
if (member.getMixins().isEmpty()) {
consumer.accept(member);
} else {
consumer.accept((MemberShape) member.accept(this));
}
}
}

@Override
public Shape operationShape(OperationShape shape) {
OperationShape.Builder builder = shape.toBuilder()
.clearMixins()
.traits(shape.getIntroducedTraits().values())
.clearErrors();

Set<ShapeId> previousErrors = new HashSet<>();
for (ShapeId mixinId : shape.getMixins()) {
previousErrors.addAll(model.expectShape(mixinId, OperationShape.class).getErrors());
}

addIntroduced(shape.getErrors(), previousErrors, builder::addError);

return builder.build();
}

@Override
public Shape serviceShape(ServiceShape shape) {
ServiceShape.Builder builder = shape.toBuilder()
.clearMixins()
.clearOperations()
.clearResources()
.clearErrors()
.clearRename()
.traits(shape.getIntroducedTraits().values());

String previousVersion = "";
Set<ShapeId> previousOperations = new HashSet<>();
Set<ShapeId> previousResources = new HashSet<>();
Set<ShapeId> previousErrors = new HashSet<>();
Map<ShapeId, String> previousRename = new HashMap<>();

for (ShapeId mixinId : shape.getMixins()) {
ServiceShape mixin = model.expectShape(mixinId, ServiceShape.class);
previousVersion = mixin.getVersion();
previousOperations.addAll(mixin.getOperations());
previousResources.addAll(mixin.getResources());
previousErrors.addAll(mixin.getErrors());
previousRename.putAll(mixin.getRename());
}

if (shape.getVersion().equals(previousVersion)) {
builder.version("");
}

addIntroduced(shape.getOperations(), previousOperations, builder::addOperation);
addIntroduced(shape.getResources(), previousResources, builder::addResource);
addIntroduced(shape.getErrors(), previousErrors, builder::addError);

for (Map.Entry<ShapeId, String> entry : shape.getRename().entrySet()) {
if (!entry.getValue().equals(previousRename.get(entry.getKey()))) {
builder.putRename(entry.getKey(), entry.getValue());
}
}

return builder.build();
}

private void addIntroduced(
Collection<ShapeId> current,
Collection<ShapeId> previous,
Consumer<ShapeId> consumer
) {
for (ShapeId shapeId : current) {
if (!previous.contains(shapeId)) {
consumer.accept(shapeId);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,14 @@
public abstract class EntityShape extends Shape {

private final Set<ShapeId> resources;
private final Set<ShapeId> introducedResources;
private final Set<ShapeId> operations;
private final Set<ShapeId> introducedOperations;

EntityShape(Builder<?, ?> builder) {
super(builder, false);

if (getMixins().isEmpty()) {
resources = builder.resources.copy();
introducedResources = resources;
operations = builder.operations.copy();
introducedOperations = operations;
} else {
Set<ShapeId> computedResources = new TreeSet<>();
Set<ShapeId> computedOperations = new TreeSet<>();
Expand All @@ -50,11 +46,8 @@ public abstract class EntityShape extends Shape {
computedOperations.addAll(mixin.getOperations());
}

introducedResources = builder.resources.copy();
introducedOperations = builder.operations.copy();

computedResources.addAll(introducedResources);
computedOperations.addAll(introducedOperations);
computedResources.addAll(builder.resources.peek());
computedOperations.addAll(builder.operations.peek());

resources = Collections.unmodifiableSet(computedResources);
operations = Collections.unmodifiableSet(computedOperations);
Expand All @@ -68,16 +61,6 @@ public final Set<ShapeId> getResources() {
return resources;
}

/**
* Gets all the directly-bound resources introduced by this shape and
* not inherited from mixins.
*
* @return Gets the introduced resources directly-bound to the shape.
*/
public final Set<ShapeId> getIntroducedResources() {
return introducedResources;
}

/**
* Gets operations bound only through the "operations" property.
*
Expand All @@ -93,20 +76,6 @@ public final Set<ShapeId> getOperations() {
return operations;
}

/**
* Gets operations bound through the "operations" property that
* were not inherited from mixins.
*
* <p>This will not include operations bound to resources using
* a lifecycle operation binding. This will also not include
* operations bound to this entity through sub-resources.
*
* @return Gets the introduced operations.
*/
public final Set<ShapeId> getIntroducedOperations() {
return introducedOperations;
}

/**
* Get all operations directly bound to this shape.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.stream.Stream;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.knowledge.PreMixinIndex;
import software.amazon.smithy.model.loader.Prelude;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.node.Node;
Expand Down Expand Up @@ -66,7 +67,7 @@ private ModelSerializer(Builder builder) {
}

public ObjectNode serialize(Model model) {
ShapeSerializer shapeSerializer = new ShapeSerializer();
ShapeSerializer shapeSerializer = new ShapeSerializer(model);

ObjectNode.Builder builder = Node.objectNodeBuilder()
.withMember("smithy", Node.from(Model.MODEL_VERSION))
Expand Down Expand Up @@ -204,6 +205,11 @@ private ObjectNode.Builder serializeTraits(ObjectNode.Builder builder, Collectio
private final class ShapeSerializer extends ShapeVisitor.Default<Node> {

private final Set<MemberShape> mixinMemberTraits = new TreeSet<>();
private final PreMixinIndex preMixinIndex;

private ShapeSerializer(Model model) {
preMixinIndex = PreMixinIndex.of(model);
}

private ObjectNode.Builder createTypedBuilder(Shape shape) {
ObjectNode.Builder builder = Node.objectNodeBuilder()
Expand Down Expand Up @@ -263,10 +269,11 @@ public Node mapShape(MapShape shape) {

@Override
public Node operationShape(OperationShape shape) {
OperationShape preMixinShape = preMixinIndex.getPreMixinShape(shape).asOperationShape().get();
return serializeAllTraits(shape, createTypedBuilder(shape)
.withMember("input", serializeReference(shape.getInputShape()))
.withMember("output", serializeReference(shape.getOutputShape()))
.withOptionalMember("errors", createOptionalIdList(shape.getIntroducedErrors())))
.withOptionalMember("errors", createOptionalIdList(preMixinShape.getErrors())))
.build();
}

Expand All @@ -288,27 +295,29 @@ public Node resourceShape(ResourceShape shape) {
.withOptionalMember("update", shape.getUpdate().map(this::serializeReference))
.withOptionalMember("delete", shape.getDelete().map(this::serializeReference))
.withOptionalMember("list", shape.getList().map(this::serializeReference))
.withOptionalMember("operations", createOptionalIdList(shape.getIntroducedOperations()))
.withOptionalMember("operations", createOptionalIdList(shape.getOperations()))
.withOptionalMember("collectionOperations", createOptionalIdList(shape.getCollectionOperations()))
.withOptionalMember("resources", createOptionalIdList(shape.getIntroducedResources())))
.withOptionalMember("resources", createOptionalIdList(shape.getResources())))
.build();
}

@Override
public Node serviceShape(ServiceShape shape) {
ObjectNode.Builder serviceBuilder = createTypedBuilder(shape);

if (!StringUtils.isBlank(shape.getIntroducedVersion())) {
serviceBuilder.withMember("version", Node.from(shape.getIntroducedVersion()));
ServiceShape preMixinShape = preMixinIndex.getPreMixinShape(shape).asServiceShape().get();

if (!StringUtils.isBlank(preMixinShape.getVersion())) {
serviceBuilder.withMember("version", Node.from(preMixinShape.getVersion()));
}

serviceBuilder.withOptionalMember("operations", createOptionalIdList(shape.getIntroducedOperations()));
serviceBuilder.withOptionalMember("resources", createOptionalIdList(shape.getIntroducedResources()));
serviceBuilder.withOptionalMember("errors", createOptionalIdList(shape.getIntroducedErrors()));
serviceBuilder.withOptionalMember("operations", createOptionalIdList(preMixinShape.getOperations()));
serviceBuilder.withOptionalMember("resources", createOptionalIdList(preMixinShape.getResources()));
serviceBuilder.withOptionalMember("errors", createOptionalIdList(preMixinShape.getErrors()));

if (!shape.getIntroducedRename().isEmpty()) {
if (!preMixinShape.getRename().isEmpty()) {
ObjectNode.Builder renameBuilder = Node.objectNodeBuilder();
for (Map.Entry<ShapeId, String> entry : shape.getIntroducedRename().entrySet()) {
for (Map.Entry<ShapeId, String> entry : preMixinShape.getRename().entrySet()) {
renameBuilder.withMember(entry.getKey().toString(), entry.getValue());
}
serviceBuilder.withMember("rename", renameBuilder.build());
Expand Down
Loading

0 comments on commit 11cb675

Please sign in to comment.