Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle unknown model versions for built-shapes #1312

Merged
merged 1 commit into from
Jul 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@ final class FullyResolvedModelFile extends AbstractMutableModelFile {
* @return Returns the create {@code FullyResolvedModelFile} containing the shapes.
*/
static FullyResolvedModelFile fromShapes(TraitFactory traitFactory, Collection<Shape> shapes) {
return fromShapes(traitFactory, shapes, null);
return fromShapes(traitFactory, shapes, Version.UNKNOWN);
}


/**
* Create a {@code FullyResolvedModelFile} from already built shapes.
*
Expand All @@ -67,9 +66,7 @@ static FullyResolvedModelFile fromShapes(TraitFactory traitFactory, Collection<S
FullyResolvedModelFile modelFile = new FullyResolvedModelFile(
SourceLocation.none().getFilename(), traitFactory);

if (version != null) {
modelFile.setVersion(version);
}
modelFile.setVersion(version);

for (Shape shape : shapes) {
// Convert the shape to a builder and remove all the traits.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ public final class ModelAssembler {
private final Map<String, Object> properties = new HashMap<>();
private boolean disablePrelude;
private Consumer<ValidationEvent> validationEventListener = DEFAULT_EVENT_LISTENER;
private Version parsedShapesVersion;

// Lazy initialization holder class idiom to hold a default trait factory.
static final class LazyTraitFactoryHolder {
Expand Down Expand Up @@ -368,20 +367,6 @@ public ModelAssembler addShapes(Shape... shapes) {
return this;
}

/**
* Sets the Smithy version to use for parsed shapes added directly to the
* assembler.
*
* If unset, the default version of 1.0 will be assumed.
*
* @param version A Smithy IDL version.
* @return Returns the assembler.
*/
public ModelAssembler setParsedShapesVersion(String version) {
this.parsedShapesVersion = Version.fromString(version);
return this;
}

/**
* Explicitly adds a trait to a shape in the assembled model.
*
Expand Down Expand Up @@ -632,8 +617,7 @@ private List<ModelFile> createModelFiles() {
}

// A modelFile is created for the assembler to capture anything that was manually added.
FullyResolvedModelFile assemblerModelFile = FullyResolvedModelFile.fromShapes(
traitFactory, shapes, parsedShapesVersion);
FullyResolvedModelFile assemblerModelFile = FullyResolvedModelFile.fromShapes(traitFactory, shapes);

modelFiles.add(assemblerModelFile);
metadata.forEach(assemblerModelFile::putMetadata);
Expand All @@ -648,11 +632,7 @@ private List<ModelFile> createModelFiles() {
List<Shape> nonPrelude = model.shapes()
.filter(FunctionalUtils.not(Prelude::isPreludeShape))
.collect(Collectors.toList());
// Since we're pulling from a loaded model, we know that it has been converted to the latest
// supported version. We include that here to ensure we don't hit any validation issues from
// using new features.
FullyResolvedModelFile resolvedFile = FullyResolvedModelFile.fromShapes(
traitFactory, nonPrelude, Version.fromString(Model.MODEL_VERSION));
FullyResolvedModelFile resolvedFile = FullyResolvedModelFile.fromShapes(traitFactory, nonPrelude);
model.getMetadata().forEach(resolvedFile::putMetadata);
modelFiles.add(resolvedFile);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,16 @@ ValidatedResult<Model> transform() {
// We must assume v2 for manually created shapes.
Version version = fileToVersion.getOrDefault(member.getSourceLocation().getFilename(),
Version.VERSION_2_0);
if (version == Version.VERSION_1_0) {

if (version == Version.VERSION_2_0) {
validateV2Member(member);
} else {
// Also attempt to upgrade unknown versions since they could be 1.0 and
// trying to upgrade 2.0 shapes has no effect.
// For v1 shape checks, we need to know the containing shape type to apply the appropriate transform.
model.getShape(member.getContainer()).ifPresent(container -> {
upgradeV1Member(container.getType(), member);
});
} else if (version == Version.VERSION_2_0) {
validateV2Member(member);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* 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.
Expand All @@ -25,15 +25,121 @@
*/
enum Version {

UNKNOWN(""),
VERSION_1_0("1.0"),
VERSION_2_0("2.0");
/**
* Unknown is used for in-memory models that aren't tied to a specific version.
* For these kinds of models, we just assume every feature is supported.
*
* <p>When loading IDL models with no $version specified, the default assumed
* version is 1.0, not UNKNOWN (see {@link TraitContainer.VersionAwareTraitContainer}).
*/
UNKNOWN {
@Override
public String toString() {
return "";
}

private final String version;
@Override
boolean supportsMixins() {
return true;
}

Version(String version) {
this.version = version;
}
@Override
boolean supportsInlineOperationIO() {
return true;
}

@Override
boolean supportsTargetElision() {
return true;
}

@Override
boolean isDefaultSupported() {
return true;
}

@Override
boolean isShapeTypeSupported(ShapeType shapeType) {
return true;
}
},

VERSION_1_0 {
@Override
public String toString() {
return "1.0";
}

@Override
boolean supportsMixins() {
return false;
}

@Override
boolean supportsInlineOperationIO() {
return false;
}

@Override
boolean supportsTargetElision() {
return false;
}

@Override
boolean isDefaultSupported() {
return false;
}

@Override
boolean isShapeTypeSupported(ShapeType shapeType) {
return shapeType != ShapeType.ENUM && shapeType != ShapeType.INT_ENUM;
}

@Override
void validateVersionedTrait(ShapeId target, ShapeId traitId, Node value) {
if (traitId.equals(MixinTrait.ID)) {
throw ModelSyntaxException.builder()
.message(String.format("Mixins can only be used in Smithy 2.0 or later. Attempted to apply "
+ "a @mixin trait to `%s` in a model file using version `%s`.",
target, this))
.shapeId(target)
.sourceLocation(value)
.build();
}
}
},

VERSION_2_0 {
@Override
public String toString() {
return "2.0";
}

@Override
boolean supportsMixins() {
return true;
}

@Override
boolean supportsInlineOperationIO() {
return true;
}

@Override
boolean supportsTargetElision() {
return true;
}

@Override
boolean isDefaultSupported() {
return true;
}

@Override
boolean isShapeTypeSupported(ShapeType shapeType) {
return shapeType != ShapeType.SET;
}
};

/**
* Creates a Version from a string, or returns null if the version
Expand All @@ -55,56 +161,41 @@ static Version fromString(String value) {
}
}

@Override
public String toString() {
return version;
}

/**
* Checks if this version of the IDL supports mixins.
*
* @return Returns true if this version supports mixins.
*/
boolean supportsMixins() {
return this == VERSION_2_0;
}
abstract boolean supportsMixins();

/**
* Checks if this version of the IDL supports inlined operation IO shapes.
*
* @return Returns true if this version supports inlined operation IO shapes.
*/
boolean supportsInlineOperationIO() {
return this == VERSION_2_0;
}
abstract boolean supportsInlineOperationIO();

/**
* Checks if this version of the IDL supports eliding targets for structures
* with mixins or structures bound to resources.
*
* @return Returns true if the version supports eliding targets.
*/
boolean supportsTargetElision() {
return this == VERSION_2_0;
}
abstract boolean supportsTargetElision();

/**
* Checks if the default trait is supported.
* @return Returns true if supported (i.e., IDL 2.0 or UNKNOWN).
*/
abstract boolean isDefaultSupported();

/**
* Checks if the given shape type is supported in this version.
*
* @param shapeType The shape type to check.
* @return Returns true if the shape type is supported in this version.
*/
boolean isShapeTypeSupported(ShapeType shapeType) {
switch (shapeType) {
case SET:
return this == VERSION_1_0;
case ENUM:
case INT_ENUM:
return this == VERSION_2_0;
default:
return true;
}
}
abstract boolean isShapeTypeSupported(ShapeType shapeType);

/**
* Perform version-specific trait validation.
Expand All @@ -115,18 +206,5 @@ boolean isShapeTypeSupported(ShapeType shapeType) {
* @throws ModelSyntaxException if the given trait cannot be used in this version.
*/
void validateVersionedTrait(ShapeId target, ShapeId traitId, Node value) {
if (traitId.equals(MixinTrait.ID) && (this != Version.VERSION_2_0)) {
throw ModelSyntaxException.builder()
.message(String.format("Mixins can only be used in Smithy 2.0 or later. Attempted to apply "
+ "a @mixin trait to `%s` in a model file using version `%s`.",
target, version))
.shapeId(target)
.sourceLocation(value)
.build();
}
}

boolean isDefaultSupported() {
return this == VERSION_2_0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.SetShape;
import software.amazon.smithy.model.shapes.SetShapeTest;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.ShapeType;
Expand Down Expand Up @@ -749,4 +751,18 @@ public void failsWhenMixinsConflictAndAreNotEquivalent() {

assertThat(e.getMessage(), containsString("Conflicting shape definition for `smithy.example#A`"));
}

@Test
public void canLoadSetsUsingBuiltModel() {
SetShape set = SetShape.builder()
.id("smithy.example#Set")
.member(ShapeId.from("smithy.api#String"))
.build();
Model model = Model.assembler()
.addShape(set)
.assemble()
.unwrap();

Model.assembler().addModel(model).assemble().unwrap();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ public void canConvertEnumToString() {
.build();

Model model = Model.assembler()
.setParsedShapesVersion("2.0")
.addShape(startShape)
.assemble()
.unwrap();
Expand Down Expand Up @@ -431,7 +430,6 @@ public void canDowngradeEnums() {
IntEnumShape intEnum = intEnumBuilder.addMember("FOO", 1).build();

Model model = Model.assembler()
.setParsedShapesVersion("2.0")
.addShapes(stringEnum, intEnum)
.assemble().unwrap();
Model result = ModelTransformer.create().downgradeEnums(model);
Expand Down