All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.model.shapes.Shape Maven / Gradle / Ivy

/*
 * 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.shapes;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceException;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.node.ExpectationNotMetException;
import software.amazon.smithy.model.traits.MixinTrait;
import software.amazon.smithy.model.traits.TagsTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.MapUtils;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.Tagged;

/**
 * A {@code Shape} defines a model component.
 *
 * 

A {@code Shape} may have an arbitrary number of typed traits * attached to it, allowing additional information to be associated * with the shape. * *

Shape does implement {@link Comparable}, but comparisons are based * solely on the ShapeId of the shape. This assumes that shapes are being * compared in the context of a Model that forbids shape ID conflicts. */ public abstract class Shape implements FromSourceLocation, Tagged, ToShapeId, Comparable { private final ShapeId id; private final Map traits; private final Map introducedTraits; private final Map mixins; private final transient SourceLocation source; private transient List memberNames; private int hash; /** * This class is package-private, which means that all subclasses of this * class must reside within the same package. Because of this, Shape is a * closed set of known concrete shape types. * * @param builder Builder to extract values from. * @param expectMemberSegments True/false if the ID must have a member. */ Shape(AbstractShapeBuilder builder, boolean expectMemberSegments) { source = builder.getSourceLocation(); id = SmithyBuilder.requiredState("id", builder.getId()); validateShapeId(expectMemberSegments); introducedTraits = MapUtils.copyOf(builder.getTraits()); mixins = MapUtils.orderedCopyOf(builder.getMixins()); if (mixins.isEmpty()) { // Simple case when there are no mixins. traits = introducedTraits; } else { validateMixins(mixins, introducedTraits); // Compute mixin traits. Map computedTraits = new HashMap<>(); for (Shape shape : mixins.values()) { // Mixin traits override other mixin traits, in order. computedTraits.putAll(MixinTrait.getNonLocalTraitsFromMap(shape.getAllTraits())); } // Traits applied to the shape directly override inherited traits. computedTraits.putAll(introducedTraits); traits = Collections.unmodifiableMap(computedTraits); } } protected void validateMixins(Map mixins, Map introducedTraits) { Set invalid = new TreeSet<>(); for (Shape mixin : mixins.values()) { if (mixin.getType() != getType()) { invalid.add(mixin.getId().toString()); } } if (!invalid.isEmpty()) { String invalidList = String.join("`, `", invalid); throw new SourceException(String.format( "Mixins may only be mixed into shapes of the same type. The following mixins were applied to the " + "%s shape `%s` which are not %1$s shapes: [`%s`]", getType(), getId(), invalidList), source ); } } protected MemberShape[] getRequiredMembers(AbstractShapeBuilder builder, String... requiredMembersNames) { // Caller knows the order of provided member names, so we don't need a dynamic data structure. MemberShape[] members = new MemberShape[requiredMembersNames.length]; int missingMemberCount = 0; for (int memberNameIndex = 0; memberNameIndex < requiredMembersNames.length; memberNameIndex++) { String requiredMemberName = requiredMembersNames[memberNameIndex]; MemberShape member = getRequiredMixinMember(builder, requiredMemberName); if (member != null) { members[memberNameIndex] = member; } else { missingMemberCount++; } } if (missingMemberCount > 0) { List missingMembers = new ArrayList<>(); for (int memberIndex = 0; memberIndex < members.length; memberIndex++) { if (members[memberIndex] == null) { missingMembers.add(requiredMembersNames[memberIndex]); } } throw missingRequiredMembersException(missingMembers); } return members; } private MemberShape getRequiredMixinMember(AbstractShapeBuilder builder, String requiredMemberName) { Optional memberOnBuilder = builder.getMember(requiredMemberName); if (memberOnBuilder.isPresent()) { return memberOnBuilder.get(); } // Get the most recently introduced mixin member with the given name. MemberShape mostRecentMember = null; for (Shape shape : builder.getMixins().values()) { for (MemberShape member : shape.members()) { if (member.getMemberName().equals(requiredMemberName)) { mostRecentMember = member; break; // break to the next mixin shape. } } } if (mostRecentMember == null) { return null; } return MemberShape.builder() .id(getId().withMember(requiredMemberName)) .target(mostRecentMember.getTarget()) .source(getSourceLocation()) .addMixin(mostRecentMember) .build(); } private SourceException missingRequiredMembersException(List missingMembersNames) { String missingRequired = missingMembersNames.size() > 1 ? "members" : "member"; String missingMembers = String.join(", ", missingMembersNames); String message = String.format("Missing required %s of shape `%s`: %s", missingRequired, getId(), missingMembers); return new SourceException(message, getSourceLocation()); } /** * Validates that a shape ID has or does not have a member. * * @param expectMember Whether or not a member is expected. */ private void validateShapeId(boolean expectMember) { if (expectMember) { if (!getId().hasMember()) { throw new SourceException(String.format( "Shapes of type `%s` must contain a member in their shape ID. Found `%s`", getType(), getId()), getSourceLocation()); } } else if (getId().hasMember()) { throw new SourceException(String.format( "Shapes of type `%s` cannot contain a member in their shape ID. Found `%s`", getType(), getId()), getSourceLocation()); } } protected final void validateMemberShapeIds() { for (MemberShape member : members()) { if (!member.getId().toString().startsWith(getId().toString())) { ShapeId expected = getId().withMember(member.getMemberName()); throw new SourceException(String.format( "Expected the `%s` member of `%s` to have an ID of `%s` but found `%s`", member.getMemberName(), getId(), expected, member.getId()), getSourceLocation()); } } } /** * Converts a shape, potentially of an unknown concrete type, into a * Shape builder. * * @param shape Shape to create a builder from. * @param Shape builder to create. * @param Shape that is being converted to a builder. * @return Returns a shape fro the given shape. */ @SuppressWarnings("unchecked") public static , S extends Shape> B shapeToBuilder(S shape) { return (B) shape.accept(new ShapeToBuilder()); } /** * Gets the type of the shape. * * @return Returns the type; */ public abstract ShapeType getType(); /** * Dispatches the shape to the appropriate {@link ShapeVisitor} method. * * @param Return type of the accept. * @param visitor ShapeVisitor to use. * @return Returns the result. */ public abstract R accept(ShapeVisitor visitor); /** * Get the {@link ShapeId} of the shape. * * @return Returns the shape ID. */ public final ShapeId getId() { return id; } /** * Checks if the shape has a specific trait by name. * *

Relative shape IDs are assumed to refer to the "smithy.api" * namespace. * * @param id The possibly relative trait ID. * @return Returns true if the shape has the given trait. */ public boolean hasTrait(String id) { return findTrait(id).isPresent(); } /** * Checks if the shape has a specific trait by name. * * @param id The fully-qualified trait ID. * @return Returns true if the shape has the given trait. */ public boolean hasTrait(ShapeId id) { return findTrait(id).isPresent(); } /** * Checks if the shape has a specific trait by class. * * @param traitClass Trait class to check. * @return Returns true if the shape has the given trait. */ public boolean hasTrait(Class traitClass) { return getTrait(traitClass).isPresent(); } /** * Attempts to find a trait applied to the shape by name. * * @param id The trait shape ID. * @return Returns the optionally found trait. */ public Optional findTrait(ShapeId id) { return Optional.ofNullable(traits.get(id)); } /** * Attempts to find a trait applied to the shape by ID. * *

Relative shape IDs are assumed to refer to the "smithy.api" * namespace. * * @param id The trait ID. * @return Returns the optionally found trait. */ public Optional findTrait(String id) { return findTrait(ShapeId.from(Trait.makeAbsoluteName(id))); } /** * Attempt to retrieve a specific {@link Trait} by class from the shape. * *

The first trait instance found matching the given type is returned. * * @param traitClass Trait class to retrieve. * @param The instance of the trait to retrieve. * @return Returns the matching trait. */ @SuppressWarnings("unchecked") public final Optional getTrait(Class traitClass) { for (Trait trait : traits.values()) { if (traitClass.isInstance(trait)) { return Optional.of((T) trait); } } return Optional.empty(); } /** * Gets specific {@link Trait} by class from the shape or throws if not found. * * @param traitClass Trait class to retrieve. * @param The instance of the trait to retrieve. * @return Returns the matching trait. * @throws ExpectationNotMetException if the trait cannot be found. */ public final T expectTrait(Class traitClass) { return getTrait(traitClass).orElseThrow(() -> new ExpectationNotMetException(String.format( "Expected shape `%s` to have a trait `%s`", getId(), traitClass.getCanonicalName()), this)); } /** * Gets all of the traits attached to the shape. * * @return Returns the attached traits. */ public final Map getAllTraits() { return traits; } /** * Gets a trait from the member shape or from the shape targeted by the * member. * *

If the shape is not a member, then the method functions the same as * {@link #getTrait(Class)}. * * @param model Model used to find member targets. * @param trait Trait type to get. * @param Trait type to get. * @return Returns the optionally found trait on the shape or member. * @see MemberShape#getTrait(Class) */ public Optional getMemberTrait(Model model, Class trait) { return getTrait(trait); } /** * Gets a trait from the member shape or from the shape targeted by the * member. * *

If the shape is not a member, then the method functions the same as * {@link #findTrait(String)}. * * @param model Model used to find member targets. * @param traitName Trait name to get. * @return Returns the optionally found trait on the shape or member. * @see MemberShape#findTrait(String) */ public Optional findMemberTrait(Model model, String traitName) { return findTrait(traitName); } /** * @return Optionally returns the shape as a {@link BigDecimalShape}. */ public Optional asBigDecimalShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link BigIntegerShape}. */ public Optional asBigIntegerShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link BlobShape}. */ public Optional asBlobShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link BooleanShape}. */ public Optional asBooleanShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link ByteShape}. */ public Optional asByteShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link ShortShape}. */ public Optional asShortShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link FloatShape}. */ public Optional asFloatShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link DocumentShape}. */ public Optional asDocumentShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link DoubleShape}. */ public Optional asDoubleShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link IntegerShape}. */ public Optional asIntegerShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link IntEnumShape}. */ public Optional asIntEnumShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link ListShape}. */ public Optional asListShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link SetShape}. */ @Deprecated public Optional asSetShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link LongShape}. */ public Optional asLongShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link MapShape}. */ public Optional asMapShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link MemberShape}. */ public Optional asMemberShape() { return Optional.empty(); } /** * @return Optionally returns the shape as an {@link OperationShape}. */ public Optional asOperationShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link ResourceShape}. */ public Optional asResourceShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link ServiceShape}. */ public Optional asServiceShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link StringShape}. */ public Optional asStringShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link EnumShape}. */ public Optional asEnumShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link StructureShape}. */ public Optional asStructureShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link UnionShape}. */ public Optional asUnionShape() { return Optional.empty(); } /** * @return Optionally returns the shape as a {@link TimestampShape}. */ public Optional asTimestampShape() { return Optional.empty(); } /** * @return Returns true if the shape is a {@link BigDecimalShape} shape. */ public final boolean isBigDecimalShape() { return getType() == ShapeType.BIG_DECIMAL; } /** * @return Returns true if the shape is a {@link BigIntegerShape} shape. */ public final boolean isBigIntegerShape() { return getType() == ShapeType.BIG_INTEGER; } /** * @return Returns true if the shape is a {@link BlobShape} shape. */ public final boolean isBlobShape() { return getType() == ShapeType.BLOB; } /** * @return Returns true if the shape is a {@link BooleanShape} shape. */ public final boolean isBooleanShape() { return getType() == ShapeType.BOOLEAN; } /** * @return Returns true if the shape is a {@link ByteShape} shape. */ public final boolean isByteShape() { return getType() == ShapeType.BYTE; } /** * @return Returns true if the shape is a {@link ShortShape} shape. */ public final boolean isShortShape() { return getType() == ShapeType.SHORT; } /** * @return Returns true if the shape is a {@link FloatShape} shape. */ public final boolean isFloatShape() { return getType() == ShapeType.FLOAT; } /** * @return Returns true if the shape is an {@link DocumentShape} shape. */ public final boolean isDocumentShape() { return getType() == ShapeType.DOCUMENT; } /** * @return Returns true if the shape is an {@link DoubleShape} shape. */ public final boolean isDoubleShape() { return getType() == ShapeType.DOUBLE; } /** * @return Returns true if the shape is a {@link ListShape} shape. */ public final boolean isListShape() { return getType() == ShapeType.LIST; } /** * @return Returns true if the shape is a {@link SetShape} shape. */ @Deprecated public final boolean isSetShape() { return getType() == ShapeType.SET; } /** * @return Returns true if the shape is a {@link IntegerShape} shape. */ public final boolean isIntegerShape() { return getType() == ShapeType.INTEGER || getType() == ShapeType.INT_ENUM; } /** * @return Returns true if the shape is a {@link IntEnumShape} shape. */ public final boolean isIntEnumShape() { return getType() == ShapeType.INT_ENUM; } /** * @return Returns true if the shape is a {@link LongShape} shape. */ public final boolean isLongShape() { return getType() == ShapeType.LONG; } /** * @return Returns true if the shape is a {@link MapShape} shape. */ public final boolean isMapShape() { return getType() == ShapeType.MAP; } /** * @return Returns true if the shape is a {@link MemberShape} shape. */ public final boolean isMemberShape() { return getType() == ShapeType.MEMBER; } /** * @return Returns true if the shape is an {@link OperationShape} shape. */ public final boolean isOperationShape() { return getType() == ShapeType.OPERATION; } /** * @return Returns true if the shape is a {@link ResourceShape} shape. */ public final boolean isResourceShape() { return getType() == ShapeType.RESOURCE; } /** * @return Returns true if the shape is a {@link ServiceShape} shape. */ public final boolean isServiceShape() { return getType() == ShapeType.SERVICE; } /** * @return Returns true if the shape is a {@link StringShape} shape. */ public final boolean isStringShape() { return getType() == ShapeType.STRING || getType() == ShapeType.ENUM; } /** * @return Returns true if the shape is an {@link EnumShape} shape. */ public final boolean isEnumShape() { return getType() == ShapeType.ENUM; } /** * @return Returns true if the shape is a {@link StructureShape} shape. */ public final boolean isStructureShape() { return getType() == ShapeType.STRUCTURE; } /** * @return Returns true if the shape is a {@link UnionShape} shape. */ public final boolean isUnionShape() { return getType() == ShapeType.UNION; } /** * @return Returns true if the shape is a {@link TimestampShape} shape. */ public final boolean isTimestampShape() { return getType() == ShapeType.TIMESTAMP; } /** * Gets all the members contained in the shape. * * @return Returns the members contained in the shape (if any). */ public Collection members() { return getAllMembers().values(); } /** * Get a specific member by name. * *

Shapes with no members return an empty Optional. * * @param name Name of the member to retrieve. * @return Returns the optional member. */ public Optional getMember(String name) { return Optional.ofNullable(getAllMembers().get(name)); } /** * Gets the members of the shape, including mixin members. * * @return Returns the immutable member map. */ public Map getAllMembers() { return Collections.emptyMap(); } /** * Returns an ordered list of member names based on the order they are * defined in the model, including mixin members. * *

The order in which map key and value members are returned might * not match the order in which they were defined in the model because * their ordering is insignificant. * * @return Returns an immutable list of member names. */ public List getMemberNames() { List result = memberNames; if (result == null) { result = ListUtils.copyOf(getAllMembers().keySet()); memberNames = result; } return result; } /** * Get an ordered set of mixins attached to the shape. * * @return Returns the ordered mixin shape IDs. */ public Set getMixins() { return mixins.keySet(); } /** * Gets the traits introduced by the shape and not inherited * from mixins. * * @return Returns the introduced traits. */ public Map getIntroducedTraits() { return introducedTraits; } @Override public ShapeId toShapeId() { return id; } @Override public final List getTags() { return getTrait(TagsTrait.class).map(TagsTrait::getValues).orElseGet(Collections::emptyList); } @Override public final SourceLocation getSourceLocation() { return source; } @Override public int compareTo(Shape other) { return getId().compareTo(other.getId()); } @Override public final String toString() { return "(" + getType() + ": `" + getId() + "`)"; } @Override public int hashCode() { int h = hash; if (h == 0) { h = Objects.hash(getType(), getId()); hash = h; } return h; } @Override public boolean equals(Object o) { if (o == this) { return true; } else if (!(o instanceof Shape)) { return false; } else if (hashCode() != o.hashCode()) { return false; // take advantage of hashcode caching } Shape other = (Shape) o; return getType() == other.getType() && getId().equals(other.getId()) && getMemberNames().equals(other.getMemberNames()) && getAllMembers().equals(other.getAllMembers()) && getAllTraits().equals(other.getAllTraits()) && mixins.equals(other.mixins); } /** * Copies the ID, source location, all traits, and mixins of the shape to the builder. * * @param builder Builder to update. * @param Type of shape being built. * @param Type of builder being built. * @return Returns the builder. */ > B updateBuilder(B builder) { builder.id(getId()); builder.source(getSourceLocation()); // Only add introduced traits to the builder to allow model load -> rebuild -> serialize roundtripping. builder.addTraits(getIntroducedTraits().values()); builder.mixins(mixins.values()); // Add members to the builder that are not just strictly inherited from mixins. for (MemberShape member : members()) { if (member.getMixins().isEmpty() || !member.getIntroducedTraits().isEmpty()) { builder.addMember(member); } } return builder; } }