Skip to content

Commit 22fe64a

Browse files
Add ability to elide targets for mixins/resources
This adds in syntactic sugar that allows modelers to elide targets for members that are inherited from mixins or defined in resources. This necessarily also adds the ability for a structure to indicate a resource to pull potential targets from. This is all syntactic sugar for the IDL, so the JSON AST isn't impacted. This is accomplished by refactoring the code used for mixins to instead be a generic system for applying modifications to a shape after some set of dependencies has been resolved.
1 parent 1922f41 commit 22fe64a

20 files changed

+973
-89
lines changed

docs/source/1.0/guides/migrating-idl-1-to-2.rst

+60-17
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,6 @@ having to copy and paste them. The following model:
199199
200200
namespace smithy.example
201201
202-
resource User {
203-
identifiers: {
204-
email: String,
205-
id: String,
206-
},
207-
read: GetUser
208-
}
209-
210202
operation GetUser {
211203
input: GetUserInput,
212204
output: GetUserOutput
@@ -240,15 +232,6 @@ Can be updated to:
240232
241233
namespace smithy.example
242234
243-
resource User {
244-
identifiers: {
245-
email: String
246-
id: String
247-
username: String
248-
},
249-
read: GetUser
250-
}
251-
252235
@mixin
253236
structure UserIdentifiers {
254237
@required
@@ -274,6 +257,66 @@ that otherwise have to be copied and pasted.
274257
work.
275258

276259

260+
Use the target elision syntax sugar to reduce boilerplate
261+
---------------------------------------------------------
262+
263+
Resource shapes contain a set of identifiers, but when writing structures that
264+
contain those identifiers you have to duplicate those definitions entirely. In
265+
IDL 2.0, you can use the target elision syntax with a structure bound to a
266+
resource. For example:
267+
268+
.. code-block:: smithy
269+
270+
$version: "2.0"
271+
272+
namespace smithy.example
273+
274+
resource User {
275+
identifiers: {
276+
id: String
277+
email: String
278+
}
279+
}
280+
281+
// The `for` syntax here determines which resource should be checked.
282+
structure UserDetails for User {
283+
// With this syntax, the target is automatically inferred from the
284+
// resource.
285+
$id
286+
287+
// Uncomment this to include an email member. Unlike with mixins, you
288+
// must opt in to the members that you want to include. This allows you
289+
// to have partial views of a resource, such as in a create operation
290+
// that does not bind all of the identifiers.
291+
// $email
292+
293+
address: String
294+
}
295+
296+
This syntax can also be used with mixins to more succinctly add additional
297+
traits to included members.
298+
299+
.. code-block:: smithy
300+
301+
$version: "2.0"
302+
303+
namespace smithy.example
304+
305+
@mixin
306+
structure UserIdentifiers {
307+
id: String
308+
email: String
309+
}
310+
311+
structure UserDetails with [UserIdentifiers] {
312+
@required
313+
$id
314+
315+
@required
316+
$email
317+
}
318+
319+
277320
Remove unsightly commas
278321
-----------------------
279322

docs/source/1.0/spec/core/idl.rst

+113-3
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,14 @@ The Smithy IDL is defined by the following ABNF:
190190
enum_shape_statement :`enum_type_name` `ws` `identifier` [`mixins`] `ws` `enum_shape_members`
191191
enum_type_name :"enum" / "intEnum"
192192
enum_shape_members :"{" `ws` 1*(`trait_statements` `identifier` `ws`) "}"
193-
shape_members :"{" `ws` *(`shape_member_kvp` `ws`) "}"
194-
shape_member_kvp :`trait_statements` `identifier` `ws` ":" `ws` `shape_id`
193+
shape_members :"{" `ws` *(`trait_statements` `shape_member` `ws`) "}"
194+
shape_member :`shape_member_kvp` / `shape_member_elided`
195+
shape_member_kvp :`identifier` `ws` ":" `ws` `shape_id`
196+
shape_member_elided :"$" `identifier`
195197
list_statement :"list" `ws` `identifier` [`mixins`] `ws` `shape_members`
196198
set_statement :"set" `ws` `identifier` [`mixins`] `ws` `shape_members`
197199
map_statement :"map" `ws` `identifier` [`mixins`] `ws` `shape_members`
198-
structure_statement :"structure" `ws` `identifier` [`mixins`] `ws` `shape_members`
200+
structure_statement :"structure" `ws` `identifier` ["for" `shape_id`] [`mixins`] `ws` `shape_members`
199201
union_statement :"union" `ws` `identifier` [`mixins`] `ws` `shape_members`
200202
service_statement :"service" `ws` `identifier` [`mixins`] `ws` `node_object`
201203
operation_statement :"operation" `ws` `identifier` [`mixins`] `ws` `inlineable_properties`
@@ -1408,6 +1410,11 @@ and defines a :ref:`read <read-lifecycle>` operation:
14081410
}
14091411
}
14101412

