Explicitly model and constrain members

Members of list, map, union, and structure are now explicitly
modeled rather than relying on a generic grammar for all members.

Two new constraints:
1. Map keys, if defined, must be defined before map values.
2. Members that are not elided must be on the same line.
   `foo:\nBar` is no longer valid.
This commit is contained in:
Michael Dowling 2022-08-04 12:57:43 -07:00 committed by Michael Dowling
parent c3ddd3b25a
commit 53c7c77021
38 changed files with 376 additions and 364 deletions

View File

@ -100,7 +100,7 @@ string support defined in `RFC 5234 <https://www.rfc-editor.org/rfc/rfc7405>`_.
WS :1*(`SP` / `NL` / `Comment` / ",") ; whitespace
SP :1*(%x20 / %x09) ; one or more spaces or tabs
NL :%x0A / %x0D.0A ; Newline: \n and \r\n
NotNL: %x09 / %x20-10FFFF ; Any character except newline
NotNL:%x09 / %x20-10FFFF ; Any character except newline
BR :*`SP` 1*(`Comment` / `NL`) *`WS`; line break followed by whitespace
.. rubric:: Comments
@ -166,58 +166,71 @@ string support defined in `RFC 5234 <https://www.rfc-editor.org/rfc/rfc7405>`_.
.. rubric:: Shapes
.. productionlist:: smithy
ShapeSection :[`NamespaceStatement` `UseSection` `ShapeStatements`]
NamespaceStatement :%s"namespace" `SP` `Namespace` `BR`
UseSection :*(`UseStatement`)
UseStatement :%s"use" `SP` `AbsoluteRootShapeId` `BR`
ShapeStatements :*(`ShapeStatement` / `ApplyStatement`)
ShapeStatement :`TraitStatements` `ShapeBody` `BR`
ShapeBody :`SimpleShapeStatement`
:/ `EnumShapeStatement`
:/ `ListStatement`
:/ `MapStatement`
:/ `StructureStatement`
:/ `UnionStatement`
:/ `ServiceStatement`
:/ `OperationStatement`
:/ `ResourceStatement`
SimpleShapeStatement :`SimpleTypeName` `SP` `Identifier` [`Mixins`]
SimpleTypeName :%s"blob" / %s"boolean" / %s"document" / %s"string"
:/ %s"byte" / %s"short" / %s"integer" / %s"long"
:/ %s"float" / %s"double" / %s"bigInteger"
:/ %s"bigDecimal" / %s"timestamp"
Mixins :*`SP` %s"with" *`WS` "[" 1*(*`WS` `ShapeId`) *`WS` "]"
EnumShapeStatement :`EnumTypeName` `SP` `Identifier` [`Mixins`] *`WS` `EnumShapeMembers`
EnumTypeName :%s"enum" / %s"intEnum"
EnumShapeMembers :"{" *`WS` 1*(`TraitStatements` `Identifier` [`ValueAssignment`] `*WS`) "}"
ValueAssignment :*`SP` "=" *`SP` `NodeValue` `BR`
ShapeMembers :"{" *`WS` *(`TraitStatements` `ShapeMember` *`WS`) "}"
ShapeMember :(`ShapeMemberKvp` / `ShapeMemberElided`) [`ValueAssignment`]
ShapeMemberKvp :`Identifier` *`WS` ":" *`WS` `ShapeId`
ShapeMemberElided :"$" `Identifier`
ListStatement :%s"list" `SP` `Identifier` [`Mixins`] *`WS` `ShapeMembers`
MapStatement :%s"map" `SP` `Identifier` [`Mixins`] *`WS` `ShapeMembers`
StructureStatement :%s"structure" `SP` `Identifier` [`StructureResource`]
: [`Mixins`] *`WS` `ShapeMembers`
StructureResource :`SP` %s"for" `SP` `ShapeId`
UnionStatement :%s"union" `SP` `Identifier` [`Mixins`] *`WS` `ShapeMembers`
ServiceStatement :%s"service" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject`
ResourceStatement :%s"resource" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject`
OperationStatement :%s"operation" `SP` `Identifier` [`Mixins`] *`WS` `OperationBody`
OperationBody :"{" *`WS`
: [`OperationInput`]
: [`OperationOutput`]
: [`OperationErrors`]
: *`WS` "}"
OperationInput :%s"input" *WS (`InlineStructure` / `Identifier`) `BR`
OperationOutput :%s"output" *WS (`InlineStructure` / `ShapeId`) `BR`
OperationErrors :%s"errors" *WS ":" *WS "[" *(*`WS` `Identifier`) *`WS` "]" `BR`
InlineStructure :":=" *`WS` `TraitStatements` [`Mixins`] *`WS` `ShapeMembers`
ShapeSection :[`NamespaceStatement` `UseSection` `ShapeStatements`]
NamespaceStatement :%s"namespace" `SP` `Namespace` `BR`
UseSection :*(`UseStatement`)
UseStatement :%s"use" `SP` `AbsoluteRootShapeId` `BR`
ShapeStatements :*(`ShapeStatement` / `ApplyStatement`)
ShapeStatement :`TraitStatements` `ShapeBody` `BR`
ShapeBody :`SimpleShapeStatement`
:/ `EnumShapeStatement`
:/ `ListStatement`
:/ `MapStatement`
:/ `StructureStatement`
:/ `UnionStatement`
:/ `ServiceStatement`
:/ `OperationStatement`
:/ `ResourceStatement`
SimpleShapeStatement :`SimpleTypeName` `SP` `Identifier` [`Mixins`]
SimpleTypeName :%s"blob" / %s"boolean" / %s"document" / %s"string"
:/ %s"byte" / %s"short" / %s"integer" / %s"long"
:/ %s"float" / %s"double" / %s"bigInteger"
:/ %s"bigDecimal" / %s"timestamp"
Mixins :*`SP` %s"with" *`WS` "[" 1*(*`WS` `ShapeId`) *`WS` "]"
EnumShapeStatement :`EnumTypeName` `SP` `Identifier` [`Mixins`] *`WS` `EnumShapeMembers`
EnumTypeName :%s"enum" / %s"intEnum"
EnumShapeMembers :"{" *`WS` 1*(`TraitStatements` `Identifier` [`ValueAssignment`] `*WS`) "}"
ValueAssignment :*`SP` "=" *`SP` `NodeValue` `BR`
ListStatement :%s"list" `SP` `Identifier` [`Mixins`] *`WS` `ListMembers`
ListMembers :"{" *`WS` `ListMember` *`WS` "}"
ListMember :[TraitStatements] (`ElidedListMember` / `ExplicitListMember`)
ElidedListMember :%s"$member"
ExplicitListMember :%s"member" *`SP` ":" *`SP` `ShapeId`
MapStatement :%s"map" `SP` `Identifier` [`Mixins`] *`WS` `MapMembers`
MapMembers :"{" *`WS` `MapKey` `BR` `MapValue` *`WS` "}"
MapKey :[TraitStatements] (`ElidedMapKey` / `ExplicitMapKey`)
MapValue :[TraitStatements] (`ElidedMapValue` / `ExplicitMapValue`)
ElidedMapKey :%s"$key"
ExplicitMapKey :%s"key" *`SP` ":" *`SP` `ShapeId`
ElidedMapValue :%s"$value"
ExplicitMapValue :%s"value" *`SP` ":" *`SP` `ShapeId`
StructureStatement :%s"structure" `SP` `Identifier` [`StructureResource`]
: [`Mixins`] *`WS` `StructureMembers`
StructureResource :`SP` %s"for" `SP` `ShapeId`
StructureMembers :"{" *`WS` *(`TraitStatements` `StructureMember` *`WS`) "}"
StructureMember :(`ExplicitStructureMember` / `ElidedStructureMember`) [`ValueAssignment`]
ExplicitStructureMember :`Identifier` *`SP` ":" *`SP` `ShapeId`
ElidedStructureMember :"$" `Identifier`
UnionStatement :%s"union" `SP` `Identifier` [`Mixins`] *`WS` `UnionMembers`
UnionMembers :"{" *`WS` *(`TraitStatements` `UnionMember` *`WS`) "}"
UnionMember :(`ExplicitStructureMember` / `ElidedStructureMember`)
ServiceStatement :%s"service" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject`
ResourceStatement :%s"resource" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject`
OperationStatement :%s"operation" `SP` `Identifier` [`Mixins`] *`WS` `OperationBody`
OperationBody :"{" *`WS`
: [`OperationInput`]
: [`OperationOutput`]
: [`OperationErrors`]
: *`WS` "}"
OperationInput :%s"input" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `BR`
OperationOutput :%s"output" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `BR`
OperationErrors :%s"errors" *WS ":" *WS "[" *(*`WS` `Identifier`) *`WS` "]" `BR`
InlineStructure :":=" *`WS` `TraitStatements` [`Mixins`] *`WS` `StructureMembers`
.. rubric:: Traits
.. productionlist:: smithy
TraitStatements : *(*`WS` `Trait`) *`WS`
TraitStatements :*(*`WS` `Trait`) *`WS`
Trait :"@" `ShapeId` [`TraitBody`]
TraitBody :"(" *`WS` [`TraitBodyValue`] *`WS` ")"
TraitBodyValue :`TraitStructure` / `NodeValue`
@ -1491,12 +1504,11 @@ Target Elision
Having to completely redefine a :ref:`resource identifier <resource-identifiers>`
to use it in a structure or redefine a member from a :ref:`mixin <mixins>` to add
additional traits can be cumbersome and potentially error-prone. The
:token:`type elision syntax <smithy:ShapeMemberElided>` can be used to cut
down on that repetition by prefixing the member name with a ``$``. If a member
is prefixed this way, its target will automatically be set to the target of a
mixin member with the same name. The following example shows how to elide the
target for a member inherited from a mixin:
additional traits can be cumbersome and potentially error-prone. Target elision
syntax can be used to cut down on that repetition by prefixing the member name
with a ``$``. If a member is prefixed this way, its target will automatically be
set to the target of a mixin member with the same name. The following example
shows how to elide the target for a member inherited from a mixin:
.. code-block:: smithy

