From ec18a7a8701074b69181832e83b9f0fa1af817fc Mon Sep 17 00:00:00 2001 From: Sean McGrail Date: Tue, 15 Dec 2020 16:52:37 -0800 Subject: [PATCH] Normalize map keys for shapes targeted by HeaderPrefix during deserialization. --- .../amazon/smithy/go/codegen/GoWriter.java | 66 ++++++-- .../smithy/go/codegen/OperationGenerator.java | 16 +- .../go/codegen/ShapeValueGenerator.java | 144 ++++++++++++++++-- .../smithy/go/codegen/UnionGenerator.java | 10 +- .../HttpBindingProtocolGenerator.java | 2 +- .../HttpProtocolTestGenerator.java | 3 + .../HttpProtocolUnitTestGenerator.java | 10 +- .../go/codegen/knowledge/GoUsageIndex.java | 90 +++++++++++ 8 files changed, 310 insertions(+), 31 deletions(-) create mode 100644 codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/knowledge/GoUsageIndex.java diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java index e2f9c372d..1ebd064a3 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.StringJoiner; import java.util.function.BiFunction; import java.util.logging.Logger; @@ -28,10 +29,13 @@ import software.amazon.smithy.codegen.core.SymbolDependency; import software.amazon.smithy.codegen.core.SymbolDependencyContainer; import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.go.codegen.knowledge.GoUsageIndex; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait; import software.amazon.smithy.model.traits.MediaTypeTrait; import software.amazon.smithy.model.traits.RequiredTrait; import software.amazon.smithy.model.traits.StringTrait; @@ -290,24 +294,60 @@ boolean writePackageShapeDocs(Shape shape) { * @return Returns true if docs were written. */ boolean writeMemberDocs(Model model, MemberShape member) { - return member.getMemberTrait(model, DocumentationTrait.class) + boolean hasDocs; + + hasDocs = member.getMemberTrait(model, DocumentationTrait.class) .map(DocumentationTrait::getValue) .map(docs -> { writeDocs(docs); - member.getMemberTrait(model, MediaTypeTrait.class) - .map(StringTrait::getValue) - .ifPresent(mediaType -> writeDocs( - "\n\nThis value conforms to the media type: " + mediaType)); - - member.getMemberTrait(model, RequiredTrait.class) - .ifPresent((value) -> { - if (docs.length() != 0) { - writeDocs(""); - } - writeDocs("This member is required."); - }); return true; }).orElse(false); + + Optional stringOptional = member.getMemberTrait(model, MediaTypeTrait.class) + .map(StringTrait::getValue); + if (stringOptional.isPresent()) { + if (hasDocs) { + writeDocs(""); + } + writeDocs("This value conforms to the media type: " + stringOptional.get()); + hasDocs = true; + } + + GoUsageIndex usageIndex = GoUsageIndex.of(model); + if (usageIndex.isUsedForOutput(member)) { + if (member.getMemberTrait(model, + HttpPrefixHeadersTrait.class).isPresent()) { + if (hasDocs) { + writeDocs(""); + } + writeDocs("Map keys will be normalized to lower-case."); + hasDocs = true; + } + } + + if (member.getMemberTrait(model, RequiredTrait.class).isPresent()) { + if (hasDocs) { + writeDocs(""); + } + writeDocs("This member is required."); + hasDocs = true; + } + + Optional deprecatedTrait = member.getMemberTrait(model, DeprecatedTrait.class); + if (deprecatedTrait.isPresent()) { + if (hasDocs) { + writeDocs(""); + } + final String defaultMessage = "This member has been deprecated."; + writeDocs("Deprecated: " + deprecatedTrait.get().getMessage().map(s -> { + if (s.length() == 0) { + return defaultMessage; + } + return s; + }).orElse(defaultMessage)); + } + + return hasDocs; } @Override diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java index 480ba670c..fb71a4ff3 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java @@ -29,6 +29,7 @@ import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.DeprecatedTrait; /** * Generates a client operation and associated custom shapes. @@ -91,7 +92,20 @@ public void run() { Symbol outputSymbol = symbolProvider.toSymbol(outputShape); // Generate operation method - writer.writeShapeDocs(operation); + final boolean hasDocs = writer.writeShapeDocs(operation); + operation.getTrait(DeprecatedTrait.class) + .ifPresent(trait -> { + if (hasDocs) { + writer.writeDocs(""); + } + final String defaultMessage = "This operation has been deprecated."; + writer.writeDocs("Deprecated: " + trait.getMessage().map(s -> { + if (s.length() == 0) { + return defaultMessage; + } + return s; + }).orElse(defaultMessage)); + }); Symbol contextSymbol = SymbolUtils.createValueSymbolBuilder("Context", SmithyGoDependency.CONTEXT).build(); writer.openBlock("func (c $P) $T(ctx $T, params $P, optFns ...func(*Options)) ($P, error) {", "}", serviceSymbol, operationSymbol, contextSymbol, inputSymbol, outputSymbol, () -> { diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ShapeValueGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ShapeValueGenerator.java index bf7ebe4ec..ae59e977f 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ShapeValueGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ShapeValueGenerator.java @@ -17,7 +17,9 @@ package software.amazon.smithy.go.codegen; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.logging.Logger; import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.codegen.core.Symbol; @@ -32,6 +34,7 @@ import software.amazon.smithy.model.node.NumberNode; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeType; @@ -39,7 +42,12 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait; import software.amazon.smithy.model.traits.StreamingTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.OptionalUtils; +import software.amazon.smithy.utils.SmithyBuilder; /** * Generates a shape type declaration based on the parameters provided. @@ -47,9 +55,10 @@ public final class ShapeValueGenerator { private static final Logger LOGGER = Logger.getLogger(ShapeValueGenerator.class.getName()); - protected final Model model; - protected final SymbolProvider symbolProvider; - protected final GoPointableIndex pointableIndex; + private final Model model; + private final SymbolProvider symbolProvider; + private final GoPointableIndex pointableIndex; + private final Config config; /** * Initializes a shape value generator. @@ -58,9 +67,21 @@ public final class ShapeValueGenerator { * @param symbolProvider the symbol provider. */ public ShapeValueGenerator(Model model, SymbolProvider symbolProvider) { + this(model, symbolProvider, Config.builder().build()); + } + + /** + * Initializes a shape value generator. + * + * @param model the Smithy model references. + * @param symbolProvider the symbol provider. + * @param config the shape value generator config. + */ + public ShapeValueGenerator(Model model, SymbolProvider symbolProvider, Config config) { this.model = model; this.symbolProvider = symbolProvider; this.pointableIndex = GoPointableIndex.of(model); + this.config = config; } /** @@ -79,7 +100,8 @@ public void writePointableStructureShapeValueInline(GoWriter writer, StructureSh // not within the context of a member shape reference. Symbol symbol = symbolProvider.toSymbol(shape); writer.write("&$T{", symbol); - params.accept(new ShapeValueNodeVisitor(writer, this, shape)); + params.accept(new ShapeValueNodeVisitor(writer, this, shape, ListUtils.copyOf(shape.getAllTraits().values()), + config)); writer.writeInline("}"); } @@ -149,7 +171,8 @@ protected void structDeclShapeValue(GoWriter writer, MemberShape member, Node pa String addr = CodegenUtils.asAddressIfAddressable(model, pointableIndex, member, ""); writer.write("$L$T{", addr, symbol); - params.accept(new ShapeValueNodeVisitor(writer, this, model.expectShape(member.getTarget()))); + params.accept(new ShapeValueNodeVisitor(writer, this, model.expectShape(member.getTarget()), + ListUtils.copyOf(member.getAllTraits().values()), config)); writer.writeInline("}"); } @@ -197,7 +220,8 @@ protected void unionDeclShapeValue(GoWriter writer, MemberShape member, ObjectNo */ protected void listDeclShapeValue(GoWriter writer, MemberShape member, Node params) { writer.write("$P{", symbolProvider.toSymbol(member)); - params.accept(new ShapeValueNodeVisitor(writer, this, model.expectShape(member.getTarget()))); + params.accept(new ShapeValueNodeVisitor(writer, this, model.expectShape(member.getTarget()), + ListUtils.copyOf(member.getAllTraits().values()), config)); writer.writeInline("}"); } @@ -210,7 +234,8 @@ protected void listDeclShapeValue(GoWriter writer, MemberShape member, Node para */ protected void mapDeclShapeValue(GoWriter writer, MemberShape member, Node params) { writer.write("$P{", symbolProvider.toSymbol(member)); - params.accept(new ShapeValueNodeVisitor(writer, this, model.expectShape(member.getTarget()))); + params.accept(new ShapeValueNodeVisitor(writer, this, model.expectShape(member.getTarget()), + ListUtils.copyOf(member.getAllTraits().values()), config)); writer.writeInline("}"); } @@ -327,17 +352,58 @@ protected void writeScalarValueInline(GoWriter writer, MemberShape member, Node break; } - params.accept(new ShapeValueNodeVisitor(writer, this, target)); + params.accept(new ShapeValueNodeVisitor(writer, this, target, + ListUtils.copyOf(member.getAllTraits().values()), config)); writer.writeInline(closing); } + /** + * Configuration that determines how shapes values are generated. + */ + public static final class Config { + private final boolean normalizeHttpPrefixHeaderKeys; + + private Config(Builder builder) { + normalizeHttpPrefixHeaderKeys = builder.normalizeHttpPrefixHeaderKeys; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Returns whether maps with the httpPrefixHeader trait should have their keys normalized. + * + * @return whether to normalize http prefix header keys + */ + public boolean isNormalizeHttpPrefixHeaderKeys() { + return normalizeHttpPrefixHeaderKeys; + } + + public static final class Builder implements SmithyBuilder { + private boolean normalizeHttpPrefixHeaderKeys; + + public Builder normalizeHttpPrefixHeaderKeys(boolean normalizeHttpPrefixHeaderKeys) { + this.normalizeHttpPrefixHeaderKeys = normalizeHttpPrefixHeaderKeys; + return this; + } + + @Override + public Config build() { + return new Config(this); + } + } + } + /** * NodeVisitor to walk shape value declarations with node values. */ private final class ShapeValueNodeVisitor implements NodeVisitor { - GoWriter writer; - ShapeValueGenerator valueGen; - Shape currentShape; + private final GoWriter writer; + private final ShapeValueGenerator valueGen; + private final Shape currentShape; + private final List traits; + private final Config config; /** * Initializes shape value visitor. @@ -347,9 +413,42 @@ private final class ShapeValueNodeVisitor implements NodeVisitor { * @param shape the shape that visiting is relative to. */ private ShapeValueNodeVisitor(GoWriter writer, ShapeValueGenerator valueGen, Shape shape) { + this(writer, valueGen, shape, ListUtils.of()); + } + + /** + * Initializes shape value visitor. + * + * @param writer writer to write generated code with. + * @param valueGen shape value generator. + * @param shape the shape that visiting is relative to. + * @param traits the traits applied to the target shape by a MemberShape. + */ + private ShapeValueNodeVisitor(GoWriter writer, ShapeValueGenerator valueGen, Shape shape, List traits) { + this(writer, valueGen, shape, traits, Config.builder().build()); + } + + /** + * Initializes shape value visitor. + * + * @param writer writer to write generated code with. + * @param valueGen shape value generator. + * @param shape the shape that visiting is relative to. + * @param traits the traits applied to the target shape by a MemberShape. + * @param config the shape value generator config. + */ + private ShapeValueNodeVisitor( + GoWriter writer, + ShapeValueGenerator valueGen, + Shape shape, + List traits, + Config config + ) { this.writer = writer; this.valueGen = valueGen; this.currentShape = shape; + this.traits = traits; + this.config = config; } /** @@ -395,10 +494,18 @@ public Void objectNode(ObjectNode node) { break; case MAP: - member = this.currentShape.asMapShape().get().getValue(); + MapShape mapShape = this.currentShape.asMapShape().get(); + + String keyValue = keyNode.getValue(); + if (config.isNormalizeHttpPrefixHeaderKeys()) { + keyValue = OptionalUtils.or(getTrait(HttpPrefixHeadersTrait.class), + () -> mapShape.getTrait(HttpPrefixHeadersTrait.class)) + .map(httpPrefixHeadersTrait -> keyNode.getValue().toLowerCase()) + .orElse(keyValue); + } - writer.write("$S: ", keyNode.getValue()); - valueGen.writeMemberValueInline(writer, member, valueNode); + writer.write("$S: ", keyValue); + valueGen.writeMemberValueInline(writer, mapShape.getValue(), valueNode); writer.write(","); break; @@ -523,6 +630,15 @@ private void writeInlineBigIntegerInit(GoWriter writer, Object value) { + "}()", value, value); } + + private Optional getTrait(Class traitClass) { + for (Trait trait : traits) { + if (traitClass.isInstance(trait)) { + return Optional.of((T) trait); + } + } + return Optional.empty(); + } } } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/UnionGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/UnionGenerator.java index c69dea64f..fbc13118a 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/UnionGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/UnionGenerator.java @@ -16,6 +16,7 @@ package software.amazon.smithy.go.codegen; import java.util.Collection; +import java.util.HashSet; import java.util.Set; import java.util.TreeSet; import software.amazon.smithy.codegen.core.Symbol; @@ -98,6 +99,8 @@ public void generateUnionExamples(GoWriter writer) { Symbol symbol = symbolProvider.toSymbol(shape); Set members = new TreeSet<>(shape.getAllMembers().values()); + Set referenced = new HashSet<>(); + writer.openBlock("func Example$L_outputUsage() {", "}", symbol.getName(), () -> { writer.write("var union $P", symbol); @@ -105,11 +108,12 @@ public void generateUnionExamples(GoWriter writer) { writer.openBlock("switch v := union.(type) {", "}", () -> { for (MemberShape member : members) { Symbol targetSymbol = symbolProvider.toSymbol(model.expectShape(member.getTarget())); + referenced.add(targetSymbol); Symbol memberSymbol = SymbolUtils.createValueSymbolBuilder(symbolProvider.toMemberName(member), symbol.getNamespace()).build(); writer.openBlock("case *$T:", "", memberSymbol, () -> { - writer.write("_ = v.Value // Value is $L", targetSymbol.getName()); + writer.write("_ = v.Value // Value is $T", targetSymbol); }); } writer.addUseImports(SmithyGoDependency.FMT); @@ -122,6 +126,10 @@ public void generateUnionExamples(GoWriter writer) { writer.write("fmt.Println(\"union is nil or unknown type\")"); }); }); + }).write(""); + + referenced.forEach(s -> { + writer.write("var _ $P", s); }); } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpBindingProtocolGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpBindingProtocolGenerator.java index 8a3e3f906..436b697e3 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpBindingProtocolGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpBindingProtocolGenerator.java @@ -1086,7 +1086,7 @@ private void writePrefixHeaderDeserializerFunction( String value = generateHttpHeaderValue(context, writer, valueMemberShape, binding, operand); - writer.write("v.$L[headerKey[lenPrefix:]] = $L", memberName, + writer.write("v.$L[strings.ToLower(headerKey[lenPrefix:])] = $L", memberName, CodegenUtils.getAsPointerIfPointable(context.getModel(), writer, GoPointableIndex.of(context.getModel()), valueMemberShape, value)); }); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpProtocolTestGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpProtocolTestGenerator.java index 6df442256..6250210a6 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpProtocolTestGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpProtocolTestGenerator.java @@ -26,6 +26,7 @@ import software.amazon.smithy.go.codegen.GoDelegator; import software.amazon.smithy.go.codegen.GoSettings; import software.amazon.smithy.go.codegen.GoWriter; +import software.amazon.smithy.go.codegen.ShapeValueGenerator; import software.amazon.smithy.go.codegen.integration.ProtocolGenerator.GenerationContext; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.OperationIndex; @@ -135,6 +136,8 @@ public void generateProtocolTests() { .service(service) .operation(operation) .testCases(trait.getTestCases()) + .shapeValueGeneratorConfig(ShapeValueGenerator.Config.builder() + .normalizeHttpPrefixHeaderKeys(true).build()) .build() .generateTestFunction(writer); }); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpProtocolUnitTestGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpProtocolUnitTestGenerator.java index b8d40d7fa..a6e924190 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpProtocolUnitTestGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpProtocolUnitTestGenerator.java @@ -63,6 +63,7 @@ public abstract class HttpProtocolUnitTestGenerator clientConfigValues = new TreeSet<>(); protected final Set skipTests = new TreeSet<>(); + protected final ShapeValueGenerator.Config shapeValueGeneratorConfig; /** * Initializes the abstract protocol tests generator. @@ -78,6 +79,7 @@ protected HttpProtocolUnitTestGenerator(Builder builder) { this.testCases = SmithyBuilder.requiredState("testCases", builder.testCases); this.clientConfigValues.addAll(builder.clientConfigValues); this.skipTests.addAll(builder.skipTests); + this.shapeValueGeneratorConfig = SmithyBuilder.requiredState("config", builder.shapeValueGeneratorConfig); opSymbol = symbolProvider.toSymbol(operation); @@ -548,7 +550,7 @@ protected void writeAssertForbidHeader(GoWriter writer, String expect, String ac * @param params values to initialize shape type with. */ protected void writeShapeValueInline(GoWriter writer, StructureShape shape, ObjectNode params) { - new ShapeValueGenerator(model, symbolProvider) + new ShapeValueGenerator(model, symbolProvider, shapeValueGeneratorConfig) .writePointableStructureShapeValueInline(writer, shape, params); } @@ -575,6 +577,7 @@ public abstract static class Builder { protected List testCases = new ArrayList<>(); protected Set clientConfigValues = new TreeSet<>(); protected Set skipTests = new TreeSet<>(); + protected ShapeValueGenerator.Config shapeValueGeneratorConfig = ShapeValueGenerator.Config.builder().build(); public Builder model(Model model) { this.model = model; @@ -641,6 +644,11 @@ public Builder addSkipTests(Set skipTests) { return this; } + public Builder shapeValueGeneratorConfig(ShapeValueGenerator.Config config) { + this.shapeValueGeneratorConfig = config; + return this; + } + abstract HttpProtocolUnitTestGenerator build(); } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/knowledge/GoUsageIndex.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/knowledge/GoUsageIndex.java new file mode 100644 index 000000000..ba3c534ca --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/knowledge/GoUsageIndex.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020 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.go.codegen.knowledge; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.KnowledgeIndex; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.neighbor.RelationshipDirection; +import software.amazon.smithy.model.neighbor.Walker; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.ToShapeId; + +/** + * Provides {@link KnowledgeIndex} of how shapes are used in the model. + */ +public class GoUsageIndex implements KnowledgeIndex { + private final Model model; + private final Walker walker; + + private final Set inputShapes = new HashSet<>(); + private final Set outputShapes = new HashSet<>(); + + public GoUsageIndex(Model model) { + this.model = model; + this.walker = new Walker(model); + + TopDownIndex topDownIndex = TopDownIndex.of(model); + OperationIndex operationIndex = OperationIndex.of(model); + + model.shapes(ServiceShape.class).forEach(serviceShape -> { + topDownIndex.getContainedOperations(serviceShape).forEach(operationShape -> { + StructureShape inputShape = operationIndex.getInput(operationShape).get(); + StructureShape outputShape = operationIndex.getOutput(operationShape).get(); + + inputShapes.addAll(walker.walkShapes(inputShape, relationship -> + relationship.getDirection() == RelationshipDirection.DIRECTED).stream() + .map(Shape::toShapeId).collect(Collectors.toList())); + + outputShapes.addAll(walker.walkShapes(outputShape, relationship -> + relationship.getDirection() == RelationshipDirection.DIRECTED).stream() + .map(Shape::toShapeId).collect(Collectors.toList())); + + }); + }); + } + + /** + * Returns whether shape is used as part of an input to an operation. + * + * @param shape the shape + * @return whether the shape is used as input. + */ + public boolean isUsedForInput(ToShapeId shape) { + return inputShapes.contains(shape.toShapeId()); + } + + /** + * Returns whether shape is used as output of an operation. + * + * @param shape the shape + * @return whether the shape is used as input. + */ + public boolean isUsedForOutput(ToShapeId shape) { + return outputShapes.contains(shape.toShapeId()); + } + + public static GoUsageIndex of(Model model) { + return model.getKnowledge(GoUsageIndex.class, GoUsageIndex::new); + } +}