software.amazon.smithy.model.shapes.EnumShape Maven / Gradle / Ivy
/*
* Copyright 2022 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.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import software.amazon.smithy.model.SourceException;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.traits.DeprecatedTrait;
import software.amazon.smithy.model.traits.DocumentationTrait;
import software.amazon.smithy.model.traits.EnumDefinition;
import software.amazon.smithy.model.traits.EnumTrait;
import software.amazon.smithy.model.traits.EnumValueTrait;
import software.amazon.smithy.model.traits.InternalTrait;
import software.amazon.smithy.model.traits.StringListTrait;
import software.amazon.smithy.model.traits.TagsTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.traits.UnitTypeTrait;
import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait;
import software.amazon.smithy.utils.BuilderRef;
public final class EnumShape extends StringShape {
private static final Pattern CONVERTABLE_VALUE = Pattern.compile("^[a-zA-Z-_.:/\\s]+[a-zA-Z_0-9-.:/\\s]*$");
private static final Logger LOGGER = Logger.getLogger(EnumShape.class.getName());
private final Map members;
private volatile Map enumValues;
private EnumShape(Builder builder) {
super(builder);
members = NamedMemberUtils.computeMixinMembers(
builder.getMixins(), builder.members, getId(), getSourceLocation());
validateMemberShapeIds();
if (members.size() < 1) {
throw new SourceException("enum shapes must have at least one member", getSourceLocation());
}
}
private EnumShape(Builder builder, Map members) {
super(builder);
this.members = members;
validateMemberShapeIds();
if (members.size() < 1) {
throw new SourceException("enum shapes must have at least one member", getSourceLocation());
}
}
/**
* Gets a map of enum member names to their corresponding values.
*
* @return A map of member names to enum values.
*/
public Map getEnumValues() {
if (enumValues == null) {
Map values = new LinkedHashMap<>(members.size());
for (MemberShape member : members()) {
values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).expectStringValue());
}
enumValues = Collections.unmodifiableMap(values);
}
return enumValues;
}
/**
* Gets the members of the shape, including mixin members.
*
* @return Returns the immutable member map.
*/
@Override
public Map getAllMembers() {
return members;
}
@Override
@SuppressWarnings("deprecation")
public Optional findTrait(ShapeId id) {
if (id.equals(EnumTrait.ID)) {
return super.findTrait(SyntheticEnumTrait.ID);
}
return super.findTrait(id);
}
public static Builder builder() {
return new Builder();
}
@Override
public Builder toBuilder() {
return (Builder) updateBuilder(builder());
}
@Override
public R accept(ShapeVisitor cases) {
return cases.enumShape(this);
}
@Override
public Optional asEnumShape() {
return Optional.of(this);
}
/**
* Converts a base {@link StringShape} to an {@link EnumShape} if possible.
*
* The result will be empty if the given shape doesn't have the {@link EnumTrait},
* if the enum doesn't have names and name synthesization is disabled, or if a name
* cannot be synthesized.
*
* @param shape A base {@link StringShape} to convert.
* @param synthesizeNames Whether names should be synthesized if possible.
* @return Optionally returns an {@link EnumShape} equivalent of the given shape.
*/
public static Optional fromStringShape(StringShape shape, boolean synthesizeNames) {
if (shape.isEnumShape()) {
return Optional.of((EnumShape) shape);
}
if (!shape.hasTrait(EnumTrait.ID)) {
return Optional.empty();
}
StringShape stringWithoutEnumTrait = shape.toBuilder().removeTrait(EnumTrait.ID).build();
Builder enumBuilder = EnumShape.builder();
stringWithoutEnumTrait.updateBuilder(enumBuilder);
try {
return Optional.of(enumBuilder
.setMembersFromEnumTrait(shape.expectTrait(EnumTrait.class), synthesizeNames)
.build());
} catch (IllegalStateException e) {
LOGGER.info(String.format("Unable to convert `%s` to an enum: %s", shape.getId(), e));
return Optional.empty();
}
}
/**
* Converts a base {@link StringShape} to an {@link EnumShape} if possible.
*
* The result will be empty if the given shape doesn't have the {@link EnumTrait}
* or if the enum definitions don't have names.
*
* @param shape A base {@link StringShape} to convert.
* @return Optionally returns an {@link EnumShape} equivalent of the given shape.
*/
public static Optional fromStringShape(StringShape shape) {
return fromStringShape(shape, false);
}
/**
* Determines whether a given string shape can be converted to an enum shape.
*
* @param shape The string shape to be converted.
* @param synthesizeEnumNames Whether synthesizing enum names should be accounted for.
* @return Returns true if the string shape can be converted to an enum shape.
*/
public static boolean canConvertToEnum(StringShape shape, boolean synthesizeEnumNames) {
if (shape.isEnumShape()) {
return true;
}
if (!shape.hasTrait(EnumTrait.class)) {
LOGGER.info(String.format(
"Unable to convert string shape `%s` to enum shape because it doesn't have an enum trait.",
shape.getId()
));
return false;
}
EnumTrait trait = shape.expectTrait(EnumTrait.class);
if (!trait.hasNames() && !synthesizeEnumNames) {
LOGGER.info(String.format(
"Unable to convert string shape `%s` to enum shape because it doesn't define names. The "
+ "`synthesizeNames` option may be able to synthesize the names for you.",
shape.getId()
));
return false;
}
for (EnumDefinition definition : trait.getValues()) {
if (!canConvertEnumDefinitionToMember(definition, synthesizeEnumNames)) {
LOGGER.info(String.format(
"Unable to convert string shape `%s` to enum shape because it has at least one value which "
+ "cannot be safely synthesized into a name: %s",
shape.getId(), definition.getValue()
));
return false;
}
}
return true;
}
/**
* Converts an enum definition to the equivalent enum member shape.
*
* If an enum definition is marked as deprecated, the DeprecatedTrait
* is applied to the converted enum member shape.
*
*
If an enum definition has an "internal" tag, the InternalTrait is
* applied to the converted enum member shape.
*
* @param parentId The {@link ShapeId} of the enum shape.
* @param synthesizeName Whether to synthesize a name if possible.
* @return An optional member shape representing the enum definition,
* or empty if conversion is impossible.
*/
static Optional memberFromEnumDefinition(
EnumDefinition definition,
ShapeId parentId,
boolean synthesizeName
) {
String name;
if (!definition.getName().isPresent()) {
if (canConvertEnumDefinitionToMember(definition, synthesizeName)) {
name = definition.getValue().replaceAll("[-.:/\\s]+", "_");
} else {
return Optional.empty();
}
} else {
name = definition.getName().get();
}
try {
MemberShape.Builder builder = MemberShape.builder()
.id(parentId.withMember(name))
.target(UnitTypeTrait.UNIT)
.addTrait(EnumValueTrait.builder().stringValue(definition.getValue()).build());
definition.getDocumentation().ifPresent(docs -> builder.addTrait(new DocumentationTrait(docs)));
if (!definition.getTags().isEmpty()) {
builder.addTrait(TagsTrait.builder().values(definition.getTags()).build());
}
if (definition.isDeprecated()) {
builder.addTrait(DeprecatedTrait.builder().build());
}
if (definition.hasTag("internal")) {
builder.addTrait(new InternalTrait());
}
return Optional.of(builder.build());
} catch (ShapeIdSyntaxException e) {
return Optional.empty();
}
}
/**
* Determines whether the definition can be converted to a member.
*
* @param withSynthesizedNames Whether to account for name synthesization.
* @return Returns true if the definition can be converted.
*/
static boolean canConvertEnumDefinitionToMember(EnumDefinition definition, boolean withSynthesizedNames) {
return definition.getName().isPresent()
|| (withSynthesizedNames && CONVERTABLE_VALUE.matcher(definition.getValue()).find());
}
/**
* Converts an enum member into an equivalent enum definition object.
*
* @param member The enum member to convert.
* @return An {@link EnumDefinition} representing the given member.
*/
static EnumDefinition enumDefinitionFromMember(MemberShape member) {
EnumDefinition.Builder builder = EnumDefinition.builder().name(member.getMemberName());
String traitValue = member
.getTrait(EnumValueTrait.class)
.flatMap(EnumValueTrait::getStringValue)
.orElseThrow(() -> new IllegalStateException("Enum definitions can only be made for string enums."));
builder.value(traitValue);
member.getTrait(DocumentationTrait.class).ifPresent(docTrait -> builder.documentation(docTrait.getValue()));
member.getTrait(DeprecatedTrait.class).ifPresent(deprecatedTrait -> builder.deprecated(true));
List tags = member.getTrait(TagsTrait.class)
.map(StringListTrait::getValues)
.orElse(Collections.emptyList());
builder.tags(tags);
if (member.hasTrait(InternalTrait.class) && !tags.contains("internal")) {
builder.addTag("internal");
}
return builder.build();
}
@Override
public ShapeType getType() {
return ShapeType.ENUM;
}
public static final class Builder extends StringShape.Builder {
private final BuilderRef