1413+
.. seealso::
1414+
1415+
The :ref:`target elision syntax <idl-target-elision>` for an easy way to
1416+
define structures that reference resource identifiers without having to
1417+
repeat the target definition.
14111418

14121419
.. _idl-mixins:
14131420

@@ -1438,6 +1445,109 @@ For example:
14381445
string SensitiveText with [SensitiveString]
14391446
14401447
1448+
.. _idl-target-elision:
1449+
1450+
Target Elision
1451+
--------------
1452+
1453+
Having to completely redefine a :ref:`resource identifier <resource-identifiers>`
1454+
to use it in a structure or redefine a member from a :ref:`mixin` to add
1455+
additional traits can be cumbersome and potentially error-prone. The
1456+
:token:`type elision syntax <smithy:shape_member_elided>` can be used to cut
1457+
down on that repetition by prefixing the member name with a ``$``. If a member
1458+
is prefixed this way, its target will automatically be set to the target of a
1459+
mixin member with the same name. The following example shows how to elide the
1460+
target for a member inherited from a mixin:
1461+
1462+
.. code-block:: smithy
1463+
1464+
$version: "2.0"
1465+
1466+
namespace smithy.example
1467+
1468+
@mixin
1469+
structure IdBearer {
1470+
id: String
1471+
}
1472+
1473+
structure IdRequired with [IdBearer] {
1474+
@required
1475+
$id
1476+
}
1477+
1478+
Additionally, structure shapes can reference a :ref:`resource <idl-resource>`
1479+
shape to define members that represent the resource's identifiers without having
1480+
to redefine the target shape. In addition to prefixing a member with ``$``, the
1481+
structure must also add ``for`` followed by the resource referenced in
1482+
the shape's definition before any mixins are specified.
1483+
1484+
To resolve elided types, first check if any bound resource defines an
1485+
identifier that case-sensitively matches the elided member name. If a match is
1486+
found, the type targeted by that identifier is used for the elided type. If no
1487+
identifier matches the elided member name, mixin members are case-sensitively
1488+
checked, and if a match is found, the type targeted by the mixin member is
1489+
used as the elided type. It is an error if neither the resource or mixin
1490+
members matches an elided member name.
1491+
1492+
The following example shows a structure reusing an identifier definition from
1493+
a resource:
1494+
1495+
.. code-block:: smithy
1496+
1497+
$version: "2.0"
1498+
1499+
namespace smithy.example
1500+
1501+
resource User {
1502+
identifiers: {
1503+
name: String
1504+
uuid: String
1505+
}
1506+
}
1507+
1508+
structure UserSummary for User {
1509+
$name
1510+
age: Short
1511+
}
1512+
1513+
Note that the ``UserSummary`` structure does not attempt to define the
1514+
``uuid`` identifier. When referencing a resource in this way, only the
1515+
identifiers that are explicitly referenced are added to the structure. This
1516+
allows structures to define subsets of identifiers, which can be useful for
1517+
operations like create operations where some of those identifiers may be
1518+
generated by the service.
1519+
1520+
Structures may only reference one resource shape in this way.
1521+
1522+
When using both mixins and a resource reference, the referenced resource will
1523+
be checked first. The following example is invalid:
1524+
1525+
.. code-block:: smithy
1526+
1527+
$version: "2.0"
1528+
1529+
namespace smithy.example
1530+
1531+
resource User {
1532+
identifiers: {
1533+
uuid: String
1534+
}
1535+
}
1536+
1537+
@mixin
1538+
structure UserIdentifiers {
1539+
uuid: Blob
1540+
}
1541+
1542+
// This is invalid because the `uuid` member's target is set to
1543+
// String, which then conflicts with the UserIdentifiers mixin.
1544+
structure UserSummary for User with [UserIdentifiers] {
1545+
$uuid
1546+
}
1547+
1548+
1549+
1550+
14411551
.. _documentation-comment:
14421552

