Leave Primitive* prelude shapes in 2.0
We previously rewrote models that target smithy.api#Primtive* shapes to target their prelude counterparts that do not have the Primitive prefix. For example, smithy.api#PrimitiveInteger would become smithy.api#Integer. The rationale being that because optionality is now controlled on members rather than based on the shape targeted by a member, there is no longer a reason to use Primitive* shapes in 2.0 models. The problem is that rewriting the shape targets during the loading process can remove information that tooling might need to determine if a shape was considered optional in IDL 1.0 (usually in fringe cases). If a member previously targets PrimitiveInteger and is rewritten to target Integer, but the member is also marked as required, then the ModelUpgrader can't add the default trait to the member. If optionality isn't determined based on the required trait, then a tool has no way to know that a member that targets Integer previously targeted PrimitiveInteger, thereby losing information. Instead of removing Primitive* shapes, we will keep them in 2.0 and mark them as deprecated. To point this out to end users, I added validation (that really should have already existed) to detect when a shape refers to a deprecated shape. However, because we don't want to break previous test cases and because models should be able to deprecate shapes without breaking test cases, DeprecatedShape does not need to be explicitly handled by Smithy errorfile test runners. This matches the special casing added for DeprecatedTrait and ModelDeprecation.
This commit is contained in:
parent
e60275c103
commit
a6b06611aa
|
@ -135,7 +135,7 @@ This proposal:
|
|||
1. Introduces a `@default` trait
|
||||
2. Introduces a `@clientOptional` trait
|
||||
3. Removes the `@box` trait
|
||||
4. Removes the `Primitive*` shapes from the prelude
|
||||
4. Deprecates the `Primitive*` shapes from the prelude
|
||||
5. Make the optionality of a structure completely controlled by members rather
|
||||
than the shape targeted by a member
|
||||
|
||||
|
@ -195,19 +195,11 @@ optionality controls from shapes to structure members. Smithy IDL 2.0 will:
|
|||
version bump of the service.
|
||||
3. Remove the `@box` trait from the Smithy 2.0 prelude and fail to load models
|
||||
that contain the `@box` trait.
|
||||
4. Remove the `PrimitiveBoolean`, `PrimitiveShort`, `PrimitiveInteger`,
|
||||
`PrimitiveLong`, `PrimitiveFloat`, and `PrimitiveDouble` shapes from the
|
||||
Smithy 2.0 prelude. IDL 2.0 models will fail if they use these shapes.
|
||||
5. Update the Smithy IDL 2.0 model loader implementation to be able to load
|
||||
Smithy 1.0 models alongside Smithy 2.0 models.
|
||||
1. Inject an appropriate `@default` trait on structure members that targeted
|
||||
shapes with a default zero value and were not marked with the `@box`
|
||||
trait.
|
||||
2. Replace the `@box` trait with `@clientOptional` on structure members.
|
||||
3. Remove the `@box` trait from non-members.
|
||||
4. Rewrite members that target one of the removed Primitive* shapes to
|
||||
target the corresponding non-primitive shape in the prelude (for example,
|
||||
`PrimitiveInteger` is rewritten to target `Integer`).
|
||||
4. Deprecate the `PrimitiveBoolean`, `PrimitiveShort`, `PrimitiveInteger`,
|
||||
`PrimitiveLong`, `PrimitiveFloat`, and `PrimitiveDouble` shapes in the
|
||||
Smithy 2.0 prelude. These shapes are treated exactly the same as their
|
||||
prelude counterparts not prefixed with "Primitive", so there is no need to
|
||||
use these shapes.
|
||||
|
||||
|
||||
## `@default` trait
|
||||
|
|
|
@ -1288,6 +1288,48 @@ referenced from within any namespace using a relative shape ID.
|
|||
@unitType
|
||||
structure Unit {}
|
||||
|
||||
@deprecated(
|
||||
message: "Use Boolean instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
boolean PrimitiveBoolean
|
||||
|
||||
@deprecated(
|
||||
message: "Use Byte instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
byte PrimitiveByte
|
||||
|
||||
@deprecated(
|
||||
message: "Use Short instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
short PrimitiveShort
|
||||
|
||||
@deprecated(
|
||||
message: "Use Integer instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
integer PrimitiveInteger
|
||||
|
||||
@deprecated(
|
||||
message: "Use Long instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
long PrimitiveLong
|
||||
|
||||
@deprecated(
|
||||
message: "Use Float instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
float PrimitiveFloat
|
||||
|
||||
@deprecated(
|
||||
message: "Use Double instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
double PrimitiveDouble
|
||||
|
||||
|
||||
.. _unit-type:
|
||||
|
||||
|
|
|
@ -46,7 +46,6 @@ import software.amazon.smithy.cli.HelpPrinter;
|
|||
import software.amazon.smithy.model.Model;
|
||||
import software.amazon.smithy.model.SourceLocation;
|
||||
import software.amazon.smithy.model.loader.ModelAssembler;
|
||||
import software.amazon.smithy.model.loader.ParserUtils;
|
||||
import software.amazon.smithy.model.loader.Prelude;
|
||||
import software.amazon.smithy.model.shapes.MemberShape;
|
||||
import software.amazon.smithy.model.shapes.NumberShape;
|
||||
|
@ -282,8 +281,6 @@ public final class Upgrade1to2Command extends SimpleCommand {
|
|||
}
|
||||
|
||||
private void handleMemberShape(MemberShape shape) {
|
||||
replacePrimitiveTarget(shape);
|
||||
|
||||
if (hasSyntheticDefault(shape)) {
|
||||
SourceLocation memberLocation = shape.getSourceLocation();
|
||||
String padding = "";
|
||||
|
@ -309,29 +306,6 @@ public final class Upgrade1to2Command extends SimpleCommand {
|
|||
}
|
||||
}
|
||||
|
||||
private void replacePrimitiveTarget(MemberShape member) {
|
||||
Shape target = completeModel.expectShape(member.getTarget());
|
||||
if (!Prelude.isPreludeShape(target) || !HAD_DEFAULT_VALUE_IN_1_0.contains(target.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
IdlAwareSimpleParser parser = new IdlAwareSimpleParser(writer.flush());
|
||||
parser.rewind(member.getSourceLocation());
|
||||
|
||||
parser.consumeUntilNoLongerMatches(character -> character != ':');
|
||||
parser.skip();
|
||||
parser.ws();
|
||||
|
||||
// Capture the start of the target identifier.
|
||||
int start = parser.position();
|
||||
parser.consumeUntilNoLongerMatches(ParserUtils::isValidIdentifierCharacter);
|
||||
|
||||
// Replace the target with the proper target. Note that we don't
|
||||
// need to do any sort of mapping because smithy already upgraded
|
||||
// the target, so we can just use the name of the target it added.
|
||||
writer.replace(start, parser.position(), target.getId().getName());
|
||||
}
|
||||
|
||||
private boolean hasSyntheticDefault(MemberShape shape) {
|
||||
Optional<SourceLocation> defaultLocation = shape.getTrait(DefaultTrait.class)
|
||||
.map(Trait::getSourceLocation);
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
$version: "2.0"
|
||||
|
||||
namespace com.example
|
||||
|
||||
structure PrimitiveBearer {
|
||||
@default(0)
|
||||
int: PrimitiveInteger,
|
||||
@default(false)
|
||||
bool: PrimitiveBoolean,
|
||||
@default(0)
|
||||
byte: PrimitiveByte,
|
||||
@default(0)
|
||||
double: PrimitiveDouble,
|
||||
@default(0)
|
||||
float: PrimitiveFloat,
|
||||
@default(0)
|
||||
long: PrimitiveLong,
|
||||
@default(0)
|
||||
short: PrimitiveShort,
|
||||
|
||||
@default(0)
|
||||
handlesComments: // Nobody actually does this right?
|
||||
PrimitiveShort,
|
||||
|
||||
@required
|
||||
handlesRequired: PrimitiveLong,
|
||||
|
||||
handlesBox: PrimitiveByte,
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
$version: "2.0"
|
||||
|
||||
namespace com.example
|
||||
|
||||
structure PrimitiveBearer {
|
||||
@default(0)
|
||||
int: Integer,
|
||||
@default(false)
|
||||
bool: Boolean,
|
||||
@default(0)
|
||||
byte: Byte,
|
||||
@default(0)
|
||||
double: Double,
|
||||
@default(0)
|
||||
float: Float,
|
||||
@default(0)
|
||||
long: Long,
|
||||
@default(0)
|
||||
short: Short,
|
||||
|
||||
@default(0)
|
||||
handlesComments: // Nobody actually does this right?
|
||||
Short,
|
||||
|
||||
@required
|
||||
handlesRequired: Long,
|
||||
|
||||
handlesBox: Byte,
|
||||
}
|
|
@ -22,7 +22,6 @@ import software.amazon.smithy.model.Model;
|
|||
import software.amazon.smithy.model.loader.ModelAssembler;
|
||||
import software.amazon.smithy.model.shapes.MemberShape;
|
||||
import software.amazon.smithy.model.shapes.Shape;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.shapes.ShapeType;
|
||||
import software.amazon.smithy.model.shapes.ToShapeId;
|
||||
import software.amazon.smithy.model.traits.BoxTrait;
|
||||
|
@ -37,22 +36,13 @@ import software.amazon.smithy.utils.SetUtils;
|
|||
* An index that checks if a member is nullable.
|
||||
*
|
||||
* <p>Note: this index assumes Smithy 2.0 nullability semantics.
|
||||
* There is basic support for detecting 1.0 models by detecting
|
||||
* when a removed primitive prelude shape is targeted by a member.
|
||||
* Beyond that, 1.0 models SHOULD be loaded through a {@link ModelAssembler}
|
||||
* to upgrade them to IDL 2.0.
|
||||
* 1.0 models SHOULD be loaded through a {@link ModelAssembler}
|
||||
* to upgrade them in memory to IDL 2.0. Use
|
||||
* {@link #isMemberNullableInV1(MemberShape)} to check if a shape
|
||||
* is nullable according to Smithy 1.0 semantics.
|
||||
*/
|
||||
public class NullableIndex implements KnowledgeIndex {
|
||||
|
||||
private static final Set<ShapeId> V1_REMOVED_PRIMITIVE_SHAPES = SetUtils.of(
|
||||
ShapeId.from("smithy.api#PrimitiveBoolean"),
|
||||
ShapeId.from("smithy.api#PrimitiveByte"),
|
||||
ShapeId.from("smithy.api#PrimitiveShort"),
|
||||
ShapeId.from("smithy.api#PrimitiveInteger"),
|
||||
ShapeId.from("smithy.api#PrimitiveLong"),
|
||||
ShapeId.from("smithy.api#PrimitiveFloat"),
|
||||
ShapeId.from("smithy.api#PrimitiveDouble"));
|
||||
|
||||
private static final Set<ShapeType> V1_INHERENTLY_BOXED = SetUtils.of(
|
||||
ShapeType.STRING,
|
||||
ShapeType.BLOB,
|
||||
|
@ -148,12 +138,6 @@ public class NullableIndex implements KnowledgeIndex {
|
|||
* {@link ClientOptionalTrait}, while non-authoritative consumers like clients
|
||||
* must honor these traits.
|
||||
*
|
||||
* <p>This method will also attempt to detect when a member targets a
|
||||
* primitive prelude shape that was removed in Smithy IDL 2.0 to account
|
||||
* for models that were created manually without passing through a
|
||||
* ModelAssembler. If a member targets a removed primitive prelude shape,
|
||||
* the member is considered non-null.
|
||||
*
|
||||
* @param member Member to check.
|
||||
* @param checkMode The mode used when checking if the member is considered nullable.
|
||||
* @return Returns true if the member is optional.
|
||||
|
@ -171,14 +155,7 @@ public class NullableIndex implements KnowledgeIndex {
|
|||
}
|
||||
|
||||
// Structure members that are @required or @default are not nullable.
|
||||
if (member.hasTrait(DefaultTrait.class) || member.hasTrait(RequiredTrait.class)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detect if the member targets a 1.0 primitive prelude shape and the shape wasn't upgraded.
|
||||
// These removed prelude shapes are impossible to appear in a 2.0 model, so it's safe to
|
||||
// detect them and honor 1.0 semantics here.
|
||||
return !V1_REMOVED_PRIMITIVE_SHAPES.contains(member.getTarget());
|
||||
return !member.hasTrait(DefaultTrait.class) && !member.hasTrait(RequiredTrait.class);
|
||||
case UNION:
|
||||
case SET:
|
||||
// Union and set members are never null.
|
||||
|
|
|
@ -18,9 +18,7 @@ package software.amazon.smithy.model.loader;
|
|||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
@ -42,13 +40,12 @@ import software.amazon.smithy.model.validation.Severity;
|
|||
import software.amazon.smithy.model.validation.ValidatedResult;
|
||||
import software.amazon.smithy.model.validation.ValidationEvent;
|
||||
import software.amazon.smithy.model.validation.Validator;
|
||||
import software.amazon.smithy.utils.MapUtils;
|
||||
import software.amazon.smithy.utils.SetUtils;
|
||||
|
||||
/**
|
||||
* Upgrades Smithy models from IDL v1 to IDL v2, specifically taking into
|
||||
* account the removal of the Primitive* shapes from the prelude, the
|
||||
* removal of the @box trait, the change in default value semantics of
|
||||
* numbers and booleans, and the @default trait.
|
||||
* account the removal of the box trait, the change in default value
|
||||
* semantics of numbers and booleans, and the @default trait.
|
||||
*/
|
||||
final class ModelUpgrader {
|
||||
|
||||
|
@ -62,18 +59,15 @@ final class ModelUpgrader {
|
|||
ShapeType.DOUBLE,
|
||||
ShapeType.BOOLEAN);
|
||||
|
||||
/** Provides a mapping of prelude shapes that were removed to the shape to use instead. */
|
||||
private static final Map<ShapeId, ShapeId> REMOVED_PRIMITIVE_SHAPES = MapUtils.of(
|
||||
ShapeId.from("smithy.api#PrimitiveBoolean"), ShapeId.from("smithy.api#Boolean"),
|
||||
ShapeId.from("smithy.api#PrimitiveByte"), ShapeId.from("smithy.api#Byte"),
|
||||
ShapeId.from("smithy.api#PrimitiveShort"), ShapeId.from("smithy.api#Short"),
|
||||
ShapeId.from("smithy.api#PrimitiveInteger"), ShapeId.from("smithy.api#Integer"),
|
||||
ShapeId.from("smithy.api#PrimitiveLong"), ShapeId.from("smithy.api#Long"),
|
||||
ShapeId.from("smithy.api#PrimitiveFloat"), ShapeId.from("smithy.api#Float"),
|
||||
ShapeId.from("smithy.api#PrimitiveDouble"), ShapeId.from("smithy.api#Double"));
|
||||
|
||||
/** Shapes that were boxed in 1.0, but @box was removed in 2.0. */
|
||||
private static final Set<ShapeId> PREVIOUSLY_BOXED = new HashSet<>(REMOVED_PRIMITIVE_SHAPES.values());
|
||||
private static final Set<ShapeId> PREVIOUSLY_BOXED = SetUtils.of(
|
||||
ShapeId.from("smithy.api#Boolean"),
|
||||
ShapeId.from("smithy.api#Byte"),
|
||||
ShapeId.from("smithy.api#Short"),
|
||||
ShapeId.from("smithy.api#Integer"),
|
||||
ShapeId.from("smithy.api#Long"),
|
||||
ShapeId.from("smithy.api#Float"),
|
||||
ShapeId.from("smithy.api#Double"));
|
||||
|
||||
private final Model model;
|
||||
private final List<ValidationEvent> events;
|
||||
|
@ -117,13 +111,6 @@ final class ModelUpgrader {
|
|||
// This builder will become non-null if/when the member needs to be updated.
|
||||
MemberShape.Builder builder = null;
|
||||
|
||||
// Rewrite the member to target the non-removed prelude shape if it's known to be removed.
|
||||
if (REMOVED_PRIMITIVE_SHAPES.containsKey(target.getId())) {
|
||||
emitWhenTargetingRemovedPreludeShape(Severity.WARNING, member);
|
||||
builder = createOrReuseBuilder(member, builder);
|
||||
builder.target(REMOVED_PRIMITIVE_SHAPES.get(target.getId()));
|
||||
}
|
||||
|
||||
// Add the @default trait to structure members when needed.
|
||||
if (shouldV1MemberHaveDefaultTrait(containerType, member, target)) {
|
||||
events.add(ValidationEvent.builder()
|
||||
|
@ -166,7 +153,6 @@ final class ModelUpgrader {
|
|||
&& containerType == ShapeType.STRUCTURE
|
||||
&& !member.hasTrait(DefaultTrait.class) // don't add box if it has a default trait.
|
||||
&& !member.hasTrait(BoxTrait.class) // don't add box again
|
||||
&& !REMOVED_PRIMITIVE_SHAPES.containsKey(target.getId())
|
||||
&& (target.hasTrait(BoxTrait.class) || PREVIOUSLY_BOXED.contains(target.getId()));
|
||||
}
|
||||
|
||||
|
@ -234,10 +220,6 @@ final class ModelUpgrader {
|
|||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void validateV2Member(MemberShape member) {
|
||||
if (REMOVED_PRIMITIVE_SHAPES.containsKey(member.getTarget())) {
|
||||
emitWhenTargetingRemovedPreludeShape(Severity.ERROR, member);
|
||||
}
|
||||
|
||||
if (member.hasTrait(BoxTrait.class)) {
|
||||
events.add(ValidationEvent.builder()
|
||||
.id(Validator.MODEL_DEPRECATION)
|
||||
|
@ -248,16 +230,4 @@ final class ModelUpgrader {
|
|||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private void emitWhenTargetingRemovedPreludeShape(Severity severity, MemberShape member) {
|
||||
events.add(ValidationEvent.builder()
|
||||
.id(Validator.MODEL_DEPRECATION)
|
||||
.severity(severity)
|
||||
.shape(member)
|
||||
.sourceLocation(member)
|
||||
.message("This member targets " + member.getTarget() + " which was removed in Smithy "
|
||||
+ "IDL " + Model.MODEL_VERSION + ". Target "
|
||||
+ REMOVED_PRIMITIVE_SHAPES.get(member.getTarget()) + " instead ")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,7 +144,8 @@ public final class SmithyTestCase {
|
|||
private boolean isModelDeprecationEvent(ValidationEvent event) {
|
||||
return event.getId().equals(Validator.MODEL_DEPRECATION)
|
||||
// Trait vendors should be free to deprecate a trait without breaking consumers.
|
||||
|| (event.getId().equals("DeprecatedTrait"));
|
||||
|| event.getId().equals("DeprecatedTrait")
|
||||
|| event.getId().equals("DeprecatedShape");
|
||||
}
|
||||
|
||||
private static String inferErrorFileLocation(String modelLocation) {
|
||||
|
|
|
@ -21,7 +21,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import software.amazon.smithy.model.Model;
|
||||
|
@ -33,11 +33,14 @@ import software.amazon.smithy.model.neighbor.RelationshipType;
|
|||
import software.amazon.smithy.model.shapes.Shape;
|
||||
import software.amazon.smithy.model.shapes.ShapeId;
|
||||
import software.amazon.smithy.model.shapes.ShapeType;
|
||||
import software.amazon.smithy.model.traits.DeprecatedTrait;
|
||||
import software.amazon.smithy.model.traits.MixinTrait;
|
||||
import software.amazon.smithy.model.traits.TraitDefinition;
|
||||
import software.amazon.smithy.model.validation.AbstractValidator;
|
||||
import software.amazon.smithy.model.validation.Severity;
|
||||
import software.amazon.smithy.model.validation.ValidationEvent;
|
||||
import software.amazon.smithy.utils.FunctionalUtils;
|
||||
import software.amazon.smithy.utils.MapUtils;
|
||||
import software.amazon.smithy.utils.SetUtils;
|
||||
import software.amazon.smithy.utils.StringUtils;
|
||||
|
||||
|
@ -50,6 +53,18 @@ public final class TargetValidator extends AbstractValidator {
|
|||
private static final Set<ShapeType> INVALID_MEMBER_TARGETS = SetUtils.of(
|
||||
ShapeType.SERVICE, ShapeType.RESOURCE, ShapeType.OPERATION, ShapeType.MEMBER);
|
||||
|
||||
// Relationship types listed here are checked to see if a shape refers to a deprecated shape.
|
||||
private static final Map<RelationshipType, String> RELATIONSHIP_TYPE_DEPRECATION_MAPPINGS = MapUtils.of(
|
||||
RelationshipType.MEMBER_TARGET, "Member targets a deprecated shape",
|
||||
RelationshipType.RESOURCE, "Binds a deprecated resource",
|
||||
RelationshipType.OPERATION, "Binds a deprecated operation",
|
||||
RelationshipType.IDENTIFIER, "Resource identifier targets a deprecated shape",
|
||||
RelationshipType.PROPERTY, "Resource property targets a deprecated shape",
|
||||
RelationshipType.INPUT, "Operation input targets a deprecated shape",
|
||||
RelationshipType.OUTPUT, "Operation output targets a deprecated shape",
|
||||
RelationshipType.ERROR, "Operation error targets a deprecated shape",
|
||||
RelationshipType.MIXIN, "Applies a deprecated mixin");
|
||||
|
||||
@Override
|
||||
public List<ValidationEvent> validate(Model model) {
|
||||
List<ValidationEvent> events = new ArrayList<>();
|
||||
|
@ -68,110 +83,144 @@ public final class TargetValidator extends AbstractValidator {
|
|||
) {
|
||||
for (Relationship relationship : relationships) {
|
||||
if (relationship.getNeighborShape().isPresent()) {
|
||||
validateTarget(model, shape, relationship.getNeighborShape().get(), relationship)
|
||||
.ifPresent(mutableEvents::add);
|
||||
validateTarget(model, shape, relationship.getNeighborShape().get(), relationship, mutableEvents);
|
||||
} else {
|
||||
mutableEvents.add(unresolvedTarget(model, shape, relationship));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<ValidationEvent> validateTarget(Model model, Shape shape, Shape target, Relationship rel) {
|
||||
private void validateTarget(
|
||||
Model model,
|
||||
Shape shape,
|
||||
Shape target,
|
||||
Relationship rel,
|
||||
List<ValidationEvent> events
|
||||
) {
|
||||
RelationshipType relType = rel.getRelationshipType();
|
||||
|
||||
if (relType != RelationshipType.MIXIN && relType.getDirection() == RelationshipDirection.DIRECTED) {
|
||||
if (target.hasTrait(TraitDefinition.class)) {
|
||||
return Optional.of(error(shape, format(
|
||||
events.add(error(shape, format(
|
||||
"Found a %s reference to trait definition `%s`. Trait definitions cannot be targeted by "
|
||||
+ "members or referenced by shapes in any other context other than applying them as "
|
||||
+ "traits.", relType, rel.getNeighborShapeId())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignoring members with the mixin trait, forbid shapes to reference mixins except as mixins.
|
||||
if (!target.isMemberShape() && target.hasTrait(MixinTrait.class)) {
|
||||
return Optional.of(error(shape, format(
|
||||
events.add(error(shape, format(
|
||||
"Illegal %s reference to mixin `%s`; shapes marked with the mixin trait can only be "
|
||||
+ "referenced to apply them as a mixin.", relType, rel.getNeighborShapeId())));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
validateDeprecatedTargets(shape, target, relType, events);
|
||||
|
||||
switch (relType) {
|
||||
case PROPERTY:
|
||||
case MEMBER_TARGET:
|
||||
// Members and property cannot target other members, service, operation, or resource shapes.
|
||||
if (INVALID_MEMBER_TARGETS.contains(target.getType())) {
|
||||
return Optional.of(error(shape, format(
|
||||
events.add(error(shape, format(
|
||||
"Members cannot target %s shapes, but found %s", target.getType(), target)));
|
||||
}
|
||||
return Optional.empty();
|
||||
break;
|
||||
case MAP_KEY:
|
||||
return target.asMemberShape().flatMap(m -> validateMapKey(shape, m.getTarget(), model));
|
||||
target.asMemberShape().ifPresent(m -> validateMapKey(shape, m.getTarget(), model, events));
|
||||
break;
|
||||
case RESOURCE:
|
||||
if (target.getType() != ShapeType.RESOURCE) {
|
||||
return Optional.of(badType(shape, target, relType, ShapeType.RESOURCE));
|
||||
events.add(badType(shape, target, relType, ShapeType.RESOURCE));
|
||||
}
|
||||
return Optional.empty();
|
||||
break;
|
||||
case OPERATION:
|
||||
if (target.getType() != ShapeType.OPERATION) {
|
||||
return Optional.of(badType(shape, target, relType, ShapeType.OPERATION));
|
||||
events.add(badType(shape, target, relType, ShapeType.OPERATION));
|
||||
}
|
||||
return Optional.empty();
|
||||
break;
|
||||
case INPUT:
|
||||
case OUTPUT:
|
||||
// Input/output must target structures and cannot have the error trait.
|
||||
if (target.getType() != ShapeType.STRUCTURE) {
|
||||
return Optional.of(badType(shape, target, relType, ShapeType.STRUCTURE));
|
||||
events.add(badType(shape, target, relType, ShapeType.STRUCTURE));
|
||||
} else if (target.findTrait("error").isPresent()) {
|
||||
return Optional.of(inputOutputWithErrorTrait(shape, target, rel.getRelationshipType()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
events.add(inputOutputWithErrorTrait(shape, target, rel.getRelationshipType()));
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
// Errors must target a structure with the error trait.
|
||||
if (target.getType() != ShapeType.STRUCTURE) {
|
||||
return Optional.of(badType(shape, target, relType, ShapeType.STRUCTURE));
|
||||
events.add(badType(shape, target, relType, ShapeType.STRUCTURE));
|
||||
} else if (!target.findTrait("error").isPresent()) {
|
||||
return Optional.of(errorNoTrait(shape, target.getId()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
events.add(errorNoTrait(shape, target.getId()));
|
||||
}
|
||||
break;
|
||||
case IDENTIFIER:
|
||||
return validateIdentifier(shape, target);
|
||||
validateIdentifier(shape, target, events);
|
||||
break;
|
||||
case CREATE:
|
||||
case READ:
|
||||
case UPDATE:
|
||||
case DELETE:
|
||||
case LIST:
|
||||
if (target.getType() != ShapeType.OPERATION) {
|
||||
return Optional.of(error(shape, format(
|
||||
events.add(error(shape, format(
|
||||
"Resource %s lifecycle operation must target an operation, but found %s",
|
||||
relType.toString().toLowerCase(Locale.US), target)));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
break;
|
||||
case MIXIN:
|
||||
if (!target.hasTrait(MixinTrait.class)) {
|
||||
return Optional.of(error(shape, format(
|
||||
events.add(error(shape, format(
|
||||
"Attempted to use %s as a mixin, but it is not marked with the mixin trait",
|
||||
target.getId())));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return Optional.empty();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<ValidationEvent> validateMapKey(Shape shape, ShapeId target, Model model) {
|
||||
return model.getShape(target)
|
||||
.filter(FunctionalUtils.not(Shape::isStringShape))
|
||||
.map(resolved -> error(shape, format(
|
||||
"Map key member targets %s, but is expected to target a string", resolved)));
|
||||
private void validateDeprecatedTargets(
|
||||
Shape shape,
|
||||
Shape target,
|
||||
RelationshipType relType,
|
||||
List<ValidationEvent> events
|
||||
) {
|
||||
if (!target.hasTrait(DeprecatedTrait.class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String relLabel = RELATIONSHIP_TYPE_DEPRECATION_MAPPINGS.get(relType);
|
||||
if (relLabel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder(relLabel).append(", ").append(target.getId());
|
||||
DeprecatedTrait deprecatedTrait = target.expectTrait(DeprecatedTrait.class);
|
||||
deprecatedTrait.getMessage().ifPresent(message -> builder.append(". ").append(message));
|
||||
deprecatedTrait.getSince().ifPresent(since -> builder.append(" (since ").append(since).append(')'));
|
||||
events.add(ValidationEvent.builder()
|
||||
.id("DeprecatedShape")
|
||||
.severity(Severity.WARNING)
|
||||
.shape(shape)
|
||||
.message(builder.toString())
|
||||
.build());
|
||||
}
|
||||
|
||||
private Optional<ValidationEvent> validateIdentifier(Shape shape, Shape target) {
|
||||
private void validateMapKey(Shape shape, ShapeId target, Model model, List<ValidationEvent> events) {
|
||||
model.getShape(target).filter(FunctionalUtils.not(Shape::isStringShape)).ifPresent(resolved -> {
|
||||
String message = format("Map key member targets %s, but is expected to target a string", resolved);
|
||||
events.add(error(shape, message));
|
||||
});
|
||||
}
|
||||
|
||||
private void validateIdentifier(Shape shape, Shape target, List<ValidationEvent> events) {
|
||||
if (target.getType() != ShapeType.STRING) {
|
||||
return Optional.of(badType(shape, target, RelationshipType.IDENTIFIER, ShapeType.STRING));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
events.add(badType(shape, target, RelationshipType.IDENTIFIER, ShapeType.STRING));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,28 +2,7 @@ $version: "2.0"
|
|||
|
||||
namespace smithy.api
|
||||
|
||||
@deprecated
|
||||
boolean PrimitiveBoolean
|
||||
|
||||
@deprecated
|
||||
byte PrimitiveByte
|
||||
|
||||
@deprecated
|
||||
short PrimitiveShort
|
||||
|
||||
@deprecated
|
||||
integer PrimitiveInteger
|
||||
|
||||
@deprecated
|
||||
long PrimitiveLong
|
||||
|
||||
@deprecated
|
||||
float PrimitiveFloat
|
||||
|
||||
@deprecated
|
||||
double PrimitiveDouble
|
||||
|
||||
/// Indicates that a shape is boxed.
|
||||
/// Used in Smithy 1.0 to indicate that a shape is boxed.
|
||||
///
|
||||
/// When a boxed shape is the target of a member, the member
|
||||
/// may or may not contain a value, and the member has no default value.
|
||||
|
|
|
@ -31,6 +31,48 @@ double Double
|
|||
@unitType
|
||||
structure Unit {}
|
||||
|
||||
@deprecated(
|
||||
message: "Use Boolean instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
boolean PrimitiveBoolean
|
||||
|
||||
@deprecated(
|
||||
message: "Use Byte instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
byte PrimitiveByte
|
||||
|
||||
@deprecated(
|
||||
message: "Use Short instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
short PrimitiveShort
|
||||
|
||||
@deprecated(
|
||||
message: "Use Integer instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
integer PrimitiveInteger
|
||||
|
||||
@deprecated(
|
||||
message: "Use Long instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
long PrimitiveLong
|
||||
|
||||
@deprecated(
|
||||
message: "Use Float instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
float PrimitiveFloat
|
||||
|
||||
@deprecated(
|
||||
message: "Use Double instead, and add the @default trait to structure members that targets this shape",
|
||||
since: "2.0"
|
||||
)
|
||||
double PrimitiveDouble
|
||||
|
||||
/// Makes a shape a trait.
|
||||
@trait(
|
||||
selector: ":is(simpleType, list, map, structure, union)",
|
||||
|
|
|
@ -231,21 +231,6 @@ public class NullableIndexTest {
|
|||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detectsPreviousPrimitivePreludeShapes() {
|
||||
IntegerShape integer = IntegerShape.builder()
|
||||
.id("smithy.api#PrimitiveInteger")
|
||||
.build();
|
||||
StructureShape struct = StructureShape.builder()
|
||||
.id("smithy.example#Struct")
|
||||
.addMember("foo", integer.getId())
|
||||
.build();
|
||||
Model model = Model.builder().addShapes(integer, struct).build();
|
||||
NullableIndex index = NullableIndex.of(model);
|
||||
|
||||
assertThat(index.isMemberNullable(struct.getMember("foo").get()), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worksWithV1NullabilityRulesForInteger() {
|
||||
// In Smithy v1, integer was non-nullable by default.
|
||||
|
|
|
@ -41,9 +41,9 @@ public class ModelUpgraderTest {
|
|||
assertThat(ShapeId.from("smithy.example#Shorts$nullable2"), ShapeMatcher.memberIsNullable(result));
|
||||
assertThat(ShapeId.from("smithy.example#Integers$nullable2"), ShapeMatcher.memberIsNullable(result));
|
||||
|
||||
assertThat(ShapeId.from("smithy.example#Bytes$nullable2"), changedMemberTarget(result, "Byte"));
|
||||
assertThat(ShapeId.from("smithy.example#Shorts$nullable2"), changedMemberTarget(result, "Short"));
|
||||
assertThat(ShapeId.from("smithy.example#Integers$nullable2"), changedMemberTarget(result, "Integer"));
|
||||
assertThat(ShapeId.from("smithy.example#Bytes$nullable2"), targetsShape(result, "PrimitiveByte"));
|
||||
assertThat(ShapeId.from("smithy.example#Shorts$nullable2"), targetsShape(result, "PrimitiveShort"));
|
||||
assertThat(ShapeId.from("smithy.example#Integers$nullable2"), targetsShape(result, "PrimitiveInteger"));
|
||||
|
||||
assertThat(ShapeId.from("smithy.example#Bytes$nonNull"), not(ShapeMatcher.memberIsNullable(result)));
|
||||
assertThat(ShapeId.from("smithy.example#Shorts$nonNull"), not(ShapeMatcher.memberIsNullable(result)));
|
||||
|
@ -57,7 +57,9 @@ public class ModelUpgraderTest {
|
|||
UpgradeTestCase testCase = UpgradeTestCase.createAndValidate("upgrade/mixed-versions");
|
||||
ValidatedResult<Model> result = testCase.actualModel;
|
||||
|
||||
assertThat(ShapeId.from("smithy.example#Foo$number"), changedMemberTarget(result, "Integer"));
|
||||
// We don't rewrite or mess with Primitive* shape references. (a previous iteration of IDL 2.0
|
||||
// attempted to do shape rewrites, but it ended up causing issues with older models).
|
||||
assertThat(ShapeId.from("smithy.example#Foo$number"), targetsShape(result, "PrimitiveInteger"));
|
||||
assertThat(ShapeId.from("smithy.example#Foo$number"), addedDefaultTrait(result));
|
||||
}
|
||||
|
||||
|
@ -66,13 +68,13 @@ public class ModelUpgraderTest {
|
|||
UpgradeTestCase testCase = UpgradeTestCase.createAndValidate("upgrade/primitives-in-v2");
|
||||
ValidatedResult<Model> result = testCase.actualModel;
|
||||
|
||||
assertThat(ShapeId.from("smithy.example#Bad$boolean"), shapeTargetsInvalidPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$byte"), shapeTargetsInvalidPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$short"), shapeTargetsInvalidPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$integer"), shapeTargetsInvalidPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$long"), shapeTargetsInvalidPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$float"), shapeTargetsInvalidPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$double"), shapeTargetsInvalidPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$boolean"), shapeTargetsDeprecatedPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$byte"), shapeTargetsDeprecatedPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$short"), shapeTargetsDeprecatedPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$integer"), shapeTargetsDeprecatedPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$long"), shapeTargetsDeprecatedPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$float"), shapeTargetsDeprecatedPrimitive(result));
|
||||
assertThat(ShapeId.from("smithy.example#Bad$double"), shapeTargetsDeprecatedPrimitive(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -88,9 +90,9 @@ public class ModelUpgraderTest {
|
|||
UpgradeTestCase testCase = UpgradeTestCase.createAndValidate("upgrade/does-not-introduce-conflict");
|
||||
ValidatedResult<Model> result = testCase.actualModel;
|
||||
|
||||
assertThat(ShapeId.from("smithy.example#Foo$alreadyDefault"), changedMemberTarget(result, "Integer"));
|
||||
assertThat(ShapeId.from("smithy.example#Foo$alreadyRequired"), changedMemberTarget(result, "Integer"));
|
||||
assertThat(ShapeId.from("smithy.example#Foo$boxedMember"), changedMemberTarget(result, "Integer"));
|
||||
assertThat(ShapeId.from("smithy.example#Foo$alreadyDefault"), targetsShape(result, "PrimitiveInteger"));
|
||||
assertThat(ShapeId.from("smithy.example#Foo$alreadyRequired"), targetsShape(result, "PrimitiveInteger"));
|
||||
assertThat(ShapeId.from("smithy.example#Foo$boxedMember"), targetsShape(result, "PrimitiveInteger"));
|
||||
assertThat(ShapeId.from("smithy.example#Foo$boxedMember"), not(addedDefaultTrait(result)));
|
||||
assertThat(ShapeId.from("smithy.example#Foo$previouslyBoxedTarget"), not(addedDefaultTrait(result)));
|
||||
assertThat(ShapeId.from("smithy.example#Foo$explicitlyBoxedTarget"), not(addedDefaultTrait(result)));
|
||||
|
@ -159,12 +161,12 @@ public class ModelUpgraderTest {
|
|||
}
|
||||
}
|
||||
|
||||
private static Matcher<ShapeId> changedMemberTarget(ValidatedResult<Model> result, String newShapeName) {
|
||||
private static Matcher<ShapeId> targetsShape(ValidatedResult<Model> result, String shapeName) {
|
||||
return ShapeMatcher.builderFor(MemberShape.class, result)
|
||||
.description("Changed member to target " + newShapeName + " with a warning")
|
||||
.addAssertion(member -> member.getTarget().equals(ShapeId.fromParts(Prelude.NAMESPACE, newShapeName)),
|
||||
.description("Targets " + shapeName)
|
||||
.addAssertion(member -> member.getTarget()
|
||||
.equals(ShapeId.fromOptionalNamespace(Prelude.NAMESPACE, shapeName)),
|
||||
member -> "targeted " + member.getTarget())
|
||||
.addEventAssertion(Validator.MODEL_DEPRECATION, Severity.WARNING, newShapeName + " instead")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -177,13 +179,14 @@ public class ModelUpgraderTest {
|
|||
.build();
|
||||
}
|
||||
|
||||
private static Matcher<ShapeId> shapeTargetsInvalidPrimitive(ValidatedResult<Model> result) {
|
||||
private static Matcher<ShapeId> shapeTargetsDeprecatedPrimitive(ValidatedResult<Model> result) {
|
||||
return ShapeMatcher.builderFor(MemberShape.class, result)
|
||||
.description("shape targets a removed primitive shape")
|
||||
.description("shape targets a deprecated primitive shape and emits a warning")
|
||||
.addAssertion(member -> member.getTarget().getName().startsWith("Primitive")
|
||||
&& member.getTarget().getNamespace().equals(Prelude.NAMESPACE),
|
||||
member -> "member does not target a primitive prelude shape")
|
||||
.addEventAssertion(Validator.MODEL_DEPRECATION, Severity.ERROR, "removed in Smithy IDL 2.0")
|
||||
.addEventAssertion("DeprecatedShape", Severity.WARNING,
|
||||
"and add the @default trait to structure members that targets this shape")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
[WARNING] -: Smithy IDL 1.0 is deprecated. Please upgrade to 2.0. | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$noRange: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$noRange: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$valid: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$valid: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$invalidMin: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$invalidMin: Cannot add the @default trait to this member due to a minimum range constraint. | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$invalidMin: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$invalidMax: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$invalidMax: Cannot add the @default trait to this member due to a maximum range constraint. | ModelDeprecation
|
||||
[WARNING] smithy.example#Integers$invalidMax: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[WARNING] smithy.example#Test$foo1: Member targets a deprecated shape, smithy.example#Foo1 | DeprecatedShape
|
||||
[WARNING] smithy.example#Test$foo2: Member targets a deprecated shape, smithy.example#Foo2 (since 2.0) | DeprecatedShape
|
||||
[WARNING] smithy.example#Test$foo3: Member targets a deprecated shape, smithy.example#Foo3. hello (since 2.0) | DeprecatedShape
|
||||
[WARNING] smithy.example#Test: Applies a deprecated mixin, smithy.example#DeprecatedMixin | DeprecatedShape
|
|
@ -0,0 +1,22 @@
|
|||
$version: "2.0"
|
||||
|
||||
namespace smithy.example
|
||||
|
||||
@deprecated
|
||||
string Foo1
|
||||
|
||||
@deprecated(since: "2.0")
|
||||
string Foo2
|
||||
|
||||
@deprecated(message: "hello", since: "2.0")
|
||||
string Foo3
|
||||
|
||||
@mixin
|
||||
@deprecated
|
||||
structure DeprecatedMixin {}
|
||||
|
||||
structure Test with [DeprecatedMixin] {
|
||||
foo1: Foo1,
|
||||
foo2: Foo2,
|
||||
foo3: Foo3
|
||||
}
|
|
@ -6,31 +6,31 @@ structure Bytes {
|
|||
nullable: Byte,
|
||||
|
||||
@default(0)
|
||||
nonNull: Byte,
|
||||
nonNull: PrimitiveByte,
|
||||
|
||||
nullable2: Byte,
|
||||
nullable2: PrimitiveByte,
|
||||
}
|
||||
|
||||
structure Shorts {
|
||||
nullable: Short,
|
||||
|
||||
@default(0)
|
||||
nonNull: Short,
|
||||
nonNull: PrimitiveShort,
|
||||
|
||||
nullable2: Short,
|
||||
nullable2: PrimitiveShort,
|
||||
}
|
||||
|
||||
structure Integers {
|
||||
nullable: Integer,
|
||||
|
||||
@default(0)
|
||||
nonNull: Integer,
|
||||
nonNull: PrimitiveInteger,
|
||||
|
||||
@default(0)
|
||||
@range(min: 0, max: 1)
|
||||
ranged: Integer,
|
||||
ranged: PrimitiveInteger,
|
||||
|
||||
nullable2: Integer
|
||||
nullable2: PrimitiveInteger
|
||||
}
|
||||
|
||||
structure BlobPayload {
|
||||
|
|
|
@ -4,12 +4,12 @@ namespace smithy.example
|
|||
|
||||
structure Foo {
|
||||
@default(0)
|
||||
alreadyDefault: Integer,
|
||||
alreadyDefault: PrimitiveInteger,
|
||||
|
||||
@required
|
||||
alreadyRequired: Integer,
|
||||
alreadyRequired: PrimitiveInteger,
|
||||
|
||||
boxedMember: Integer,
|
||||
boxedMember: PrimitiveInteger,
|
||||
|
||||
previouslyBoxedTarget: Integer,
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace smithy.example
|
|||
|
||||
structure Foo {
|
||||
@default(0)
|
||||
number: Integer
|
||||
number: PrimitiveInteger
|
||||
}
|
||||
|
||||
structure Baz {
|
||||
|
|
Loading…
Reference in New Issue