Using the Semantic Model#

The Java reference implementation of Smithy provides various abstractions to interact with the in-memory semantic model. This document provides a kind of "cookbook" for achieving various tasks with the Smithy model.

Traversing the model#

Each of the following examples assume a variable named model is defined that is a software.amazon.smithy.model.Model.

Iterate over all shapes#

Model#toSet is a cheap operation that just provides a Set view over a model.

for (Shape shape : model.toSet()) {
    // ...
}

Iterate over all shapes of a specific type#

Each type of shape in Smithy has a dedicated Model#getXShapes method. These methods are cheap to invoke. They just provide a filtered Set view over a model.

for (ServiceShape shape : model.getServiceShapes()) {
    // ...
}

for (StructureShape shape : model.getStructureShapes()) {
    // ...
}

for (MemberShape shape : model.getMemberShapes()) {
    // ...
}

// etc...

Iterate over all shapes with a specific trait#

Model#getShapesWithTrait returns shapes that have a specific trait. This is a cheap method to call and uses caches internally. The provided trait class can be retrieved from each returned shape. The following example uses DeprecatedTrait but any trait class can be used.

for (Shape shape : model.getShapesWithTrait(DeprecatedTrait.class)) {
    DeprecatedTrait trait = shape.expectTrait(DeprecatedTrait.class);
}

Iterate over shapes of a specific type with a specific trait#

Model#getXShapesWithTrait returns shapes of type X that have a specific trait. Each type of shape has a dedicated Model#getXShapesWithTrait method. This is a cheap method to call and uses caches internally. The provided trait class can be retrieved from each returned shape. The following example uses SensitiveTrait but any trait class can be used.

for (StructureShape shape : model.getStructureShapesWithTrait(SensitiveTrait.class)) {
    SensitiveTrait trait = shape.expectTrait(SensitiveTrait.class);
}

for (StringShape shape : model.getStringShapesWithTrait(SensitiveTrait.class)) {
    SensitiveTrait trait = shape.expectTrait(SensitiveTrait.class);
}

// etc..

Stream over all shapes#

Stream<StringShape> strings = model.shapes(StringShape.class)
    .filter(shape -> shape.getId().getNamespace().equals("foo.bar"));

Tip

In general, prefer the named methods that convert Model to a set. However, it's sometimes useful to break down complicated pipeline style transformations into streams.

Traversing the members of a shape#

StructureShape struct;

for (MemberShape member : struct.members()) {
    // Get the shape targeted by the member.
    Shape target = model.expectShape(member.getTarget());
    System.out.println(member.getMemberName() + " targets " + target);

    // Get that container of the member.
    Shape container = model.expectShape(member.getContainer());
}

Note

  • Members are ordered based on the order given in the Smithy model
  • You can order the members differently if needed (for example sorting them using a TreeMap).
  • The above code works the same way for any shape, whether it's a structure, union, list, or map.
  • By the time a code generator is running, the model has been thoroughly validated. You should use the various methods that start with expect to more easily interact with shapes.

Visiting shapes#

Smithy often relies on visitors to dispatch to different typed methods for handling different kinds of shapes.

// Silly example that returns the numbers of members a shape has.
ShapeVisitor<Integer> visitor = new ShapeVisitor.Default<Integer>() {
    @Override
    protected Integer getDefault(Shape shape) {
        return 0;
    }

    @Override
    public Integer listShape(ListShape shape) {
        return 1;
    }

    @Override
    public Integer mapShape(MapShape shape) {
        return 2;
    }

    @Override
    public Integer structureShape(StructureShape shape) {
        return shape.members().size();
    }

    @Override
    public Integer unionShape(UnionShape shape) {
        return shape.members().size();
    }
};

StringShape string = exampleThatGetsString();
int count = string.accept(visitor);
assert(count == 0);

Note

  • The accept method of a shape is used to apply a visitor to the shape.
  • You should typically use the Visitor.Default implementation to implement a visitor.
  • A simpler way to get the answer of the above example is to just call shape.members().size().

Knowledge Indexes#

Smithy provides various knowledge index implementations that are used to break down more complex tasks into easily queried, pre-computed data stores. These knowledge indexes are also cached on a Model object, making them cheaper to use than recomputing information multiple times across things like validators.

Get every operation in a service or resource#