14431553
Documentation comment

smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java

+36-47
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ abstract class AbstractMutableModelFile implements ModelFile {
5050
private final Set<ShapeId> allShapeIds = new HashSet<>();
5151
private final Map<ShapeId, AbstractShapeBuilder<?, ?>> shapes = new LinkedHashMap<>();
5252
private final Map<ShapeId, Map<String, MemberShape.Builder>> members = new HashMap<>();
53-
private final Map<ShapeId, Set<ShapeId>> pendingShapes = new HashMap<>();
53+
private final Map<ShapeId, List<PendingShapeModifier>> pendingModifications = new HashMap<>();
54+
private final Map<ShapeId, Set<ShapeId>> pendingDependencies = new HashMap<>();
5455
private final List<ValidationEvent> events = new ArrayList<>();
5556
private final MetadataContainer metadata = new MetadataContainer(events);
5657
private final TraitFactory traitFactory;
@@ -106,7 +107,13 @@ void onShape(AbstractShapeBuilder<?, ?> builder) {
106107
}
107108

108109
void addPendingMixin(ShapeId shape, ShapeId mixin) {
109-
pendingShapes.computeIfAbsent(shape, id -> new LinkedHashSet<>()).add(mixin);
110+
addPendingModification(shape, new ApplyMixin(mixin, events));
111+
}
112+
113+
void addPendingModification(ShapeId shape, PendingShapeModifier pendingModification) {
114+
pendingDependencies.computeIfAbsent(shape, id -> new LinkedHashSet<>())
115+
.addAll(pendingModification.getDependencies());
116+
pendingModifications.computeIfAbsent(shape, id -> new ArrayList<>()).add(pendingModification);
110117
}
111118

112119
private SourceException onConflict(AbstractShapeBuilder<?, ?> builder, AbstractShapeBuilder<?, ?> previous) {
@@ -181,13 +188,15 @@ public final CreatedShapes createShapes(TraitContainer resolvedTraits) {
181188
List<Shape> resolvedShapes = new ArrayList<>(shapes.size());
182189
List<PendingShape> pendingMixins = new ArrayList<>();
183190

184-
for (Map.Entry<ShapeId, Set<ShapeId>> entry : pendingShapes.entrySet()) {
191+
for (Map.Entry<ShapeId, List<PendingShapeModifier>> entry : this.pendingModifications.entrySet()) {
185192
ShapeId subject = entry.getKey();
186-
Set<ShapeId> mixins = entry.getValue();
193+
List<PendingShapeModifier> pendingModifications = entry.getValue();
194+
Set<ShapeId> dependencies = pendingDependencies.getOrDefault(subject, Collections.emptySet());
187195
AbstractShapeBuilder<?, ?> builder = shapes.get(entry.getKey());
188196
Map<String, MemberShape.Builder> builderMembers = claimMembersOfContainer(builder.getId());
189197
shapes.remove(entry.getKey());
190-
pendingMixins.add(createPendingShape(subject, builder, builderMembers, mixins, traitContainer));
198+
pendingMixins.add(createPendingShape(
199+
subject, builder, builderMembers, dependencies, pendingModifications, traitContainer));
191200
}
192201

193202
// Build members and add them to top-level shapes.
@@ -223,55 +232,21 @@ private PendingShape createPendingShape(
223232
AbstractShapeBuilder<?, ?> builder,
224233
Map<String, MemberShape.Builder> builderMembers,
225234
Set<ShapeId> mixins,
235+
List<PendingShapeModifier> pendingModifications,
226236
TraitContainer resolvedTraits
227237
) {
228238
return PendingShape.create(subject, builder, mixins, shapeMap -> {
229-
// Build normal members first.
230239
for (MemberShape.Builder memberBuilder : builderMembers.values()) {
240+
for (PendingShapeModifier pendingModification : pendingModifications) {
241+
pendingModification.modifyMember(builder, memberBuilder, resolvedTraits, shapeMap);
242+
}
231243
buildShape(memberBuilder, resolvedTraits).ifPresent(builder::addMember);
232244
}
233-
// Add each mixin and ensure there are no member conflicts.
234-
for (ShapeId mixin : mixins) {
235-
Shape mixinShape = shapeMap.get(mixin);
236-
for (MemberShape member : mixinShape.members()) {
237-
ShapeId targetId = builder.getId().withMember(member.getMemberName());
238-
Map<ShapeId, Trait> introducedTraits = traitContainer.getTraitsForShape(targetId);
239-
240-
MemberShape introducedMember = null;
241-
if (builderMembers.containsKey(member.getMemberName())) {
242-
introducedMember = builderMembers.get(member.getMemberName())
243-
.addMixin(member)
244-
.build();
245-
246-
if (!introducedMember.getTarget().equals(member.getTarget())) {
247-
// Members cannot be redefined if their targets conflict.
248-
MemberShape.Builder conflict = builderMembers.get(member.getMemberName());
249-
events.add(ValidationEvent.builder()
250-
.severity(Severity.ERROR)
251-
.id(Validator.MODEL_ERROR)
252-
.shapeId(conflict.getId())
253-
.sourceLocation(conflict.getSourceLocation())
254-
.message("Member conflicts with an inherited mixin member: " + member.getId())
255-
.build());
256-
}
257-
} else if (!introducedTraits.isEmpty()) {
258-
// Build local member copies before adding mixins if traits
259-
// were introduced to inherited mixin members.
260-
introducedMember = MemberShape.builder()
261-
.id(targetId)
262-
.target(member.getTarget())
263-
.source(member.getSourceLocation())
264-
.addTraits(introducedTraits.values())
265-
.addMixin(member)
266-
.build();
267-
}
268-
269-
if (introducedMember != null) {
270-
builder.addMember(introducedMember);
271-
}
272-
}
273-
builder.addMixin(mixinShape);
245+
246+
for (PendingShapeModifier pendingModification : pendingModifications) {
247+
pendingModification.modifyShape(builder, builderMembers, resolvedTraits, shapeMap);
274248
}
249+
275250
buildShape(builder, resolvedTraits).ifPresent(result -> shapeMap.put(result.getId(), result));
276251
});
277252
}
@@ -285,10 +260,24 @@ private <S extends Shape, B extends AbstractShapeBuilder<? extends B, S>> Option
285260
builder.addTrait(trait);
286261
}
287262
return Optional.of(builder.build());
263+
} catch (IllegalStateException e) {
264+
if (builder.getShapeType() == ShapeType.MEMBER && ((MemberShape.Builder) builder).getTarget() == null) {
265+
events.add(ValidationEvent.builder()
266+
.severity(Severity.ERROR)
267+
.id(Validator.MODEL_ERROR)
268+
.shapeId(builder.getId())
269+
.sourceLocation(builder.getSourceLocation())
270+
.message("Member target was elided, but no bound resource or mixin contained a matching "
271+
+ "identifier or member name.")
272+
.build());
273+
return Optional.empty();
274+
}
275+
throw e;
288276
} catch (SourceException e) {
289277
events.add(ValidationEvent.fromSourceException(e, "", builder.getId()));
290278
resolvedTraits.clearTraitsForShape(builder.getId());
291279
return Optional.empty();
292280
}
293281
}
282+
294283
}

0 commit comments

Comments
 (0)