Skip to content
Open
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
@@ -0,0 +1,81 @@
/*
* Copyright 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.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.codegen.smithy;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import software.amazon.awssdk.codegen.model.service.PaginatorDefinition;
import software.amazon.awssdk.codegen.smithy.traits.PaginatorOverrideTrait;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.TopDownIndex;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.traits.PaginatedTrait;

/**
* Builds a service's paginator definitions, keyed by operation name as in {@code paginators-1.json}. Smithy
* counterpart to loading that file.
*
* <p>An operation's {@link PaginatorOverrideTrait} wins. Otherwise its {@code @paginated} trait is translated, with the
* service's {@code @paginated} filling the members the operation leaves out, as Smithy's {@code PaginatedIndex} does.
*/
final class AddSmithyPaginators {

private AddSmithyPaginators() {
}

static Map<String, PaginatorDefinition> paginators(Model model, ServiceShape service) {
PaginatedTrait serviceDefaults = service.getTrait(PaginatedTrait.class).orElse(null);
Map<String, PaginatorDefinition> paginators = new TreeMap<>();
for (OperationShape operation : TopDownIndex.of(model).getContainedOperations(service)) {
String name = operation.getId().getName();
Optional<PaginatorOverrideTrait> override = operation.getTrait(PaginatorOverrideTrait.class);
if (override.isPresent()) {
if (!override.get().isSkip()) {
paginators.put(name, fromOverride(override.get()));
}
continue;
}
operation.getTrait(PaginatedTrait.class)
.ifPresent(trait -> paginators.put(name, fromPaginated(trait.merge(serviceDefaults))));
}
return paginators;
}

/**
* {@code @paginated} has no {@code moreResults} and one member per field, so each list has at most one element.
*/
private static PaginatorDefinition fromPaginated(PaginatedTrait trait) {
PaginatorDefinition definition = new PaginatorDefinition();
definition.setInputToken(trait.getInputToken().map(Collections::singletonList).orElse(null));
definition.setOutputToken(trait.getOutputToken().map(Collections::singletonList).orElse(null));
definition.setResultKey(trait.getItems().map(Collections::singletonList).orElse(null));
definition.setLimitKey(trait.getPageSize().orElse(null));
return definition;
}

private static PaginatorDefinition fromOverride(PaginatorOverrideTrait override) {
PaginatorDefinition definition = new PaginatorDefinition();
definition.setInputToken(override.getInputToken());
definition.setOutputToken(override.getOutputToken());
definition.setResultKey(override.getResultKey().orElse(null));
definition.setMoreResults(override.getMoreResults().orElse(null));
definition.setLimitKey(override.getLimitKey().orElse(null));
return definition;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import software.amazon.awssdk.codegen.model.service.AuthType;
import software.amazon.awssdk.codegen.model.service.ClientContextParam;
import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel;
import software.amazon.awssdk.codegen.model.service.PaginatorDefinition;
import software.amazon.awssdk.codegen.naming.DefaultSmithyNamingStrategy;
import software.amazon.awssdk.codegen.naming.NamingStrategy;
import software.amazon.awssdk.codegen.utils.ProtocolUtils;
Expand All @@ -64,7 +65,7 @@
* {@link software.amazon.awssdk.codegen.IntermediateModelBuilder}.
*
* <p>Source-model customizations are expected to have been applied to the model as smithy-build
* transforms. Paginators and waiters are not applied yet.
* transforms. Paginators come from {@code @paginated} and codegen overrides; waiters are not applied yet.
*/
public final class SmithyIntermediateModelBuilder {

Expand Down Expand Up @@ -143,6 +144,11 @@ public IntermediateModel build() {
new TreeMap<>(new AddSmithyOperations(model, service, namingStrategy, customConfig, protocol)
.constructOperations());

Map<String, PaginatorDefinition> paginators = AddSmithyPaginators.paginators(model, service);
// The client methods follow isPaginated and the paginator classes follow the map, so the two must agree.
operations.forEach((name, operation) -> operation.setPaginated(
paginators.containsKey(name) && paginators.get(name).hasAllRequiredFields()));

OperationModel endpointOperation = null;
boolean endpointCacheRequired = false;
for (OperationModel o : operations.values()) {
Expand All @@ -164,6 +170,7 @@ public IntermediateModel build() {
}

operations.entrySet().removeIf(e -> customConfig.getDeprecatedOperations().contains(e.getKey()));
paginators.entrySet().removeIf(e -> customConfig.getDeprecatedOperations().contains(e.getKey()));

log.info("{} shapes found in total.", shapes.size());

Expand All @@ -172,7 +179,7 @@ public IntermediateModel build() {
IntermediateModel fullModel = new IntermediateModel(
AddSmithyMetadata.constructMetadata(model, service, serviceIndex, namingStrategy, customConfig),
operations, shapes, customConfig, endpointOperation,
Collections.emptyMap(), namingStrategy, Collections.emptyMap(),
paginators, namingStrategy, Collections.emptyMap(),
endpointRuleSet, endpointTestSuiteModel, clientContextParams);

SmithyIntermediateModelPostprocessor.create(customConfig).postprocess(fullModel);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright 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.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.codegen.smithy.traits;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.smithy.model.node.ExpectationNotMetException;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.AbstractTrait;
import software.amazon.smithy.model.traits.Trait;

/**
* {@code software.amazon.awssdk.codegen#paginatorOverride}: the paginator to generate for an operation instead of the
* one {@code @paginated} produces, for a {@code paginators-1.json} entry that {@code @paginated} cannot express, or
* {@code skip} to generate none. Each member means what the {@code paginators-1.json} field of the same name means.
*/
@SdkInternalApi
public final class PaginatorOverrideTrait extends AbstractTrait {
public static final ShapeId ID = ShapeId.from("software.amazon.awssdk.codegen#paginatorOverride");

private static final String SKIP = "skip";
private static final String INPUT_TOKEN = "inputToken";
private static final String OUTPUT_TOKEN = "outputToken";
private static final String RESULT_KEY = "resultKey";
private static final String MORE_RESULTS = "moreResults";
private static final String LIMIT_KEY = "limitKey";

private final ObjectNode node;
private final boolean skip;
private final List<String> inputToken;
private final List<String> outputToken;
private final List<String> resultKey;
private final String moreResults;
private final String limitKey;

public PaginatorOverrideTrait(ObjectNode node) {
super(ID, node);
node.expectNoAdditionalProperties(Arrays.asList(SKIP, INPUT_TOKEN, OUTPUT_TOKEN, RESULT_KEY, MORE_RESULTS,
LIMIT_KEY));
this.node = node;
this.skip = node.getMember(SKIP).map(n -> n.expectBooleanNode().getValue()).orElse(false);
this.inputToken = strings(node, INPUT_TOKEN);
this.outputToken = strings(node, OUTPUT_TOKEN);
this.resultKey = strings(node, RESULT_KEY);
this.moreResults = node.getMember(MORE_RESULTS).map(n -> n.expectStringNode().getValue()).orElse(null);
this.limitKey = node.getMember(LIMIT_KEY).map(n -> n.expectStringNode().getValue()).orElse(null);

if (skip && node.getMembers().size() > 1) {
throw new ExpectationNotMetException(ID + ": skip cannot be combined with other members", node);
}
if (!skip && (isNullOrEmpty(inputToken) || isNullOrEmpty(outputToken))) {
throw new ExpectationNotMetException(
ID + " needs inputToken and outputToken unless skip is true. Remove the trait to use @paginated.", node);
}
}

/**
* True to generate no paginator for the operation. The other members are then absent.
*/
public boolean isSkip() {
return skip;
}

/**
* Empty only when {@link #isSkip()}.
*/
public List<String> getInputToken() {
return inputToken == null ? Collections.emptyList() : inputToken;
}

/**
* Empty only when {@link #isSkip()}.
*/
public List<String> getOutputToken() {
return outputToken == null ? Collections.emptyList() : outputToken;
}

public Optional<List<String>> getResultKey() {
return Optional.ofNullable(resultKey);
}

public Optional<String> getMoreResults() {
return Optional.ofNullable(moreResults);
}

public Optional<String> getLimitKey() {
return Optional.ofNullable(limitKey);
}

@Override
protected Node createNode() {
return node;
}

private static List<String> strings(ObjectNode node, String member) {
return node.getMember(member)
.map(n -> n.expectArrayNode().getElements().stream()
.map(element -> element.expectStringNode().getValue())
.collect(Collectors.toList()))
.map(Collections::unmodifiableList)
.orElse(null);
}

private static boolean isNullOrEmpty(List<String> list) {
return list == null || list.isEmpty();
}

/**
* Registered in {@code META-INF/services} so Smithy builds this class when it loads the trait.
*/
public static final class Provider extends AbstractTrait.Provider {
public Provider() {
super(ID);
}

@Override
public Trait createTrait(ShapeId target, Node value) {
return new PaginatorOverrideTrait(value.expectObjectNode());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ software.amazon.awssdk.codegen.smithy.traits.DeprecatedNameTrait$Provider
software.amazon.awssdk.codegen.smithy.traits.EnumConstantNameTrait$Provider
software.amazon.awssdk.codegen.smithy.traits.IgnoreDataTypeConversionFailuresTrait$Provider
software.amazon.awssdk.codegen.smithy.traits.SyntheticTrait$Provider
software.amazon.awssdk.codegen.smithy.traits.PaginatorOverrideTrait$Provider
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,21 @@ string enumConstantName
@trait(selector: ":is(simpleType, list, map, structure, union)")
structure synthetic {}


/// Replaces the paginator that @paginated produces for this operation, for a paginator @paginated cannot express.
/// Each member means what the paginators-1.json field of the same name means, and names members by their Smithy
/// member names. With skip, no paginator is generated.
@trait(selector: "operation")
structure paginatorOverride {
skip: Boolean
inputToken: PaginatorMemberPaths
outputToken: PaginatorMemberPaths
resultKey: PaginatorMemberPaths
moreResults: String
limitKey: String
}

@private
list PaginatorMemberPaths {
member: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -390,21 +390,6 @@ void unsignedPayload_setsFlag() {
assertThat(op(model, "rest-json").isUnsignedPayload()).isTrue();
}

// ---- Pagination (deferred to PR 15) -----------------------------------

@Test
void paginatedTrait_doesNotSetPaginated() {
Model model = modelOf(
"@paginated(inputToken: \"nextToken\", outputToken: \"nextToken\", items: \"items\")\n"
+ "@http(method: \"POST\", uri: \"/op\")\n"
+ "operation Op { input: OpRequest, output: OpResponse }\n"
+ "structure OpRequest { nextToken: String }\n"
+ "structure OpResponse { nextToken: String, items: StringList }\n"
+ "list StringList { member: String }\n");
// Pagination is deferred to PR 15; isPaginated stays false here.
assertThat(op(model, "rest-json").isPaginated()).isFalse();
}

// ---- Endpoint discovery -----------------------------------------------

@Test
Expand Down
Loading
Loading