Service shapes can contain resources which can contain operations. TopDownIndex will walk the service/resource to find all contained operations.

TopDownIndex index = TopDownIndex.of(model);
index.getContainedOperations(serviceShape);

Get every resource in a service or resource#

Service shapes can contain resources which can themselves contain resources. TopDownIndex will walk the service/resource to find all contained operations.

TopDownIndex index = TopDownIndex.of(model);
index.getContainedResources(serviceShape);

Determine if a member is nullable#

Taking the version of the Smithy IDL into account when computing the nullability of a member can be complex. NullableIndex hides all of this complexity by providing a simple boolean result for a given member shape.

NullableIndex index = NullableIndex.of(model);

if (index.isMemberNullable(someMemberShape)) {
    // nullable
}

Get pagination information about an operation#

Resolving information about paginated operations in Smithy requires some bookkeeping. PaginatedIndex tries to consolidate all the information you might need when interacting with paginated traits.

PaginatedIndex index = PaginatedIndex.of(model);

index.getPaginationInfo(service, operation).ifPresenet(info -> {
    // method invoked if the operation is paginated.
    System.out.println("Service shape: " + info.getService());
    System.out.println("Operation shape: " + info.getOperation());
    System.out.println("Input shape: " + info.getInput());
    System.out.println("Output shape: " + info.getOutput());
    System.out.println("Paginated trait: " + info.getPaginatedTrait());
    System.out.println("Input token member: " + info.getInputTokenMember());
    System.out.println("Output token membber: " + info.getOutputTokenMemberPath());
    // etc...
});

Get the HTTP binding response status code of an operation#

The HttpBindingIndex can provide all kinds of information about the HTTP bindings of an operation, including the response status code.

HttpBindingIndex index = HttpBindingIndex.of(model);
int code = index.getResponseCode(operationShape);

Get the request content-type of an operation#

HttpBindingIndex can attempt to resolve the Content-Type header of a request. The content-type might not be statically known by the model and might rely on protocol-specific information.

HttpBindingIndex index = HttpBindingIndex.of(model);

String defaultPayloadType = "application/json";
String eventStreamType = "application/vnd.amazon.event-stream");
String contentType = index
    .determineRequestContentType(operation, defaultPayloadType, eventStreamType)
    .orElseNull();

Get the response content-type of an operation#

HttpBindingIndex can attempt to resolve the Content-Type header of a response. The content-type might not be statically known by the model and might rely on protocol-specific information.

HttpBindingIndex index = HttpBindingIndex.of(model);

String defaultPayloadType = "application/json";
String eventStreamType = "application/vnd.amazon.event-stream");
String contentType = index
    .determineResponseContentType(operation, defaultPayloadType, eventStreamType)
    .orElseNull();

Get HTTP binding information of an operation#

HttpBindingIndex index = HttpBindingIndex.of(model);
var requestBindings = index.getRequestBindings(operationShape);
var responseBindings = index.getResponseBindings(operationShape);

// This loop works the same way for request or response bindings.
for (var entry : requestBindings.entrySet()) {
    String memberName = entry.getKey();
    HttpBinding binding = entry.getValue();
    System.out.println("Member: " + memberName);
    System.out.println("Member shape: " + binding.getMember());
    System.out.println("Location: " + binding.getLocation());
    System.out.println("Location name: " + binding.getLocationName());
    binding.getBindingTrait().ifPresent(trait -> {
        System.out.println("Binding trait: " + trait);
    });
}

Get the timestamp format used for a specific HTTP binding#

// Determine the format used for members bound to HTTP labels.
HttpBindingIndex index = HttpBindingIndex.of(model);
var formatUsedInPayloads = TimestampFormatTrait.Format.EPOCH_SECONDS;
var format = index.determineTimestampFormat(
    member, HttpBinding.Location.LABEL, formatUsedInPayloads);

Get members that have specific HTTP bindings#

// Find every member in the input of the operation bound to an HTTP label.
HttpBindingIndex index = HttpBindingIndex.of(model);
var locationTypeToFind = HttpBinding.Location.LABEL;
var result = index.getRequestBindings(operation, locationTypeToFind);

Transforming the model#

It's often necessary to transform a Smithy model prior to code generation. For example, you might need to remove operations that use unsupported features, remove shapes that aren't in the closure of a service, or add traits to shapes that are specific to your code generator. Smithy provides a model transformation abstraction in ModelTransformer. ModelTransformer provides various methods for transforming a model, some of which are documented below.

