diff --git a/smithy-aws-protocol-tests/model/awsQuery/xml-lists.smithy b/smithy-aws-protocol-tests/model/awsQuery/xml-lists.smithy index be3c8981869..5d61ded7e58 100644 --- a/smithy-aws-protocol-tests/model/awsQuery/xml-lists.smithy +++ b/smithy-aws-protocol-tests/model/awsQuery/xml-lists.smithy @@ -185,8 +185,10 @@ structure XmlListsOutput { @xmlName("renamed") renamedListMembers: RenamedListMembers, + @suppress(["XmlFlattenedTrait"]) @xmlFlattened // The xmlname on the targeted list is ignored, and the member name is used. + // The validation that flags this is suppressed so we can test this behavior. flattenedList: RenamedListMembers, @xmlName("customName") diff --git a/smithy-aws-protocol-tests/model/ec2Query/xml-lists.smithy b/smithy-aws-protocol-tests/model/ec2Query/xml-lists.smithy index cd8f59f04ee..c68123881d6 100644 --- a/smithy-aws-protocol-tests/model/ec2Query/xml-lists.smithy +++ b/smithy-aws-protocol-tests/model/ec2Query/xml-lists.smithy @@ -183,8 +183,10 @@ structure XmlListsOutput { @xmlName("renamed") renamedListMembers: RenamedListMembers, + @suppress(["XmlFlattenedTrait"]) @xmlFlattened // The xmlname on the targeted list is ignored, and the member name is used. + // The validation that flags this is suppressed so we can test this behavior. flattenedList: RenamedListMembers, @xmlName("customName") diff --git a/smithy-aws-protocol-tests/model/restXml/document-lists.smithy b/smithy-aws-protocol-tests/model/restXml/document-lists.smithy index b2dc28b992d..c5ed12f0b96 100644 --- a/smithy-aws-protocol-tests/model/restXml/document-lists.smithy +++ b/smithy-aws-protocol-tests/model/restXml/document-lists.smithy @@ -352,8 +352,10 @@ structure XmlListsInputOutput { @xmlName("renamed") renamedListMembers: RenamedListMembers, + @suppress(["XmlFlattenedTrait"]) @xmlFlattened // The xmlname on the targeted list is ignored, and the member name is used. + // The validation that flags this is suppressed so we can test this behavior. flattenedList: RenamedListMembers, @xmlName("customName") @@ -376,7 +378,10 @@ structure XmlListsInputOutput { @xmlName("myStructureList") structureList: StructureList, + @suppress(["XmlFlattenedTrait"]) @xmlFlattened + // The xmlname on the targeted list is ignored, and the member name is used. + // The validation that flags this is suppressed so we can test this behavior. flattenedStructureList: StructureList } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/XmlFlattenedTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/XmlFlattenedTraitValidator.java new file mode 100644 index 00000000000..6a5dc14e85e --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/XmlFlattenedTraitValidator.java @@ -0,0 +1,54 @@ +/* + * 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.ListShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.XmlFlattenedTrait; +import software.amazon.smithy.model.traits.XmlNameTrait; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; + +/** + * Validates that xmlFlattened members aren't unintentionally ignoring the + * xmlName of their targets. + */ +public final class XmlFlattenedTraitValidator extends AbstractValidator { + @Override + public List validate(Model model) { + List events = new ArrayList<>(); + for (MemberShape member : model.getMemberShapesWithTrait(XmlFlattenedTrait.class)) { + // Don't emit the event if they're being explicit about the xmlName on this member + if (member.hasTrait(XmlNameTrait.class)) { + continue; + } + + Shape target = model.expectShape(member.getTarget()); + if (target instanceof ListShape) { + ListShape targetList = (ListShape) target; + validateMemberTargetingList(member, targetList, events); + } + } + return events; + } + + private void validateMemberTargetingList(MemberShape member, ListShape targetList, List events) { + if (targetList.getMember().hasTrait(XmlNameTrait.class)) { + XmlNameTrait xmlName = targetList.getMember().expectTrait(XmlNameTrait.class); + if (!member.getMemberName().equals(xmlName.getValue())) { + events.add(warning(member, String.format( + "Member is `@xmlFlattened`, so `@xmlName` of target's member (`%s`) has no effect." + + " The flattened list elements will have the name of this member - `%s`. If this" + + " is unintended, you can add `@xmlName(\"%s\")` to this member.", + targetList.getMember().getId(), member.getMemberName(), xmlName.getValue()))); + } + } + } +} diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator index a43408752c1..4b76f019461 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -59,3 +59,4 @@ software.amazon.smithy.model.validation.validators.UnitTypeValidator software.amazon.smithy.model.validation.validators.UnstableTraitValidator software.amazon.smithy.model.validation.validators.XmlNamespaceTraitValidator software.amazon.smithy.model.validation.validators.TraitValidatorsValidator +software.amazon.smithy.model.validation.validators.XmlFlattenedTraitValidator diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/xml-flattened-and-name.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/xml-flattened-and-name.errors new file mode 100644 index 00000000000..a8b044edf7c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/xml-flattened-and-name.errors @@ -0,0 +1 @@ +[WARNING] smithy.example#Struct$flattenedNoXmlName: Member is `@xmlFlattened`, so `@xmlName` of target's member (`smithy.example#ListWithXmlName$member`) has no effect. The flattened list elements will have the name of this member - `flattenedNoXmlName`. If this is unintended, you can add `@xmlName("customMember")` to this member. | XmlFlattenedTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/xml-flattened-and-name.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/xml-flattened-and-name.smithy new file mode 100644 index 00000000000..ae2080f5627 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/xml-flattened-and-name.smithy @@ -0,0 +1,43 @@ +$version: "2" + +namespace smithy.example + +structure Struct { + // No event because not flattened + notFlattened: ListWithXmlName + + // Event because flattened and non-matching member name + @xmlFlattened + flattenedNoXmlName: ListWithXmlName + + // No event because the member name matches the xml name + @xmlFlattened + customMember: ListWithXmlName + + // No event because you're being explicit about the name to use + @xmlFlattened + @xmlName("customMember") + flattenedMatchingXmlName: ListWithXmlName + + // No event because you're being explicit about the name to use + @xmlFlattened + @xmlName("Bar") + flattenedNonMatchingXmlName: ListWithXmlName + + // Validator doesn't apply to maps + @xmlFlattened + flattenedMap: MapWithXmlName +} + +list ListWithXmlName { + @xmlName("customMember") + member: String +} + +map MapWithXmlName { + @xmlName("customKey") + key: String + + @xmlName("customValue") + value: String +}