View File

@ -549,7 +549,7 @@ Shape IDs are formally defined by the following ABNF:
RootShapeId :`AbsoluteRootShapeId` / `Identifier`
AbsoluteRootShapeId :`Namespace` "#" `Identifier`
Namespace :`Identifier` *("." `Identifier`)
Identifier :IdentifierStart *IdentifierChars
Identifier :`IdentifierStart` *`IdentifierChars`
IdentifierStart :*"_" ALPHA
IdentifierChars :ALPHA / DIGIT / "_"
ShapeIdMember :"$" `Identifier`

View File

@ -11,8 +11,7 @@ structure PrimitiveBearer {
long: PrimitiveLong,
short: PrimitiveShort,
handlesComments: // Nobody actually does this right?
PrimitiveShort,
handlesComments: PrimitiveShort, // comment
@required
handlesRequired: PrimitiveLong,

View File

@ -19,8 +19,7 @@ structure PrimitiveBearer {
short: PrimitiveShort,
@default(0)
handlesComments: // Nobody actually does this right?
PrimitiveShort,
handlesComments: PrimitiveShort, // comment
@required
handlesRequired: PrimitiveLong,

View File

@ -75,7 +75,6 @@ import software.amazon.smithy.model.validation.Severity;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.model.validation.Validator;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SetUtils;
import software.amazon.smithy.utils.SimpleParser;
import software.amazon.smithy.utils.StringUtils;
@ -233,7 +232,7 @@ final class IdlModelParser extends SimpleParser {
@Override
public void sp() {
while (!eof() && isSpaceOrComma(peek())) {
while (isSpaceOrComma(peek())) {
skip();
}
}
@ -246,7 +245,7 @@ final class IdlModelParser extends SimpleParser {
public void br() {
int line = line();
ws();
if (line == line() && peek() != Character.MIN_VALUE) {
if (line == line() && !eof()) {
throw syntax("Expected a line break");
}
}
@ -517,7 +516,7 @@ final class IdlModelParser extends SimpleParser {
parseSimpleShape(id, location, StringShape.builder());
break;
case "enum":
parseEnumShape(id, location, EnumShape.builder(), MemberParsing.PARSING_ENUM);
parseEnumShape(id, location, EnumShape.builder());
break;
case "blob":
parseSimpleShape(id, location, BlobShape.builder());
@ -532,7 +531,7 @@ final class IdlModelParser extends SimpleParser {
parseSimpleShape(id, location, IntegerShape.builder());
break;
case "intEnum":
parseEnumShape(id, location, IntEnumShape.builder(), MemberParsing.PARSING_INT_ENUM);
parseEnumShape(id, location, IntEnumShape.builder());
break;
case "long":
parseSimpleShape(id, location, LongShape.builder());
@ -585,15 +584,41 @@ final class IdlModelParser extends SimpleParser {
operations.accept(operation);
}
private void parseEnumShape(
ShapeId id,
SourceLocation location,
AbstractShapeBuilder<?, ?> builder,
MemberParsing memberParsing
) {
private void parseEnumShape(ShapeId id, SourceLocation location, AbstractShapeBuilder<?, ?> builder) {
LoadOperation.DefineShape operation = createShape(builder.id(id).source(location));
parseMixins(operation);
parseMembers(operation, Collections.emptySet(), memberParsing);
ws();
expect('{');
clearPendingDocs();
ws();
while (!eof() && peek() != '}') {
List<TraitEntry> memberTraits = parseDocsAndTraits();
SourceLocation memberLocation = currentLocation();
String memberName = ParserUtils.parseIdentifier(this);
MemberShape.Builder memberBuilder = MemberShape.builder()
.id(id.withMember(memberName))
.source(memberLocation)
.target(UnitTypeTrait.UNIT);
operation.addMember(memberBuilder);
addTraits(memberBuilder.getId(), memberTraits);
// Check for optional value assignment.
sp();
if (peek() == '=') {
expect('=');
sp();
Node value = IdlNodeParser.parseNode(this);
memberBuilder.addTrait(new EnumValueTrait.Provider().createTrait(memberBuilder.getId(), value));
br();
}
clearPendingDocs();
ws();
}
expect('}');
clearPendingDocs();
operations.accept(operation);
}
@ -603,223 +628,69 @@ final class IdlModelParser extends SimpleParser {
private void parseCollection(ShapeId id, SourceLocation location, CollectionShape.Builder<?, ?> builder) {
LoadOperation.DefineShape operation = createShape(builder.id(id).source(location));
parseMixins(operation);
parseMembers(operation, SetUtils.of("member"));
ws();
expect('{');
clearPendingDocs();
ws();
parsePossiblyElidedMember(operation, "member");
ws();
expect('}');
clearPendingDocs();
operations.accept(operation);
}
private void parseMembers(LoadOperation.DefineShape operation, Set<String> requiredMembers) {
parseMembers(operation, requiredMembers, MemberParsing.PARSING_MEMBER);
}
private enum MemberParsing {
PARSING_INT_ENUM {
@Override
boolean supportsAssignment() {
return true;
}
@Override
Trait createAssignmentTrait(ShapeId id, Node value) {
NumberNode number = value.asNumberNode().orElseThrow(() -> ModelSyntaxException.builder()
.shapeId(id)
.sourceLocation(value)
.message("intEnum shapes require integer values but found: " + Node.printJson(value))
.build());
if (number.isFloatingPointNumber()) {
throw ModelSyntaxException.builder()
.shapeId(id)
.message("intEnum shapes do not support floating point values: " + value)
.sourceLocation(value)
.build();
}
long longValue = number.getValue().longValue();
if (longValue > Integer.MAX_VALUE || longValue < Integer.MIN_VALUE) {
throw ModelSyntaxException.builder()
.shapeId(id)
.message("intEnum must fit within an integer, but found: " + longValue)
.sourceLocation(value)
.build();
}
return EnumValueTrait.builder()
.sourceLocation(value.getSourceLocation())
.intValue(number.getValue().intValue())
.build();
}
@Override
boolean targetsUnit() {
return true;
}
},
PARSING_ENUM {
@Override
boolean supportsAssignment() {
return true;
}
@Override
Trait createAssignmentTrait(ShapeId id, Node value) {
String stringValue = value.asStringNode().orElseThrow(() -> ModelSyntaxException.builder()
.shapeId(id)
.sourceLocation(value)
.message("enum shapes require string values but found: " + Node.printJson(value))
.build())
.getValue();
return EnumValueTrait.builder()
.sourceLocation(value.getSourceLocation())
.stringValue(stringValue)
.build();
}
@Override
boolean targetsUnit() {
return true;
}
},
PARSING_STRUCTURE_MEMBER {
@Override
boolean supportsAssignment() {
return true;
}
@Override
Trait createAssignmentTrait(ShapeId id, Node value) {
return new DefaultTrait(value);
}
@Override
boolean targetsUnit() {
return false;
}
},
PARSING_MEMBER {
@Override
boolean supportsAssignment() {
return false;
}
@Override
Trait createAssignmentTrait(ShapeId id, Node value) {
throw new UnsupportedOperationException();
}
@Override
boolean targetsUnit() {
return false;
}
};
abstract boolean supportsAssignment();
abstract Trait createAssignmentTrait(ShapeId id, Node value);
abstract boolean targetsUnit();
}
private void parseMembers(LoadOperation.DefineShape op, Set<String> requiredMembers, MemberParsing memberParsing) {
Set<String> definedMembers = new HashSet<>();
ws();
expect('{');
ws();
while (!eof()) {
if (peek() == '}') {
break;
}
parseMember(op, requiredMembers, definedMembers, memberParsing);
// Clears out any previously captured documentation
// comments that may have been found when parsing the member.
clearPendingDocs();
ws();
}
if (eof()) {
expect('}');
}
expect('}');
}
private void parseMember(
LoadOperation.DefineShape operation,
Set<String> allowed,
Set<String> defined,
MemberParsing memberParsing
) {
ShapeId parent = operation.toShapeId();
// Parse optional member traits.
// Parsed list, set, and map members.
private void parsePossiblyElidedMember(LoadOperation.DefineShape operation, String memberName) {
boolean isElided = false;
List<TraitEntry> memberTraits = parseDocsAndTraits();
SourceLocation memberLocation = currentLocation();
boolean isTargetElided = !memberParsing.targetsUnit() && peek() == '$';
if (isTargetElided) {
if (peek() == '$') {
isElided = true;
if (!modelVersion.supportsTargetElision()) {
throw syntax(operation.toShapeId().withMember(memberName),
"Members can only elide targets in IDL version 2 or later");
}
expect('$');
}
String memberName = ParserUtils.parseIdentifier(this);
if (defined.contains(memberName)) {
// This is a duplicate member name.
throw syntax(parent, "Duplicate member of " + parent + ": '" + memberName + '\'');
}
defined.add(memberName);
// Only enforce "allowedMembers" if it isn't empty.
if (!allowed.isEmpty() && !allowed.contains(memberName)) {
throw syntax(parent, "Unexpected member of " + parent + ": '" + memberName + '\'');
}
ShapeId memberId = parent.withMember(memberName);
if (isTargetElided && !modelVersion.supportsTargetElision()) {
throw syntax(memberId, "Members can only elide targets in IDL version 2 or later. "
+ "Attempted to elide a target with version `" + modelVersion + "`.");
}
MemberShape.Builder memberBuilder = MemberShape.builder().id(memberId).source(memberLocation);
// Members whose targets are elided will have those targets resolved later,
// for example by SetResourceBasedTargets
if (!isTargetElided) {
if (memberParsing.targetsUnit()) {
addForwardReference(UnitTypeTrait.UNIT.toString(), memberBuilder::target);
} else {
ws();
expect(':');
ws();
addForwardReference(ParserUtils.parseShapeId(this), memberBuilder::target);
} else if (peek() != memberName.charAt(0)) {
if (!memberTraits.isEmpty()) {
throw syntax("Expected member definition to follow traits");
}
return;
}
// Skip spaces to check if there is default trait sugar.
sp();
MemberShape.Builder memberBuilder = MemberShape.builder()
.id(operation.toShapeId().withMember(memberName))
.source(currentLocation());
if (memberParsing.supportsAssignment() && peek() == '=') {
if (!modelVersion.isDefaultSupported()) {
throw syntax("@default assignment is only supported in IDL version 2 or later");
}
expect('=');
for (int i = 0; i < memberName.length(); i++) {
expect(memberName.charAt(i));
}
if (!isElided) {
sp();
memberBuilder.addTrait(memberParsing.createAssignmentTrait(memberId, IdlNodeParser.parseNode(this)));
br();
expect(':');
sp();
addForwardReference(ParserUtils.parseShapeId(this), memberBuilder::target);
}
// Only add the member once fully parsed.
operation.addMember(memberBuilder);
addTraits(memberBuilder.getId(), memberTraits);
clearPendingDocs();
}
private void parseMapStatement(ShapeId id, SourceLocation location) {
LoadOperation.DefineShape operation = createShape(MapShape.builder().id(id).source(location));
parseMixins(operation);
parseMembers(operation, SetUtils.of("key", "value"));
ws();
expect('{');
clearPendingDocs();
ws();
parsePossiblyElidedMember(operation, "key");
ws();
parsePossiblyElidedMember(operation, "value");
ws();
expect('}');
clearPendingDocs();
operations.accept(operation);
}
@ -840,7 +711,7 @@ final class IdlModelParser extends SimpleParser {
// Parse optional "with" statements to add mixins, but only if it's supported by the version.
parseMixins(operation);
parseMembers(operation, Collections.emptySet(), memberParsing);
parseMembers(operation, memberParsing);
clearPendingDocs();
operations.accept(operation);
}
@ -878,6 +749,115 @@ final class IdlModelParser extends SimpleParser {
clearPendingDocs();
}
private enum MemberParsing {
PARSING_STRUCTURE_MEMBER {
@Override
boolean supportsAssignment() {
return true;
}
@Override
Trait createAssignmentTrait(ShapeId id, Node value) {
return new DefaultTrait(value);
}
},
PARSING_MEMBER {
@Override
boolean supportsAssignment() {
return false;
}
@Override
Trait createAssignmentTrait(ShapeId id, Node value) {
throw new UnsupportedOperationException();
}
};
abstract boolean supportsAssignment();
abstract Trait createAssignmentTrait(ShapeId id, Node value);
}
private void parseMembers(LoadOperation.DefineShape op, MemberParsing memberParsing) {
Set<String> definedMembers = new HashSet<>();
ws();
expect('{');
ws();
while (!eof()) {
if (peek() == '}') {
break;
}
parseMember(op, definedMembers, memberParsing);
// Clears out any previously captured documentation
// comments that may have been found when parsing the member.
clearPendingDocs();
ws();
}
expect('}');
}
private void parseMember(LoadOperation.DefineShape operation, Set<String> defined, MemberParsing memberParsing) {
ShapeId parent = operation.toShapeId();
// Parse optional member traits.
List<TraitEntry> memberTraits = parseDocsAndTraits();
SourceLocation memberLocation = currentLocation();
boolean isTargetElided = peek() == '$';
if (isTargetElided) {
expect('$');
}
String memberName = ParserUtils.parseIdentifier(this);
if (defined.contains(memberName)) {
// This is a duplicate member name.
throw syntax(parent, "Duplicate member of " + parent + ": '" + memberName + '\'');
}
defined.add(memberName);
ShapeId memberId = parent.withMember(memberName);
if (isTargetElided && !modelVersion.supportsTargetElision()) {
throw syntax(memberId, "Members can only elide targets in IDL version 2 or later");
}
MemberShape.Builder memberBuilder = MemberShape.builder().id(memberId).source(memberLocation);
// Members whose targets are elided will have those targets resolved later,
// for example by SetResourceBasedTargets
if (!isTargetElided) {
sp();
expect(':');
sp();
addForwardReference(ParserUtils.parseShapeId(this), memberBuilder::target);
}
// Skip spaces to check if there is default trait sugar.
sp();
if (memberParsing.supportsAssignment() && peek() == '=') {
if (!modelVersion.isDefaultSupported()) {
throw syntax("@default assignment is only supported in IDL version 2 or later");
}
expect('=');
sp();
memberBuilder.addTrait(memberParsing.createAssignmentTrait(memberId, IdlNodeParser.parseNode(this)));
br();
}
// Only add the member once fully parsed.
operation.addMember(memberBuilder);
addTraits(memberBuilder.getId(), memberTraits);
}
private void parseOperationStatement(ShapeId id, SourceLocation location) {
OperationShape.Builder builder = OperationShape.builder().id(id).source(location);
LoadOperation.DefineShape operation = createShape(builder);
@ -926,8 +906,6 @@ final class IdlModelParser extends SimpleParser {
parseIdList(builder::addError);
br();
expect('}');
} else if (next != '}') {
expect('}');
}
clearPendingDocs();
@ -966,7 +944,7 @@ final class IdlModelParser extends SimpleParser {
LoadOperation.DefineShape operation = createShape(builder);
parseMixins(operation);
parseForResource(operation);
parseMembers(operation, Collections.emptySet(), MemberParsing.PARSING_STRUCTURE_MEMBER);
parseMembers(operation, MemberParsing.PARSING_STRUCTURE_MEMBER);
addTraits(id, traits);
clearPendingDocs();
operations.accept(operation);

View File

@ -169,7 +169,7 @@ final class LoadOperationProcessor implements Consumer<LoadOperation> {
} else {
// Try to find a prelude shape by ID if no ID exists in the namespace with this name.
ShapeId preludeId = ShapeId.fromOptionalNamespace(Prelude.NAMESPACE, reference.name);
if (prelude.getShapeIds().contains(preludeId)) {
if (prelude != null && prelude.getShapeIds().contains(preludeId)) {
reference.resolve(preludeId, test -> prelude.expectShape(test).getType());
} else {
reference.resolve(inNamespace, test -> null);

View File

@ -16,7 +16,6 @@
package software.amazon.smithy.model.traits;
import java.util.Optional;
import software.amazon.smithy.model.SourceException;
import software.amazon.smithy.model.node.ExpectationNotMetException;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.NumberNode;
@ -31,19 +30,14 @@ import software.amazon.smithy.utils.ToSmithyBuilder;
public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuilder<EnumValueTrait> {
public static final ShapeId ID = ShapeId.from("smithy.api#enumValue");
private final String string;
private final Integer integer;
private final Node value;
private EnumValueTrait(Builder builder) {
super(ID, builder.sourceLocation);
string = builder.string;
integer = builder.integer;
if (string == null && integer == null) {
throw new SourceException(
"Either a string value or an integer value must be set for the enumValue trait.",
getSourceLocation()
);
if (builder.value == null) {
throw new IllegalStateException("No integer or string value set on EnumValueTrait");
}
value = builder.value;
}
/**
@ -52,7 +46,7 @@ public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuild
* @return Optionally returns the string value.
*/
public Optional<String> getStringValue() {
return Optional.ofNullable(string);
return value.asStringNode().map(StringNode::getValue);
}
/**
@ -73,7 +67,7 @@ public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuild
* @return Returns the set int value.
*/
public Optional<Integer> getIntValue() {
return Optional.ofNullable(integer);
return value.asNumberNode().map(NumberNode::getValue).map(Number::intValue);
}
/**
@ -96,41 +90,20 @@ public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuild
@Override
public Trait createTrait(ShapeId target, Node value) {
Builder builder = builder().sourceLocation(value);
value.asStringNode().ifPresent(node -> builder.stringValue(node.getValue()));
value.asNumberNode().ifPresent(node -> {
if (node.isNaturalNumber()) {
builder.intValue(node.getValue().intValue());
} else {
throw new SourceException(
"Enum values may not use fractional numbers.",
value.getSourceLocation()
);
}
});
EnumValueTrait result = builder.build();
result.setNodeCache(value);
return result;
builder.value = value;
return builder.build();
}
}
@Override
protected Node createNode() {
if (getIntValue().isPresent()) {
return new NumberNode(integer, getSourceLocation());
} else {
return new StringNode(string, getSourceLocation());
}
return value;
}
@Override
public SmithyBuilder<EnumValueTrait> toBuilder() {
Builder builder = builder().sourceLocation(getSourceLocation());
if (getIntValue().isPresent()) {
builder.intValue(getIntValue().get());
} else if (getStringValue().isPresent()) {
builder.stringValue(getStringValue().get());
}
builder.value = value;
return builder;
}
@ -139,8 +112,7 @@ public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuild
}
public static final class Builder extends AbstractTraitBuilder<EnumValueTrait, Builder> {
private String string;
private Integer integer;
private Node value;
@Override
public EnumValueTrait build() {
@ -148,14 +120,12 @@ public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuild
}
public Builder stringValue(String string) {
this.string = string;
this.integer = null;
this.value = Node.from(string);
return this;
}
public Builder intValue(int integer) {
this.integer = integer;
this.string = null;
this.value = Node.from(integer);
return this;
}
}

View File

@ -23,6 +23,8 @@ import java.util.Set;
import java.util.regex.Pattern;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.NumberNode;
import software.amazon.smithy.model.shapes.EnumShape;
import software.amazon.smithy.model.shapes.IntEnumShape;
import software.amazon.smithy.model.shapes.MemberShape;
@ -60,16 +62,16 @@ public final class EnumShapeValidator extends AbstractValidator {
private void validateEnumShape(List<ValidationEvent> events, EnumShape shape) {
Set<String> values = new HashSet<>();
for (MemberShape member : shape.members()) {
Optional<String> value = member.expectTrait(EnumValueTrait.class).getStringValue();
EnumValueTrait trait = member.expectTrait(EnumValueTrait.class);
Optional<String> value = trait.getStringValue();
if (!value.isPresent()) {
events.add(error(member, member.expectTrait(EnumValueTrait.class),
"The enumValue trait must use the string option when applied to enum shapes."));
"enum members can only be assigned string values, but found: "
+ Node.printJson(trait.toNode())));
} else {
if (!values.add(value.get())) {
events.add(error(member, String.format(
"Multiple enum members found with duplicate value `%s`",
value.get()
)));
events.add(error(member, String.format("Multiple enum members found with duplicate value `%s`",
value.get())));
}
if (value.get().equals("")) {
events.add(error(member, "enum values may not be empty."));
@ -82,25 +84,49 @@ public final class EnumShapeValidator extends AbstractValidator {
private void validateIntEnumShape(List<ValidationEvent> events, IntEnumShape shape) {
Set<Integer> values = new HashSet<>();
for (MemberShape member : shape.members()) {
// intEnum must all have the EnumValueTrait.
if (!member.hasTrait(EnumValueTrait.ID)) {
events.add(missingIntEnumValue(member, member));
} else if (!member.expectTrait(EnumValueTrait.class).getIntValue().isPresent()) {
events.add(missingIntEnumValue(member, member.expectTrait(EnumValueTrait.class)));
} else {
int value = member.expectTrait(EnumValueTrait.class).getIntValue().get();
if (!values.add(value)) {
events.add(error(member, String.format(
"Multiple enum members found with duplicate value `%s`",
value
)));
}
continue;
}
EnumValueTrait trait = member.expectTrait(EnumValueTrait.class);
// The EnumValueTrait must point to a number.
if (!trait.getIntValue().isPresent()) {
ValidationEvent event = error(member, trait, "intEnum members require integer values, but found: "
+ Node.printJson(trait.toNode()));
events.add(event);
continue;
}
NumberNode number = trait.toNode().asNumberNode().get();
// Validate the it is an integer.
if (number.isFloatingPointNumber()) {
events.add(error(member, trait, "intEnum members do not support floating point values: "
+ number.getValue()));
continue;
}
long longValue = number.getValue().longValue();
if (longValue > Integer.MAX_VALUE || longValue < Integer.MIN_VALUE) {
events.add(error(member, trait, "intEnum members must fit within an integer, but found: "
+ longValue));
continue;
}
if (!values.add(number.getValue().intValue())) {
events.add(error(member, String.format("Multiple intEnum members found with duplicate value `%d`",
number.getValue().intValue())));
}
validateEnumMemberName(events, member);
}
}
private ValidationEvent missingIntEnumValue(Shape shape, FromSourceLocation sourceLocation) {
return error(shape, sourceLocation, "intEnum members must have the enumValue trait with the `int` member set");
return error(shape, sourceLocation, "intEnum members must be assigned an integer value");
}
private void validateEnumMemberName(List<ValidationEvent> events, MemberShape member) {

View File

@ -873,4 +873,17 @@ public class ModelAssemblerTest {
assertThat(createdMember.getAllTraits(), not(hasKey(BoxTrait.ID)));
}
@Test
public void canResolveTargetsWithoutPrelude() {
ValidatedResult<Model> model = Model.assembler()
.disablePrelude()
.addUnparsedModel("foo.smithy", "$version: \"2.0\"\n"
+ "namespace smithy.example\n"
+ "list Foo { member: String }\n")
.assemble();
assertThat(model.getValidationEvents(), hasSize(1));
assertThat(model.getValidationEvents().get(0).getMessage(), containsString("unresolved shape"));
}
}

View File

@ -1 +1 @@
[ERROR] com.foo#List: Parse error at line 7, column 11 near `: `: Duplicate member of com.foo#List: 'member' | Model
[ERROR] -: Parse error at line 7, column 5 near `member`: Expected: '}', but found 'm' | Model

View File

@ -1 +1 @@
[ERROR] com.foo#Map: Parse error at line 7, column 8 near `: `: Duplicate member of com.foo#Map: 'key' | Model
[ERROR] -: Parse error at line 7, column 5 near `key`: Expected: '}', but found 'k' | Model

View File

@ -1 +1 @@
[ERROR] com.foo#Set: Parse error at line 7, column 11 near `: `: Duplicate member of com.foo#Set: 'member' | Model
[ERROR] -: Parse error at line 7, column 5 near `member`: Expected: '}', but found 'm' | Model

View File

@ -1,5 +0,0 @@
[ERROR] smithy.example#IntEnum$FLOAT: Error creating trait `enumValue`: Enum values may not use fractional numbers. | Model
[ERROR] smithy.example#IntEnum$ARRAY: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model
[ERROR] smithy.example#IntEnum$MAP: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model
[ERROR] smithy.example#IntEnum$NULL: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model
[ERROR] smithy.example#IntEnum$BOOLEAN: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model

View File

@ -1,9 +1,9 @@
[ERROR] ns.foo#StringEnum$INT_VALUE: The enumValue trait must use the string option when applied to enum shapes. | EnumShape
[ERROR] ns.foo#StringEnum$INT_VALUE: enum members can only be assigned string values, but found: 1 | EnumShape
[ERROR] ns.foo#StringEnum$DUPLICATE_VALUE: Multiple enum members found with duplicate value `explicit` | EnumShape
[WARNING] ns.foo#StringEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape
[ERROR] ns.foo#IntEnum$IMPLICIT_VALUE: intEnum members must have the enumValue trait with the `int` member set | EnumShape
[ERROR] ns.foo#IntEnum$STRING_VALUE: intEnum members must have the enumValue trait with the `int` member set | EnumShape
[ERROR] ns.foo#IntEnum$DUPLICATE_VALUE: Multiple enum members found with duplicate value `1` | EnumShape
[ERROR] ns.foo#IntEnum$IMPLICIT_VALUE: intEnum members must be assigned an integer value | EnumShape
[ERROR] ns.foo#IntEnum$STRING_VALUE: intEnum members require integer values, but found: "foo" | EnumShape
[ERROR] ns.foo#IntEnum$DUPLICATE_VALUE: Multiple intEnum members found with duplicate value `1` | EnumShape
[WARNING] ns.foo#IntEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape
[WARNING] ns.foo#EnumWithEnumTrait: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait
[ERROR] ns.foo#EnumWithEnumTrait: Trait `enum` cannot be applied to `ns.foo#EnumWithEnumTrait`. This trait may only be applied to shapes that match the following selector: string :not(enum) | TraitTarget

View File

@ -0,0 +1,5 @@
[ERROR] smithy.example#IntEnum$FLOAT: intEnum members do not support floating point values: 1.1 | EnumShape
[ERROR] smithy.example#IntEnum$ARRAY: intEnum members require integer values, but found: [1] | EnumShape
[ERROR] smithy.example#IntEnum$MAP: intEnum members require integer values, but found: {"foo":"bar"} | EnumShape
[ERROR] smithy.example#IntEnum$NULL: intEnum members require integer values, but found: null | EnumShape
[ERROR] smithy.example#IntEnum$BOOLEAN: intEnum members require integer values, but found: true | EnumShape

View File

@ -0,0 +1,7 @@
// Parse error at line 6, column 17 near `= `: @default assignment is only supported in IDL version 2 or later | Model
$version: "1.0"
namespace smithy.example
structure Foo {
baz: String = "hello"
}

View File

@ -1,4 +1,4 @@
// Members can only elide targets in IDL version 2 or later. Attempted to elide a target with version `1.0`.
// Members can only elide targets in IDL version 2 or later
$version: "1.0"
namespace smithy.example

View File

@ -0,0 +1,7 @@
// Parse error at line 6, column 5 near `$m`: Members can only elide targets in IDL version 2 or later
$version: "1.0"
namespace smithy.example
list Foos {
$member
}

View File

@ -0,0 +1,7 @@
// Parse error at line 6, column 9 near `\n}`: Members can only elide targets in IDL version 2 or later
$version: "1.0"
namespace smithy.example
structure Foo {
$bar
}

View File

@ -1,4 +1,4 @@
// smithy.example#Foo$BAR: enum shapes require string values but found: 10
// smithy.example#Foo$BAR: enum members can only be assigned string values, but found: 10 | EnumShape
$version: "2.0"
namespace smithy.example

View File

@ -1,4 +1,4 @@
// [ERROR] smithy.example#Foo$BAR: intEnum shapes require integer values but found: "Abc"
// smithy.example#Foo$BAR: intEnum members require integer values, but found: "Abc"
$version: "2.0"
namespace smithy.example

View File

@ -1,4 +1,4 @@
// [ERROR] smithy.example#Foo$BAR: intEnum shapes do not support floating point values
// smithy.example#Foo$BAR: intEnum members do not support floating point values
$version: "2.0"
namespace smithy.example

View File

@ -1,4 +1,4 @@
// smithy.example#Foo$BAR: intEnum must fit within an integer, but found: 2147483648
// smithy.example#Foo$BAR: intEnum members must fit within an integer, but found: 2147483648
$version: "2.0"
namespace smithy.example

View File

@ -1,4 +1,4 @@
// enum must have at least one entry
// smithy.example#Enum: enum must have at least one entry | Model
$version: "2.0"

View File

@ -1,4 +1,4 @@
// Parse error at line 5, column 6 near `: `: Unexpected member of com.foo#MyList: 'foo' | Model
// Parse error at line 5, column 3 near `foo`: Expected: '}', but found 'f' | Model
namespace com.foo
list MyList {

View File

@ -1,4 +1,4 @@
// Parse error at line 7, column 7 near `: `: Unexpected member of com.foo#MyMap: 'fuzz'
// Parse error at line 7, column 3 near `fuzz`: Expected: '}', but found 'f' | Model
namespace com.foo
map MyMap {

View File

@ -1,4 +1,4 @@
// Parse error at line 5, column 6 near `: `: Unexpected member of com.foo#MySet: 'foo'
// Parse error at line 5, column 3 near `foo`: Expected: '}', but found 'f' | Model
namespace com.foo
set MySet {

View File

@ -51,10 +51,7 @@ structure L {
structure M {
@deprecated
@since("2.0")
foo:
E,
foo:E,
@deprecated
baz
:
H
baz:H
}

View File

@ -55,10 +55,7 @@ union L {
union M {
@deprecated
@since("2.0")
foo:
E,
foo:E,
@deprecated
baz
:
H
baz:H
}