Remove deprecated operations#

ModelTransformer will remove any broken relationships when a shape is removed. If you remove an operation from the model, it's removed from any service or resource.

model = ModelTransformer.create().removeShapesIf(shape -> {
    return shape.isOperationShape() && shape.hasTrait(DeprecatedTrait.class);
)};

Add a trait to every shape#

model = ModelTransformer.create().mapShapes(shape -> {
    return Shape.shapeToBuilder(shape).addTrait(new MyCustomTrait()).build();
});

Tip

You can convert any shape to a builder using the static method Shape#shapeToBuilder

Flattening mixins#

Mixins are used to share shape definitions across a model. They're essentially build-time copy and paste, and they have no meaningful impact on generated code. For example, the following model uses mixins:

@mixin
structure HasUsername {
    @required
    username: String
}

structure UserData with [HasUserName] {
    isAdmin: Boolean
}

Code generators should flatten mixins out of a model before generating code, allowing them to more easily generate code without needing to implement special handling for mixins. This can be done using a Smithy model transformation:

ModelTransformer transformer = ModelTransformer.create();
Model transformedModel = transformer.flattenAndRemoveMixins(model);

After flattening mixins, the above model is equivalent to:

structure UserData with [HasUserName] {
    @required
    username: String

    isAdmin: Boolean
}

Copying service errors to operation errors#

Service shapes can define a set of errors that can be returned from any operation. While this is great for modeling a service, it makes code generation harder.

For example:

service MyService {
    operations: GetSomething
    errors: [ValidationError]
}

operation GetSomething {
    input := {}
    output := {}
}

Code generators can flatten these errors using a model transformer:

ModelTransformer transformer = ModelTransformer.create();
Model transformed = transformer.copyServiceErrorsToOperations(model, service);

After flattening the error hierarchy, the above model is equivalent to:

service MyService {
    operations: GetSomething
}

operation GetSomething {
    input := {}
    output := {}
    errors: [ValidationError]
}

Remove shapes not in the closure of a service#

Smithy models can contain multiple services and shapes that aren't connected to any service. Code generation is often easier if you remove shapes from the model that are not connected to the service being generated.

Walker walker = new Walker(someModel);
Set<Shape> closure = walker.walkShapes(someService);
model = ModelTransformer.create().removeShapesIf(shape -> !closure.contains(shape));

Selectors#

Selectors are used to find shapes in the model that match a query. While you should typically not need selectors when writing Java code, they can sometimes make getting the desired set of shapes far simpler than writing complex loops and conditionals. Selectors have similar caveats as regular expressions: selectors are slower than handwritten code, and sometimes handwritten code is easier to understand than the DSL. Whether a selector is appropriate for a given use case will mostly depend on the complexity of the query and if there's already a built-in abstraction for what you're trying to do.

Creating Selectors#

Let's say you want to find something complex, like every operation that has a streaming trait member in its input. This can be achieved through the following selector:

Selector selector = Selector.parse("operation :test(-[input]-> structure > member > [trait|streaming])");

Finding shapes that match a selector#

Selector#select finds every matching shape and put them in a Set.

Set<Shape> matches = selector.select(model);

Iterate over shapes that match a selector#

If the result set does not need to be loaded into memory, then using shapes() is cheaper than using select().

selector.shapes().forEach(shape -> {
    // do something with each shape
});

Reuse parsed Selectors#

Be sure to use a previously parsed selector if a selector will be used repeatedly. For example don't do this:

// ❌ DON'T DO THIS ❌

for (var shape : model.getServiceShapes()) {
    // This is bad! Reuse Selector instances!
    // This has to parse the selector in each iteration of the loop.
    Selector selector = Selector.parse(String.format(
        "[id=%s] -> structure > member[trait|required]",
        shape.getId()));

    selector.shapes(model).forEach(match -> {
        // do something with each found shape
    });
}

Instead, do this:

// ✅ DO THIS

Selector selector = Selector.parse(String.format(
    "[id=%s] -> structure > member[trait|required]",
    shape.getId()));

for (var shape : model.getServiceShapes()) {
    selector.shapes(model).forEach(match -> {
        // do something with each found shape
    });
}